feat(ai): provider factories, per-provider catalogs, createProvider (phase 3)

Auth helpers in src/auth/helpers.ts: envApiKeyAuth() (stored key wins,
then env vars in order, with secret-prompt login) and lazyOAuth()
(flow loads on first use through bundler-opaque dynamic imports in
utils/oauth/load.ts; the OAuthAuth flow exports land in phase 4).
There is no OAuth factory toggle: providers that support OAuth always
attach it, advertising costs nothing until login/refresh runs.

createProvider() in models.ts builds providers from parts: single API
implementation or a map dispatched on model.api (mixed-API providers
like opencode and github-copilot); unknown api yields a stream error.

generate-models.ts now emits one providers/<id>.models.ts catalog per
provider (35 files, biome-excluded like models.generated.ts) and
models.generated.ts becomes a generated aggregator, so importing one
provider factory pulls one catalog. Typed getModel globals unchanged.

One factory per built-in provider under src/providers/: envApiKeyAuth
for standard providers, OAuth for anthropic/openai-codex/github-copilot,
ambient ApiKeyAuth for amazon-bedrock (AWS env/profile/IAM) and
google-vertex (explicit key or ADC+project+location).

providers/all.ts: builtinProviders(), builtinModels(), getBuiltin*
re-exports. fauxProvider() factory returns a real Provider for tests;
legacy registerFauxProvider() unchanged.
This commit is contained in:
Mario Zechner
2026-06-10 20:33:20 +02:00
parent afc2bd370e
commit fec0c3d12f
83 changed files with 18409 additions and 17094 deletions
+65 -6
View File
@@ -1,4 +1,5 @@
import { registerApiProvider, unregisterApiProviders } from "../api-registry.ts";
import { createProvider, type Provider } from "../models.ts";
import type {
AssistantMessage,
AssistantMessageEventStream,
@@ -125,6 +126,18 @@ export interface FauxProviderRegistration {
unregister: () => void;
}
export interface FauxProviderHandle {
provider: Provider;
api: string;
models: [Model<string>, ...Model<string>[]];
getModel(): Model<string>;
getModel(modelId: string): Model<string> | undefined;
state: { callCount: number };
setResponses: (responses: FauxResponseStep[]) => void;
appendResponses: (responses: FauxResponseStep[]) => void;
getPendingResponseCount: () => number;
}
function estimateTokens(text: string): number {
return Math.ceil(text.length / 4);
}
@@ -388,10 +401,9 @@ async function streamWithDeltas(
stream.end(message);
}
export function registerFauxProvider(options: RegisterFauxProviderOptions = {}): FauxProviderRegistration {
function createFauxCore(options: RegisterFauxProviderOptions) {
const api = options.api ?? randomId(DEFAULT_API);
const provider = options.provider ?? DEFAULT_PROVIDER;
const sourceId = randomId("faux-provider");
const minTokenSize = Math.max(
1,
Math.min(options.tokenSize?.min ?? DEFAULT_MIN_TOKEN_SIZE, options.tokenSize?.max ?? DEFAULT_MAX_TOKEN_SIZE),
@@ -467,8 +479,6 @@ export function registerFauxProvider(options: RegisterFauxProviderOptions = {}):
const streamSimple: StreamFunction<string, SimpleStreamOptions> = (streamModel, context, streamOptions) =>
stream(streamModel, context, streamOptions);
registerApiProvider({ api, stream, streamSimple }, sourceId);
function getModel(): Model<string>;
function getModel(requestedModelId: string): Model<string> | undefined;
function getModel(requestedModelId?: string): Model<string> | undefined {
@@ -480,20 +490,69 @@ export function registerFauxProvider(options: RegisterFauxProviderOptions = {}):
return {
api,
provider,
models,
stream,
streamSimple,
getModel,
state,
setResponses(responses) {
setResponses(responses: FauxResponseStep[]) {
pendingResponses = [...responses];
},
appendResponses(responses) {
appendResponses(responses: FauxResponseStep[]) {
pendingResponses.push(...responses);
},
getPendingResponseCount() {
return pendingResponses.length;
},
};
}
/** Registers the faux api into the legacy global api-registry. */
export function registerFauxProvider(options: RegisterFauxProviderOptions = {}): FauxProviderRegistration {
const core = createFauxCore(options);
const sourceId = randomId("faux-provider");
registerApiProvider({ api: core.api, stream: core.stream, streamSimple: core.streamSimple }, sourceId);
return {
api: core.api,
models: core.models,
getModel: core.getModel,
state: core.state,
setResponses: core.setResponses,
appendResponses: core.appendResponses,
getPendingResponseCount: core.getPendingResponseCount,
unregister() {
unregisterApiProviders(sourceId);
},
};
}
/**
* Faux provider for tests built on explicit `Models` collections:
*
* ```ts
* const faux = fauxProvider();
* const models = createModels();
* models.setProvider(faux.provider);
* faux.setResponses([fauxAssistantMessage("hi")]);
* ```
*/
export function fauxProvider(options: RegisterFauxProviderOptions = {}): FauxProviderHandle {
const core = createFauxCore(options);
const provider = createProvider({
id: core.provider,
auth: { apiKey: { name: "Faux", resolve: async () => ({ auth: {} }) } },
models: core.models,
api: { stream: core.stream, streamSimple: core.streamSimple },
});
return {
provider,
api: core.api,
models: core.models,
getModel: core.getModel,
state: core.state,
setResponses: core.setResponses,
appendResponses: core.appendResponses,
getPendingResponseCount: core.getPendingResponseCount,
};
}