| import { dirname, join } from "node:path"; |
| import { |
| type Api, |
| type ApiStreamOptions, |
| type AssistantMessage, |
| type AssistantMessageEventStream, |
| type AuthCheck, |
| type AuthInteraction, |
| type AuthOperationOptions, |
| type AuthResult, |
| type AuthType, |
| type Context, |
| type Credential, |
| type CredentialInfo, |
| type CredentialStore, |
| createModels, |
| type DeferredCancelOptions, |
| type DeferredFetchOptions, |
| type DeferredHandle, |
| lazyStream, |
| type Model, |
| type Models, |
| type ModelsApiStreamOptions, |
| type ModelsDeferredCancelOptions, |
| type ModelsDeferredFetchOptions, |
| ModelsError, |
| type ModelsRefreshOptions, |
| type ModelsRefreshResult, |
| type ModelsRequestTransforms, |
| type ModelsSimpleStreamOptions, |
| type ModelsStore, |
| type MutableModels, |
| type Provider, |
| type ProviderHeaders, |
| type ProviderRequestOptions, |
| type SimpleStreamOptions, |
| type StreamOptions, |
| } from "@earendil-works/pi-ai"; |
| import * as builtinProviderCatalog from "@earendil-works/pi-ai/providers/all"; |
| import { getAgentDir } from "../config.ts"; |
| import { operationSignal, raceWithAbortSignal } from "../utils/abort.ts"; |
| import { AuthStorage as DefaultAuthStorage } from "./auth-storage.ts"; |
| import { ModelConfig } from "./model-config.ts"; |
| import { FileModelsStore, InMemoryCodingAgentModelsStore } from "./models-store.ts"; |
| import { |
| type AuthStatus, |
| type CompatibilityRequestConfig, |
| composeModelProvider, |
| configuredRequestAuthStatus, |
| type ProviderConfigInput, |
| resolveCompatibilityRequestConfig, |
| resolveConfiguredModelHeaders, |
| validateExtensionProvider, |
| } from "./provider-composer.ts"; |
| import { withRemoteCatalog } from "./remote-catalog-provider.ts"; |
| import { RuntimeCredentials } from "./runtime-credentials.ts"; |
|
|
| interface ModelRuntimeSnapshot { |
| all: readonly Model<Api>[]; |
| available: readonly Model<Api>[]; |
| configuredProviders: ReadonlySet<string>; |
| storedProviders: ReadonlySet<string>; |
| auth: ReadonlyMap<string, AuthCheck | undefined>; |
| } |
|
|
| export interface CreateModelRuntimeOptions { |
| |
| credentials?: CredentialStore; |
| authPath?: string; |
| modelsPath?: string | null; |
| modelsStore?: ModelsStore; |
| modelsStorePath?: string; |
| |
| allowModelNetwork?: boolean; |
| |
| modelRefreshTimeoutMs?: number; |
| catalogBaseUrl?: string; |
| |
| signal?: AbortSignal; |
| |
| refreshOnCreate?: boolean; |
| } |
|
|
| export interface ModelRuntimeAuthOverrides extends AuthOperationOptions { |
| apiKey?: string; |
| env?: Record<string, string>; |
| |
| minOAuthValidityMs?: number; |
| } |
|
|
| export type CredentialSynchronizationOperation = "login" | "logout" | "setRuntimeApiKey" | "removeRuntimeApiKey"; |
|
|
| |
| export class CredentialSynchronizationError extends Error { |
| readonly providerId: string; |
| readonly operation: CredentialSynchronizationOperation; |
| readonly credential: Credential | undefined; |
|
|
| constructor( |
| providerId: string, |
| operation: CredentialSynchronizationOperation, |
| credential: Credential | undefined, |
| options: ErrorOptions, |
| ) { |
| super(`Credential ${operation} committed for ${providerId}, but local synchronization failed`, options); |
| this.name = "CredentialSynchronizationError"; |
| this.providerId = providerId; |
| this.operation = operation; |
| this.credential = credential; |
| } |
| } |
|
|
| function mergeHeaders( |
| base: ProviderHeaders | undefined, |
| override: ProviderHeaders | undefined, |
| ): ProviderHeaders | undefined { |
| if (!base && !override) return undefined; |
| const merged = { ...base }; |
| for (const [name, value] of Object.entries(override ?? {})) { |
| const lowerName = name.toLowerCase(); |
| for (const existingName of Object.keys(merged)) { |
| if (existingName.toLowerCase() === lowerName) delete merged[existingName]; |
| } |
| merged[name] = value; |
| } |
| return merged; |
| } |
|
|
| |
| export class ModelRuntime implements Models { |
| private readonly models: MutableModels; |
| private readonly credentials: RuntimeCredentials; |
| private readonly defaultBuiltins: ReadonlyMap<string, Provider>; |
| private readonly builtins = new Map<string, Provider>(); |
| private readonly nativeExtensionProviders = new Map<string, Provider>(); |
| private readonly extensionProviders = new Map<string, ProviderConfigInput>(); |
| private readonly compositionErrors = new Map<string, string>(); |
| private readonly modelsPath: string | undefined; |
| private readonly modelNetworkEnabled: boolean; |
| private config: ModelConfig; |
| private snapshot: ModelRuntimeSnapshot = { |
| all: [], |
| available: [], |
| configuredProviders: new Set(), |
| storedProviders: new Set(), |
| auth: new Map(), |
| }; |
| private availabilityRefreshSeq = 0; |
| private availabilityErrorSeq = 0; |
| private readonly providerAvailabilitySeq = new Map<string, number>(); |
| private availabilityError: string | undefined; |
| private readonly credentialOperations = new Map<string, Promise<unknown>>(); |
|
|
| private constructor( |
| credentials: RuntimeCredentials, |
| config: ModelConfig, |
| modelsPath: string | undefined, |
| modelsStore: ModelsStore, |
| providers: readonly Provider[], |
| modelNetworkEnabled: boolean, |
| ) { |
| this.credentials = credentials; |
| this.config = config; |
| this.modelsPath = modelsPath; |
| this.modelNetworkEnabled = modelNetworkEnabled; |
| this.defaultBuiltins = new Map(providers.map((provider) => [provider.id, provider])); |
| for (const [providerId, provider] of this.defaultBuiltins) this.builtins.set(providerId, provider); |
| this.models = createModels({ credentials, modelsStore }); |
| this.rebuildProviders(); |
| } |
|
|
| static async create(options: CreateModelRuntimeOptions = {}): Promise<ModelRuntime> { |
| const credentials = new RuntimeCredentials(options.credentials ?? DefaultAuthStorage.create(options.authPath)); |
| const modelsPath = |
| options.modelsPath === null ? undefined : (options.modelsPath ?? join(getAgentDir(), "models.json")); |
| const config = await ModelConfig.load(modelsPath); |
| const modelsStore = |
| options.modelsStore ?? |
| (modelsPath |
| ? new FileModelsStore(options.modelsStorePath ?? join(dirname(modelsPath), "models-store.json")) |
| : new InMemoryCodingAgentModelsStore()); |
| const builtinModelDataGeneratedAt = builtinProviderCatalog.getBuiltinModelDataGeneratedAt(); |
| const providers = builtinProviderCatalog |
| .builtinProviders() |
| .map((provider) => |
| provider.id === "radius" |
| ? provider |
| : withRemoteCatalog(provider, options.catalogBaseUrl, builtinModelDataGeneratedAt), |
| ); |
| const runtime = new ModelRuntime( |
| credentials, |
| config, |
| modelsPath, |
| modelsStore, |
| providers, |
| process.env.PI_OFFLINE === undefined, |
| ); |
| runtime.configureRadiusProviders(); |
| runtime.rebuildProviders(); |
| const refreshFromNetwork = runtime.modelNetworkEnabled && options.allowModelNetwork === true; |
| const controller = |
| refreshFromNetwork && options.modelRefreshTimeoutMs !== undefined ? new AbortController() : undefined; |
| const timeout = controller ? setTimeout(() => controller.abort(), options.modelRefreshTimeoutMs) : undefined; |
| const signal = controller |
| ? options.signal |
| ? AbortSignal.any([options.signal, controller.signal]) |
| : controller.signal |
| : options.signal; |
| try { |
| if (options.refreshOnCreate !== false) { |
| await runtime.refresh({ allowNetwork: refreshFromNetwork, signal }); |
| } |
| } finally { |
| if (timeout) clearTimeout(timeout); |
| } |
| return runtime; |
| } |
|
|
| private configureRadiusProviders(): void { |
| this.builtins.clear(); |
| for (const [providerId, provider] of this.defaultBuiltins) this.builtins.set(providerId, provider); |
| for (const providerId of this.config.getProviderIds()) { |
| const config = this.config.getProvider(providerId); |
| if (config?.oauth !== "radius" || !config.baseUrl) continue; |
| this.builtins.set( |
| providerId, |
| builtinProviderCatalog.radiusProvider({ |
| id: providerId, |
| name: config.name ?? providerId, |
| gateway: config.baseUrl.replace(/\/v1\/?$/u, ""), |
| }), |
| ); |
| } |
| } |
|
|
| private providerIds(): Set<string> { |
| return new Set([ |
| ...this.builtins.keys(), |
| ...this.nativeExtensionProviders.keys(), |
| ...this.config.getProviderIds(), |
| ...this.extensionProviders.keys(), |
| ]); |
| } |
|
|
| private recomposeProvider(providerId: string): void { |
| const base = this.nativeExtensionProviders.get(providerId) ?? this.builtins.get(providerId); |
| const extension = this.extensionProviders.get(providerId); |
| if (!base && !this.config.getProvider(providerId) && !extension) { |
| this.models.deleteProvider(providerId); |
| this.compositionErrors.delete(providerId); |
| return; |
| } |
| if (base && !this.config.getProvider(providerId) && !extension) { |
| |
| this.models.setProvider(base); |
| this.compositionErrors.delete(providerId); |
| return; |
| } |
| try { |
| this.models.setProvider(composeModelProvider(providerId, base, this.config, extension)); |
| this.compositionErrors.delete(providerId); |
| } catch (error) { |
| this.compositionErrors.set(providerId, error instanceof Error ? error.message : String(error)); |
| if (base) this.models.setProvider(base); |
| else this.models.deleteProvider(providerId); |
| } |
| } |
|
|
| private rebuildProviders(): void { |
| this.models.clearProviders(); |
| this.compositionErrors.clear(); |
| for (const providerId of this.providerIds()) this.recomposeProvider(providerId); |
| this.updateModelSnapshot(); |
| } |
|
|
| private updateModelSnapshot(): void { |
| const all = [...this.models.getModels()]; |
| this.snapshot = { |
| ...this.snapshot, |
| all, |
| available: all.filter((model) => this.snapshot.configuredProviders.has(model.provider)), |
| }; |
| } |
|
|
| private async runAvailabilityRefresh(seq: number, errorSeq: number, signal: AbortSignal): Promise<void> { |
| const providers = this.models.getProviders(); |
| const [available, checks, credentials] = await Promise.all([ |
| this.models.getAvailable(undefined, { signal }), |
| Promise.all( |
| providers.map( |
| async (provider): Promise<[string, AuthCheck | undefined]> => [ |
| provider.id, |
| await this.models.checkAuth(provider.id, { signal }), |
| ], |
| ), |
| ), |
| this.credentials.list({ signal }), |
| ]); |
| if (seq !== this.availabilityRefreshSeq) return; |
| const auth = new Map(checks); |
| const configuredProviders = new Set( |
| checks |
| .filter((entry): entry is [string, AuthCheck] => entry[1] !== undefined) |
| .map(([providerId]) => providerId), |
| ); |
| this.snapshot = { |
| all: [...this.models.getModels()], |
| available: [...available], |
| configuredProviders, |
| storedProviders: new Set(credentials.map((entry) => entry.providerId)), |
| auth, |
| }; |
| if (errorSeq === this.availabilityErrorSeq) this.availabilityError = undefined; |
| } |
|
|
| private queueAvailabilityRefresh(signal?: AbortSignal): Promise<void> { |
| const seq = ++this.availabilityRefreshSeq; |
| for (const [providerId, providerSeq] of this.providerAvailabilitySeq) { |
| this.providerAvailabilitySeq.set(providerId, providerSeq + 1); |
| } |
| const errorSeq = ++this.availabilityErrorSeq; |
| const effectiveSignal = operationSignal(signal); |
| return this.runAvailabilityRefresh(seq, errorSeq, effectiveSignal).catch((error) => { |
| if (errorSeq === this.availabilityErrorSeq && !effectiveSignal.aborted) { |
| this.availabilityError = error instanceof Error ? error.message : String(error); |
| } |
| throw error; |
| }); |
| } |
|
|
| private async refreshProviderAvailability(providerId: string, signal: AbortSignal): Promise<void> { |
| |
| ++this.availabilityRefreshSeq; |
| const providerSeq = (this.providerAvailabilitySeq.get(providerId) ?? 0) + 1; |
| this.providerAvailabilitySeq.set(providerId, providerSeq); |
| const errorSeq = ++this.availabilityErrorSeq; |
| try { |
| const [available, auth, credential] = await Promise.all([ |
| this.models.getAvailable(providerId, { signal }), |
| this.models.checkAuth(providerId, { signal }), |
| this.credentials.read(providerId, { signal }), |
| ]); |
| signal.throwIfAborted(); |
| if (this.providerAvailabilitySeq.get(providerId) !== providerSeq) return; |
| const configuredProviders = new Set(this.snapshot.configuredProviders); |
| const storedProviders = new Set(this.snapshot.storedProviders); |
| const authByProvider = new Map(this.snapshot.auth); |
| if (auth) { |
| configuredProviders.add(providerId); |
| authByProvider.set(providerId, auth); |
| } else { |
| configuredProviders.delete(providerId); |
| authByProvider.delete(providerId); |
| } |
| if (credential) storedProviders.add(providerId); |
| else storedProviders.delete(providerId); |
| const all = [...this.models.getModels()]; |
| const availableById = new Map( |
| [...this.snapshot.available.filter((model) => model.provider !== providerId), ...available].map((model) => [ |
| `${model.provider}\0${model.id}`, |
| model, |
| ]), |
| ); |
| this.snapshot = { |
| all, |
| available: all.flatMap((model) => availableById.get(`${model.provider}\0${model.id}`) ?? []), |
| configuredProviders, |
| storedProviders, |
| auth: authByProvider, |
| }; |
| if (errorSeq === this.availabilityErrorSeq) this.availabilityError = undefined; |
| } catch (error) { |
| if ( |
| this.providerAvailabilitySeq.get(providerId) === providerSeq && |
| errorSeq === this.availabilityErrorSeq && |
| !signal.aborted |
| ) { |
| this.availabilityError = error instanceof Error ? error.message : String(error); |
| } |
| throw error; |
| } |
| } |
|
|
| getProviders(): readonly Provider[] { |
| return this.models.getProviders(); |
| } |
|
|
| getProvider(providerId: string): Provider | undefined { |
| return this.models.getProvider(providerId); |
| } |
|
|
| getModels(providerId?: string): readonly Model<Api>[] { |
| return this.models.getModels(providerId); |
| } |
|
|
| getModel(providerId: string, modelId: string): Model<Api> | undefined { |
| return this.models.getModel(providerId, modelId); |
| } |
|
|
| async checkAuth(providerId: string, options?: AuthOperationOptions): Promise<AuthCheck | undefined> { |
| return this.models.checkAuth(providerId, options); |
| } |
|
|
| async getAvailable(providerId?: string, options?: AuthOperationOptions): Promise<readonly Model<Api>[]> { |
| if (providerId) { |
| const errorSeq = ++this.availabilityErrorSeq; |
| try { |
| const available = await this.models.getAvailable(providerId, options); |
| if (errorSeq === this.availabilityErrorSeq) this.availabilityError = undefined; |
| return available; |
| } catch (error) { |
| if (errorSeq === this.availabilityErrorSeq && !options?.signal?.aborted) { |
| this.availabilityError = error instanceof Error ? error.message : String(error); |
| } |
| throw error; |
| } |
| } |
| await this.queueAvailabilityRefresh(options?.signal); |
| return this.snapshot.available; |
| } |
|
|
| getAvailableSnapshot(): readonly Model<Api>[] { |
| return this.snapshot.available; |
| } |
|
|
| getError(): string | undefined { |
| const errors: string[] = []; |
| const configError = this.config.getError(); |
| if (configError) errors.push(configError); |
| for (const [providerId, error] of this.compositionErrors) { |
| errors.push(`Provider "${providerId}": ${error}`); |
| } |
| if (this.availabilityError) errors.push(`Availability refresh: ${this.availabilityError}`); |
| return errors.length > 0 ? errors.join("\n\n") : undefined; |
| } |
|
|
| getRegisteredProviderConfig(providerId: string): ProviderConfigInput | undefined { |
| return this.extensionProviders.get(providerId); |
| } |
|
|
| getRegisteredProviderIds(): readonly string[] { |
| return [...new Set([...this.extensionProviders.keys(), ...this.nativeExtensionProviders.keys()])]; |
| } |
|
|
| getRegisteredNativeProvider(providerId: string): Provider | undefined { |
| return this.nativeExtensionProviders.get(providerId); |
| } |
|
|
| |
| getCompatibilityRequestConfig(model: Model<Api>): CompatibilityRequestConfig { |
| return resolveCompatibilityRequestConfig( |
| model, |
| this.config.getProvider(model.provider), |
| this.extensionProviders.get(model.provider), |
| ); |
| } |
|
|
| isUsingOAuth(providerId: string): boolean { |
| return this.snapshot.auth.get(providerId)?.type === "oauth"; |
| } |
|
|
| isUsingSubscription(providerId: string): boolean { |
| return this.isUsingOAuth(providerId) && this.models.getProvider(providerId)?.auth.oauth?.isSubscription === true; |
| } |
|
|
| hasConfiguredAuth(providerId: string): boolean { |
| return this.snapshot.configuredProviders.has(providerId); |
| } |
|
|
| getAuth(providerId: string, overrides?: ModelRuntimeAuthOverrides): Promise<AuthResult | undefined>; |
| getAuth(model: Model<Api>, overrides?: ModelRuntimeAuthOverrides): Promise<AuthResult | undefined>; |
| async getAuth( |
| providerOrModel: string | Model<Api>, |
| overrides: ModelRuntimeAuthOverrides = {}, |
| ): Promise<AuthResult | undefined> { |
| if (typeof providerOrModel === "string") return this.models.getAuth(providerOrModel, overrides); |
| const resolution = await this.models.getAuth(providerOrModel, overrides); |
| if (!resolution) return undefined; |
| const configuredHeaders = resolveConfiguredModelHeaders( |
| providerOrModel, |
| this.config.getProvider(providerOrModel.provider), |
| this.extensionProviders.get(providerOrModel.provider), |
| { ...(resolution.env ?? {}), ...(overrides.env ?? {}) }, |
| ); |
| return { |
| ...resolution, |
| auth: { |
| ...resolution.auth, |
| headers: mergeHeaders(resolution.auth.headers, configuredHeaders), |
| }, |
| }; |
| } |
|
|
| private enqueueCredentialOperation<T>(providerId: string, signal: AbortSignal, task: () => Promise<T>): Promise<T> { |
| const previous = this.credentialOperations.get(providerId) ?? Promise.resolve(); |
| let markStarted: (() => void) | undefined; |
| const started = new Promise<void>((resolve) => { |
| markStarted = resolve; |
| }); |
| const operation = (async () => { |
| await previous.catch(() => {}); |
| signal.throwIfAborted(); |
| markStarted?.(); |
| return task(); |
| })(); |
| const tail = operation.catch(() => {}); |
| this.credentialOperations.set(providerId, tail); |
| void tail.then(() => { |
| if (this.credentialOperations.get(providerId) === tail) this.credentialOperations.delete(providerId); |
| }); |
| return raceWithAbortSignal(started, signal).then(() => operation); |
| } |
|
|
| private async synchronizeCredentialState( |
| providerId: string, |
| operation: CredentialSynchronizationOperation, |
| credential: Credential | undefined, |
| signal: AbortSignal, |
| ): Promise<void> { |
| try { |
| signal.throwIfAborted(); |
| this.recomposeProvider(providerId); |
| const compositionError = this.compositionErrors.get(providerId); |
| if (compositionError) throw new Error(compositionError); |
| const result = await this.models.refresh({ allowNetwork: false, providers: [providerId], signal }); |
| if (result.aborted) signal.throwIfAborted(); |
| const refreshError = result.errors.get(providerId); |
| if (refreshError) throw refreshError; |
| this.updateModelSnapshot(); |
| await this.refreshProviderAvailability(providerId, signal); |
| } catch (cause) { |
| throw new CredentialSynchronizationError(providerId, operation, credential, { cause }); |
| } |
| } |
|
|
| setRuntimeApiKey(providerId: string, apiKey: string, options: AuthOperationOptions = {}): Promise<void> { |
| const signal = operationSignal(options.signal); |
| return this.enqueueCredentialOperation(providerId, signal, async () => { |
| this.credentials.setRuntimeApiKey(providerId, apiKey); |
| await this.synchronizeCredentialState( |
| providerId, |
| "setRuntimeApiKey", |
| { type: "api_key", key: apiKey }, |
| signal, |
| ); |
| }); |
| } |
|
|
| removeRuntimeApiKey(providerId: string, options: AuthOperationOptions = {}): Promise<void> { |
| const signal = operationSignal(options.signal); |
| return this.enqueueCredentialOperation(providerId, signal, async () => { |
| this.credentials.removeRuntimeApiKey(providerId); |
| await this.synchronizeCredentialState(providerId, "removeRuntimeApiKey", undefined, signal); |
| }); |
| } |
|
|
| listCredentials(options?: AuthOperationOptions): Promise<readonly CredentialInfo[]> { |
| return this.credentials.list(options); |
| } |
|
|
| getProviderAuthStatus(providerId: string): AuthStatus { |
| if (this.credentials.hasRuntimeApiKey(providerId)) return { configured: true, source: "runtime" }; |
| if (this.snapshot.storedProviders.has(providerId)) return { configured: true, source: "stored" }; |
| const configured = configuredRequestAuthStatus( |
| this.config.getProvider(providerId), |
| this.extensionProviders.get(providerId), |
| ); |
| if (configured) return configured; |
| const check = this.snapshot.auth.get(providerId); |
| return check ? { configured: true, source: "environment", label: check.source } : { configured: false }; |
| } |
|
|
| private async prepareRequest<TOptions extends ProviderRequestOptions & ModelsRequestTransforms>( |
| model: Model<Api>, |
| options: TOptions | undefined, |
| ): Promise<{ |
| provider: Provider; |
| model: Model<Api>; |
| options: Omit<TOptions, "transformHeaders"> & ProviderRequestOptions; |
| }> { |
| const provider = this.models.getProvider(model.provider); |
| if (!provider) throw new ModelsError("provider", `Unknown provider: ${model.provider}`); |
| const resolution = await this.getAuth(model, { |
| apiKey: options?.apiKey, |
| env: options?.env, |
| signal: options?.signal, |
| }); |
| if (!resolution) throw new ModelsError("auth", `Provider is not configured: ${model.provider}`); |
|
|
| const { transformHeaders, ...rawProviderOptions } = options ?? {}; |
| const providerOptions = rawProviderOptions as Omit<TOptions, "transformHeaders"> & ProviderRequestOptions; |
| let headers = mergeHeaders(resolution.auth.headers, providerOptions.headers); |
| if (transformHeaders) headers = await transformHeaders(headers ?? {}); |
| const env = |
| resolution.env || providerOptions.env |
| ? { ...(resolution.env ?? {}), ...(providerOptions.env ?? {}) } |
| : undefined; |
| return { |
| provider, |
| model: resolution.auth.baseUrl ? { ...model, baseUrl: resolution.auth.baseUrl } : model, |
| options: { |
| ...providerOptions, |
| apiKey: providerOptions.apiKey ?? resolution.auth.apiKey, |
| headers, |
| env, |
| } as Omit<TOptions, "transformHeaders"> & ProviderRequestOptions, |
| }; |
| } |
|
|
| stream<TApi extends Api>( |
| model: Model<TApi>, |
| context: Context, |
| options?: ModelsApiStreamOptions<TApi>, |
| ): AssistantMessageEventStream { |
| return lazyStream(model, async () => { |
| const prepared = await this.prepareRequest( |
| model, |
| options as (StreamOptions & ModelsRequestTransforms) | undefined, |
| ); |
| return prepared.provider.stream( |
| prepared.model as Model<TApi>, |
| context, |
| prepared.options as ApiStreamOptions<TApi>, |
| ); |
| }); |
| } |
|
|
| complete<TApi extends Api>( |
| model: Model<TApi>, |
| context: Context, |
| options?: ModelsApiStreamOptions<TApi>, |
| ): Promise<AssistantMessage> { |
| return this.stream(model, context, options).result(); |
| } |
|
|
| streamSimple(model: Model<Api>, context: Context, options?: ModelsSimpleStreamOptions): AssistantMessageEventStream { |
| return lazyStream(model, async () => { |
| const prepared = await this.prepareRequest(model, options); |
| return prepared.provider.streamSimple(prepared.model, context, prepared.options as SimpleStreamOptions); |
| }); |
| } |
|
|
| completeSimple(model: Model<Api>, context: Context, options?: ModelsSimpleStreamOptions): Promise<AssistantMessage> { |
| return this.streamSimple(model, context, options).result(); |
| } |
|
|
| streamDeferred( |
| model: Model<Api>, |
| handle: DeferredHandle, |
| options?: ModelsDeferredFetchOptions, |
| ): AssistantMessageEventStream { |
| return lazyStream(model, async () => { |
| const prepared = await this.prepareRequest(model, options); |
| if (!prepared.provider.fetchDeferred) { |
| throw new ModelsError("provider", `Provider ${model.provider} does not support deferred responses`); |
| } |
| return prepared.provider.fetchDeferred(prepared.model, handle, prepared.options as DeferredFetchOptions); |
| }); |
| } |
|
|
| async fetchDeferred( |
| model: Model<Api>, |
| handle: DeferredHandle, |
| options?: ModelsDeferredFetchOptions, |
| ): Promise<AssistantMessage> { |
| return this.streamDeferred(model, handle, options).result(); |
| } |
|
|
| async cancelDeferred( |
| model: Model<Api>, |
| handle: DeferredHandle, |
| options?: ModelsDeferredCancelOptions, |
| ): Promise<void> { |
| const prepared = await this.prepareRequest(model, options); |
| if (!prepared.provider.cancelDeferred) { |
| throw new ModelsError("provider", `Provider ${model.provider} does not support deferred responses`); |
| } |
| await prepared.provider.cancelDeferred(prepared.model, handle, prepared.options as DeferredCancelOptions); |
| } |
|
|
| login(providerId: string, type: AuthType, interaction: AuthInteraction): Promise<Credential> { |
| const signal = operationSignal(interaction.signal); |
| return this.enqueueCredentialOperation(providerId, signal, async () => { |
| const credential = await this.models.login(providerId, type, { ...interaction, signal }); |
| await this.synchronizeCredentialState(providerId, "login", credential, signal); |
| return credential; |
| }); |
| } |
|
|
| logout(providerId: string, options: AuthOperationOptions = {}): Promise<void> { |
| const signal = operationSignal(options.signal); |
| return this.enqueueCredentialOperation(providerId, signal, async () => { |
| await this.models.logout(providerId, { signal }); |
| await this.synchronizeCredentialState(providerId, "logout", undefined, signal); |
| }); |
| } |
|
|
| async refresh(options: ModelsRefreshOptions = {}): Promise<ModelsRefreshResult> { |
| this.config = await ModelConfig.load(this.modelsPath); |
| this.configureRadiusProviders(); |
| if (options.providers) { |
| for (const providerId of new Set(options.providers)) this.recomposeProvider(providerId); |
| this.updateModelSnapshot(); |
| } else { |
| this.rebuildProviders(); |
| } |
| const refreshOptions = { |
| ...options, |
| allowNetwork: options.allowNetwork ?? this.modelNetworkEnabled, |
| }; |
| |
| |
| const result = ((await this.models.refresh(refreshOptions)) as ModelsRefreshResult | undefined) ?? { |
| aborted: refreshOptions.signal?.aborted ?? false, |
| errors: new Map(), |
| }; |
| const errors = new Map(result.errors); |
| this.updateModelSnapshot(); |
| if (options.providers) { |
| await Promise.all( |
| [...new Set(options.providers)].map(async (providerId) => { |
| try { |
| await this.refreshProviderAvailability(providerId, operationSignal(options.signal)); |
| } catch (error) { |
| if (!options.signal?.aborted) { |
| errors.set(providerId, error instanceof Error ? error : new Error(String(error))); |
| } |
| } |
| }), |
| ); |
| } else { |
| try { |
| await this.queueAvailabilityRefresh(options.signal); |
| } catch { |
| |
| } |
| } |
| return { aborted: result.aborted || (options.signal?.aborted ?? false), errors }; |
| } |
|
|
| registerNativeProvider(provider: Provider): void { |
| if (!provider.id.trim()) throw new Error("Provider id must not be empty."); |
| this.extensionProviders.delete(provider.id); |
| this.nativeExtensionProviders.set(provider.id, provider); |
| this.recomposeProvider(provider.id); |
| this.updateModelSnapshot(); |
| void this.refresh({ allowNetwork: false }); |
| } |
|
|
| registerProvider(providerId: string, config: ProviderConfigInput): void { |
| |
| |
| validateExtensionProvider(providerId, this.builtins.get(providerId), this.config.getProvider(providerId), config); |
| this.nativeExtensionProviders.delete(providerId); |
| |
| |
| const previous = this.extensionProviders.get(providerId); |
| const effective: ProviderConfigInput = { ...previous }; |
| for (const [key, value] of Object.entries(config)) { |
| if (value !== undefined) (effective as Record<string, unknown>)[key] = value; |
| } |
| this.extensionProviders.set(providerId, effective); |
| this.recomposeProvider(providerId); |
| this.updateModelSnapshot(); |
| if ( |
| this.snapshot.storedProviders.has(providerId) || |
| configuredRequestAuthStatus(this.config.getProvider(providerId), effective)?.configured |
| ) { |
| const configuredProviders = new Set(this.snapshot.configuredProviders).add(providerId); |
| const auth = new Map(this.snapshot.auth); |
| |
| if (!auth.get(providerId)) { |
| auth.set(providerId, { |
| type: effective.oauth && !effective.apiKey ? "oauth" : "api_key", |
| source: "configured provider", |
| }); |
| } |
| this.snapshot = { |
| ...this.snapshot, |
| auth, |
| configuredProviders, |
| available: this.snapshot.all.filter((model) => configuredProviders.has(model.provider)), |
| }; |
| } |
| void this.refresh({ allowNetwork: false }); |
| } |
|
|
| unregisterProvider(providerId: string): void { |
| this.extensionProviders.delete(providerId); |
| this.nativeExtensionProviders.delete(providerId); |
| this.recomposeProvider(providerId); |
| this.updateModelSnapshot(); |
| void this.refresh({ allowNetwork: false }); |
| } |
| } |
|
|