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
+8 -40
View File
@@ -2,52 +2,20 @@
// Run from packages/ai: node test/scratch.ts
// Requires ANTHROPIC_API_KEY.
import { anthropicMessagesApi } from "../src/api/anthropic-messages.lazy.ts";
import { createModels, getModels, type Provider } from "../src/models.ts";
import { createModels } from "../src/models.ts";
import { anthropicProvider } from "../src/providers/anthropic.ts";
import type { Context } from "../src/types.ts";
const anthropicApi = anthropicMessagesApi();
// ---------------------------------------------------------------------------
// 1. Define a provider. In the final design this comes from
// `@earendil-works/pi-ai/providers/anthropic` as `anthropicProvider()`;
// until Phase 3 lands we wire it by hand from existing parts.
// ---------------------------------------------------------------------------
const anthropic: Provider<"anthropic-messages"> = {
id: "anthropic",
name: "Anthropic",
baseUrl: "https://api.anthropic.com/v1",
auth: {
apiKey: {
name: "Anthropic API key",
resolve: async ({ ctx, credential }) => {
// stored credential (from a /login flow) wins, env is the ambient fallback
const key = credential?.key ?? (await ctx.env("ANTHROPIC_API_KEY"));
if (!key) return undefined;
return { auth: { apiKey: key }, source: credential ? "stored credential" : "ANTHROPIC_API_KEY" };
},
},
},
// static catalog source; a dynamic provider would fetch here
getModels: async () => getModels("anthropic"),
// shared lazy API implementation (loads the SDK on first request)
stream: anthropicApi.stream,
streamSimple: anthropicApi.streamSimple,
};
// ---------------------------------------------------------------------------
// 2. Build a Models runtime and register the provider.
// 1. Build a Models runtime and register a built-in provider factory.
// (Apps wanting everything use `builtinModels()` from providers/all.)
// ---------------------------------------------------------------------------
const models = createModels();
models.setProvider(anthropic);
models.setProvider(anthropicProvider());
// ---------------------------------------------------------------------------
// 3. Look up a model and check auth.
// 2. Look up a model and check auth.
// ---------------------------------------------------------------------------
const model = await models.getModel("anthropic", "claude-haiku-4-5");
@@ -64,14 +32,14 @@ const context: Context = {
};
// ---------------------------------------------------------------------------
// 4. Simple completion (request-level auth resolution happens inside).
// 3. Simple completion (request-level auth resolution happens inside).
// ---------------------------------------------------------------------------
const message = await models.completeSimple(model, context);
console.log(`completeSimple -> [${message.stopReason}]`, message.content);
// ---------------------------------------------------------------------------
// 5. Streaming with deltas.
// 4. Streaming with deltas.
// ---------------------------------------------------------------------------
context.messages.push(message, {