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:
@@ -22,6 +22,7 @@ import type {
|
||||
KnownProvider,
|
||||
Model,
|
||||
ModelThinkingLevel,
|
||||
ProviderStreams,
|
||||
SimpleStreamOptions,
|
||||
StreamOptions,
|
||||
Usage,
|
||||
@@ -351,6 +352,61 @@ export function createModels(options?: CreateModelsOptions): MutableModels {
|
||||
return new ModelsImpl(options);
|
||||
}
|
||||
|
||||
export interface CreateProviderOptions<TApi extends Api = Api> {
|
||||
id: string;
|
||||
/** Display name. Default: `id`. */
|
||||
name?: string;
|
||||
baseUrl?: string;
|
||||
headers?: Record<string, string>;
|
||||
/** Required — every provider has auth semantics, even ambient/keyless ones. */
|
||||
auth: ProviderAuth;
|
||||
models:
|
||||
| readonly Model<TApi>[]
|
||||
| ((options?: { forceRefresh?: boolean }) => Promise<readonly Model<TApi>[]> | readonly Model<TApi>[]);
|
||||
/** Single implementation, or map keyed by `model.api` for mixed-API providers. */
|
||||
api: ProviderStreams | Partial<Record<TApi, ProviderStreams>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a provider from parts. Built-in provider factories and models.json
|
||||
* custom providers both go through this. A single `api` streams all models;
|
||||
* an `api` map dispatches on `model.api`, and a model whose api has no entry
|
||||
* produces a stream error.
|
||||
*/
|
||||
export function createProvider<TApi extends Api = Api>(input: CreateProviderOptions<TApi>): Provider<TApi> {
|
||||
const { models } = input;
|
||||
const single =
|
||||
typeof (input.api as ProviderStreams).stream === "function" ? (input.api as ProviderStreams) : undefined;
|
||||
const byApi = single ? undefined : (input.api as Partial<Record<string, ProviderStreams>>);
|
||||
|
||||
const apiFor = (model: Model<Api>): ProviderStreams | undefined => single ?? byApi?.[model.api];
|
||||
|
||||
const dispatch = (
|
||||
model: Model<Api>,
|
||||
run: (streams: ProviderStreams) => AssistantMessageEventStream,
|
||||
): AssistantMessageEventStream => {
|
||||
const streams = apiFor(model);
|
||||
if (!streams) {
|
||||
return lazyStream(model, async () => {
|
||||
throw new ModelsError("stream", `Provider ${input.id} has no API implementation for "${model.api}"`);
|
||||
});
|
||||
}
|
||||
return run(streams);
|
||||
};
|
||||
|
||||
return {
|
||||
id: input.id,
|
||||
name: input.name ?? input.id,
|
||||
baseUrl: input.baseUrl,
|
||||
headers: input.headers,
|
||||
auth: input.auth,
|
||||
getModels: typeof models === "function" ? (options) => models(options) : () => models,
|
||||
stream: (model, context, options) => dispatch(model, (streams) => streams.stream(model, context, options)),
|
||||
streamSimple: (model, context, options) =>
|
||||
dispatch(model, (streams) => streams.streamSimple(model, context, options)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime-checked narrowing for dynamically looked-up models:
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user