feat(coding-agent): replace model registry with model runtime
Move provider auth and OAuth flows onto pi-ai Models, compose models.json and extension overlays through ModelRuntime, and retain ModelRegistry as an extension compatibility facade.
This commit is contained in:
@@ -68,7 +68,7 @@ packages/ai/src/
|
||||
openrouter-images.ts # image-generation provider factory
|
||||
faux.ts # test provider factory
|
||||
all.ts # explicit aggregate: builtinModels(), builtinImagesModels(), getBuiltin*()
|
||||
utils/oauth/ # OAuth flow implementations (node), lazy-loaded
|
||||
auth/oauth/ # Canonical OAuth implementations (node), lazy-loaded
|
||||
```
|
||||
|
||||
`src/index.ts` must stay core-only. It must not import:
|
||||
@@ -407,7 +407,7 @@ export interface ApiKeyAuth {
|
||||
name: string; // "Anthropic API key"
|
||||
|
||||
/** Interactive setup (prompt for key/provider env). Absent = ambient-only (env, ADC, IAM). */
|
||||
login?(callbacks: AuthLoginCallbacks): Promise<ApiKeyCredential>;
|
||||
login?(interaction: AuthInteraction): Promise<ApiKeyCredential>;
|
||||
|
||||
/**
|
||||
* Resolve auth from the stored credential and/or ambient sources, merging
|
||||
@@ -424,7 +424,7 @@ export interface ApiKeyAuth {
|
||||
export interface OAuthAuth {
|
||||
name: string; // "Anthropic (Claude Pro/Max)"
|
||||
|
||||
login(callbacks: AuthLoginCallbacks): Promise<OAuthCredential>;
|
||||
login(interaction: AuthInteraction): Promise<OAuthCredential>;
|
||||
|
||||
/** Exchange the refresh token. Network call; throws on failure (invalid_grant etc.). Runs under the store lock. */
|
||||
refresh(credential: OAuthCredential): Promise<OAuthCredential>;
|
||||
@@ -445,9 +445,9 @@ export interface AuthContext {
|
||||
}
|
||||
```
|
||||
|
||||
The OAuth split (`refresh` + `toAuth` instead of one `resolve`) matches the old `OAuthProviderInterface` (`refreshToken` + `getApiKey`) and lets `Models` own the locking pattern without closure gymnastics: refresh produces a credential, `toAuth` derives request auth from whatever credential ends up stored.
|
||||
The `refresh`/`toAuth` split lets `Models` own the locked refresh pattern without closure gymnastics: refresh produces a credential, while `toAuth` derives request auth from whatever credential ends up stored.
|
||||
|
||||
There is no `usesCallbackServer` flag. With `prompt()/notify()` callbacks the flow self-describes at runtime: a flow that runs a callback server issues a `manual_code` prompt racing the server and aborts the prompt when the callback wins. The UI needs no static foreknowledge.
|
||||
OAuth implementations use the provider-neutral `AuthInteraction` protocol directly. A callback-server flow issues a `manual_code` prompt racing the server and aborts the prompt when the callback wins, so the UI needs no provider-specific callback or static callback-server flag.
|
||||
|
||||
### Credentials
|
||||
|
||||
@@ -564,7 +564,7 @@ FileCredentialStore ports AuthStorage's lock backend: read = memory snaps
|
||||
└─ withRuntimeOverrides --api-key
|
||||
└─ createModels({ credentials: store })
|
||||
|
||||
login/logout UI provider.auth.{oauth,apiKey}.login(callbacks) + store.modify/delete
|
||||
login/logout UI provider.auth.{oauth,apiKey}.login(interaction) + store.modify/delete
|
||||
status UI store.read(pid) + getAuth try/catch ("needs /login" on rejection)
|
||||
getOAuthProviders presence of provider.auth.oauth across registered providers
|
||||
```
|
||||
@@ -574,7 +574,7 @@ getOAuthProviders presence of provider.auth.oauth across registered pro
|
||||
One interface serves api-key and OAuth login:
|
||||
|
||||
```ts
|
||||
export interface AuthLoginCallbacks {
|
||||
export interface AuthInteraction {
|
||||
/** Aborts the whole login flow. Per-prompt cancellation uses AuthPrompt.signal. */
|
||||
signal?: AbortSignal;
|
||||
|
||||
@@ -612,7 +612,7 @@ export function anthropicProvider(): Provider {
|
||||
apiKey: envApiKeyAuth("Anthropic API key", ["ANTHROPIC_API_KEY"]),
|
||||
oauth: lazyOAuth({
|
||||
name: "Anthropic (Claude Pro/Max)",
|
||||
load: () => import("../utils/oauth/anthropic.ts").then((m) => m.anthropicOAuth),
|
||||
load: () => import("../auth/oauth/anthropic.ts").then((m) => m.anthropicOAuth),
|
||||
}),
|
||||
},
|
||||
models: ANTHROPIC_MODELS,
|
||||
@@ -632,7 +632,7 @@ export function lazyOAuth(input: {
|
||||
|
||||
OAuth must not force Node-only code (`node:http`, `node:crypto`) into browser bundles: the dynamic import inside `lazyOAuth()` uses the same bundler-opaque variable-specifier trick as the bedrock lazy wrapper. Browser hosts never trigger the load (no stored node OAuth credentials, no login flow). If web OAuth lands later (sitegeist proved feasibility: Web Crypto PKCE, auth tab, fetch token exchange, device-code polling), it is just a different `OAuthAuth` implementation — no reserved option values.
|
||||
|
||||
The existing flows in `src/utils/oauth/` (anthropic, openai-codex, github-copilot) are adapted to `OAuthAuth` (`login`/`refresh`/`toAuth`, replacing `login`/`refreshToken`/`getApiKey`/`modifyModels`) with the new callbacks, staying Node-targeted and lazy-loaded. Copilot's `modifyModels` baseUrl rewriting becomes `toAuth` returning `ModelAuth.baseUrl`.
|
||||
The built-in flows in `src/auth/oauth/` implement `OAuthAuth` and `AuthInteraction` directly while remaining Node-targeted and lazy-loaded. Copilot derives its credential-specific request endpoint through `toAuth().baseUrl`.
|
||||
|
||||
## Provider wrappers and models.json
|
||||
|
||||
@@ -817,7 +817,7 @@ Check items off as they land. Keep this list current; it is the working state fo
|
||||
|
||||
### Phase 3 — provider factories + catalogs
|
||||
|
||||
- [x] Auth helpers in `src/auth/helpers.ts`: `envApiKeyAuth()` (with secret-prompt `login`), `lazyOAuth()`. OAuth flow loads go through `utils/oauth/load.ts` (bundler-opaque dynamic import); the `OAuthAuth` exports it references land in Phase 4.
|
||||
- [x] Auth helpers in `src/auth/helpers.ts`: `envApiKeyAuth()` (with secret-prompt `login`), `lazyOAuth()`. OAuth flow loads go through `auth/oauth/load.ts` (bundler-opaque dynamic import); the `OAuthAuth` exports it references land in Phase 4.
|
||||
- [x] `createProvider()` in `models.ts` (single + mixed `api` map, dispatch on `model.api`, unknown api -> stream error).
|
||||
- [x] Per-provider factories under `src/providers/` for all built-in catalog providers; OAuth attached via `lazyOAuth()` (anthropic, openai-codex, github-copilot); ambient `ApiKeyAuth` for amazon-bedrock (AWS env/profile) and google-vertex (key or ADC+project+location).
|
||||
- [x] `providers/all.ts`: `builtinProviders()`, `builtinModels()`, `getBuiltinModel/getBuiltinModels/getBuiltinProviders` re-exports.
|
||||
@@ -826,8 +826,8 @@ Check items off as they land. Keep this list current; it is the working state fo
|
||||
|
||||
### Phase 4 — OAuth adaptation
|
||||
|
||||
- [x] Adapt `utils/oauth/anthropic.ts`, `openai-codex.ts`, `github-copilot.ts` to `OAuthAuth` (`login`/`refresh`/`toAuth`) + `prompt()/notify()`; `modifyModels` baseUrl rewriting becomes `toAuth().baseUrl`. New exports (`anthropicOAuth`, `openaiCodexOAuth`, `githubCopilotOAuth`) sit next to the old `OAuthProviderInterface` objects, which survive until Phase 7.
|
||||
- [x] No `usesCallbackServer` on `OAuthAuth`: callback-server flows race a `manual_code` prompt (aborted via `AuthPrompt.signal` once the flow settles). The old interface keeps its flag until it dies with compat.
|
||||
- [x] Built-in implementations live under `auth/oauth/` and implement `OAuthAuth` directly through `AuthInteraction.prompt()`/`notify()`. They are private provider implementations loaded lazily by provider factories.
|
||||
- [x] Callback-server flows race a `manual_code` prompt, aborted through `AuthPrompt.signal` once the flow settles. The public `oauth` subpath retains only coding-agent extension compatibility types.
|
||||
|
||||
### Phase 5 — packaging
|
||||
|
||||
@@ -876,7 +876,7 @@ Decisions:
|
||||
- Runtime `--api-key` overrides are an explicit store overlay (an override reads as an ephemeral stored api-key credential, masking stored OAuth — matches today's priority). Every registered provider is guaranteed an `apiKey` auth slot so overrides apply to OAuth-only providers too.
|
||||
- `ModelRegistry.getAll`/`find`/`getAvailable` stay sync for SDK and extension compatibility, delegating to the collection's last-known sync model lists and fast configured-looking status checks. Dynamic providers update through explicit async `refresh()`, and request auth remains async through `getApiKeyAndHeaders()`/`Models.getAuth()`. Extensions also get the collection itself as the forward API.
|
||||
- models.json keeps FULL feature parity, implemented as provider decoration: builtin factories wrapped so `getModels()` applies provider `baseUrl`/`compat` overlays, `modelOverrides`, and custom-model merges (async-safe); provider `apiKey`/`headers`/`authHeader` configs become that provider's `ApiKeyAuth` (config first, factory auth fallback); parse errors keep `getError()` semantics.
|
||||
- Extension `ProviderConfig` parity: provider-keyed `streamSimple`, old-style `oauth` adapted to `OAuthAuth` (`modifyModels` -> `getModels` wrap + `toAuth`), full model replacement per provider. Legacy `registerApiProvider` writes stay compat-local for consumers that call global `complete()`; they die with compat.
|
||||
- Extension `ProviderConfig` parity: provider-keyed `streamSimple`, legacy extension OAuth callbacks adapted to `OAuthAuth`, and full model replacement per provider. Legacy `registerApiProvider` writes stay compat-local for consumers that call global `complete()`; they die with compat.
|
||||
- Copilot: stored-credential baseUrl applied in the wrapped `getModels()` (extension-visible models stay correct) plus per-request `toAuth().baseUrl`.
|
||||
- Cloudflare: provider-auth substitution (key + `CLOUDFLARE_ACCOUNT_ID`/`CLOUDFLARE_GATEWAY_ID` from credential `env` or ambient `AuthContext.env()` -> `ModelAuth.baseUrl`). Built-in compat calls route through `Models`, so they use the same provider auth path.
|
||||
|
||||
@@ -909,13 +909,11 @@ Ordering for new sessions:
|
||||
- Wrap Copilot's provider `getModels()` when an OAuth credential is present so extension/UI-visible model metadata also carries the authenticated account base URL.
|
||||
- Keep API-key/env-token Copilot behavior unchanged.
|
||||
- Add tests for model metadata before login, after OAuth credential, after refresh/baseUrl change, and logout.
|
||||
7. [ ] Extension OAuth adapter.
|
||||
- Adapt old extension `OAuthProviderInterface` configs to pi-ai `OAuthAuth`.
|
||||
- `login` maps old callbacks/events to `prompt()/notify()`.
|
||||
- `refreshToken` maps to `refresh`.
|
||||
- `getApiKey` maps to `toAuth`.
|
||||
- `modifyModels` becomes a provider `getModels()` wrapper plus `toAuth().baseUrl` where applicable.
|
||||
- Preserve existing extension runtime compatibility through the `/compat` alias until Phase 10.
|
||||
7. [x] Extension OAuth adapter.
|
||||
- Keep only the legacy callback/credential declarations required by coding-agent `ProviderConfig.oauth`.
|
||||
- `login` maps legacy callbacks/events to `AuthInteraction.prompt()`/`notify()`.
|
||||
- `refreshToken` maps to `refresh`; `getApiKey` maps to `toAuth`.
|
||||
- Preserve the type-only pi-ai `oauth` barrel and extension-loader aliases.
|
||||
8. [ ] Rebuild coding-agent `ModelRegistry` over `MutableModels`.
|
||||
- It owns a `MutableModels` instance built from decorated built-ins + models.json custom providers + extension providers.
|
||||
- `getAll()`, `find()`, and `getAvailable()` remain sync compatibility methods over last-known model lists and fast configured-looking auth status. Do not break the extension-facing `modelRegistry` surface for these reads.
|
||||
@@ -939,7 +937,7 @@ Ordering for new sessions:
|
||||
|
||||
- [ ] AgentSession -> AgentHarness; the registry facade dies in favor of harness `Models`.
|
||||
- [ ] Move ALL internal `/compat` imports to the new API: every package's src, all tests, and the example extensions (examples then demonstrate the new API). Nothing inside the repo may import `/compat` at that point.
|
||||
- [ ] Delete `/compat`, `env-api-keys.ts`, the extension-loader root-to-compat alias, the old `pi-ai/oauth` registry and `OAuthProviderInterface` (incl. `usesCallbackServer`), and the compat-local legacy API registry. This is the extension-author breaking release; changelog carries the migration guide.
|
||||
- [ ] Delete `/compat`, `env-api-keys.ts`, the extension-loader root-to-compat alias, and the compat-local legacy API registry. The old OAuth registry/provider interface is already gone; the type-only `oauth` barrel remains for extension compatibility.
|
||||
|
||||
### Deferred / follow-ups
|
||||
|
||||
|
||||
Reference in New Issue
Block a user