From 216ba41fc3602ca5367d9fd4e15065c0c5379756 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 8 Jun 2026 17:04:57 +0200 Subject: [PATCH 001/122] docs(agent): add models architecture design --- packages/agent/docs/models.md | 860 ++++++++++++++++++++++++++++++++++ 1 file changed, 860 insertions(+) create mode 100644 packages/agent/docs/models.md diff --git a/packages/agent/docs/models.md b/packages/agent/docs/models.md new file mode 100644 index 00000000..0814068f --- /dev/null +++ b/packages/agent/docs/models.md @@ -0,0 +1,860 @@ +# Models architecture + +This document describes the target design for the next `pi-ai` model/provider refactor. It intentionally describes the desired shape, not the current implementation. + +Goals: + +- `Models` is a dumb runtime collection of providers. +- Concrete providers own metadata, auth, model listing, and stream behavior. +- API implementations live under `src/ai/` and are reusable/lazy. +- Concrete provider factories live under `src/providers/`. +- Users can import only the providers they need. +- Importing a provider should not eagerly import heavy SDKs. +- Dynamic model lists are first-class and side-effect-free. +- `models.json` and extensions layer by wrapping providers, not by mutating provider internals ad hoc. + +Non-goals for the immediate `pi-ai` pass: + +- Do not migrate coding-agent `ModelRegistry` yet. +- Do not preserve old process-global APIs unless as explicit temporary compatibility shims. +- Do not keep the stream/API registry inside `Models`. + +## Package layout + +Target source layout: + +```txt +packages/ai/src/ + index.ts # core exports only; no built-in provider imports + models.ts # Models, Provider, auth, runtime types + auth/ # shared auth helpers, local/OAuth wrappers + ai/ # API implementations and lazy API wrappers + openai-compatible.ts # real implementation, imports SDKs + openai-compatible-lazy.ts # lightweight lazy wrapper + anthropic.ts + anthropic-lazy.ts + bedrock.ts + bedrock-lazy.ts + ... + providers/ # concrete provider factories and per-provider catalogs + openai.ts + openai.models.ts # OpenAI provider catalog + openai-codex.ts + openai-codex.models.ts # OpenAI Codex provider catalog + openrouter.ts + openrouter.models.ts + anthropic.ts + anthropic.models.ts + google-vertex.ts + google-vertex.models.ts + bedrock.ts + bedrock.models.ts + cloudflare-ai-gateway.ts + cloudflare-ai-gateway.models.ts + all.ts # explicit aggregate for pi CLI/coding-agent +``` + +`src/index.ts` must stay core-only. It must not import: + +- all generated model metadata +- built-in provider factories +- provider SDK implementations +- Node-only OAuth modules +- `providers/all` + +Provider and API entrypoints are explicit subpath exports. + +## Public usage + +Minimal provider usage: + +```ts +import { createModels } from "@earendil-works/pi-ai"; +import { openaiProvider } from "@earendil-works/pi-ai/providers/openai"; + +const models = createModels(); +models.setProvider(openaiProvider()); + +const model = await models.getModel("openai", "gpt-4o-mini"); +if (!model) throw new Error("model not found"); + +const response = await models.complete(model, context); +``` + +Multiple providers: + +```ts +import { createModels } from "@earendil-works/pi-ai"; +import { openaiProvider } from "@earendil-works/pi-ai/providers/openai"; +import { openrouterProvider } from "@earendil-works/pi-ai/providers/openrouter"; + +const models = createModels(); +models.setProvider(openaiProvider()); +models.setProvider(openrouterProvider()); +``` + +All built-ins, explicitly heavy metadata entrypoint: + +```ts +import { builtinModels } from "@earendil-works/pi-ai/providers/all"; + +const models = builtinModels(); +``` + +`providers/all` may import all provider metadata/catalogs. It still must not eagerly import heavy SDK implementations; provider streams use lazy wrappers. + +## Core runtime: Models + +`Models` is a provider collection plus auth application and stream convenience. It does not contain a stream registry. + +```ts +export interface Models { + getProviders(): readonly Provider[]; + getProvider(id: string): Provider | undefined; + + getModels(provider?: string, options?: { forceRefresh?: boolean }): Promise[]>; + getModel(provider: string, id: string, options?: { forceRefresh?: boolean }): Promise | undefined>; + + getAuth(model: Model): Promise; + + stream( + model: Model, + context: Context, + options?: ApiStreamOptions, + ): AssistantMessageEventStream; + + complete( + model: Model, + context: Context, + options?: ApiStreamOptions, + ): Promise; + + streamSimple( + model: Model, + context: Context, + options?: SimpleStreamOptions, + ): AssistantMessageEventStream; + + completeSimple( + model: Model, + context: Context, + options?: SimpleStreamOptions, + ): Promise; +} + +export interface MutableModels extends Models { + /** Upsert/replace by provider.id. Provider ids are unique. */ + setProvider(provider: Provider): void; + deleteProvider(id: string): void; + clearProviders(): void; + + getAuthResolver(): ModelAuthResolver; + setAuthResolver(resolver: ModelAuthResolver): void; +} +``` + +No stream registry: + +```txt +remove Models.setStreamFunctions() +remove Models.getStreamFunctions() +remove api-registry as real API +``` + +No provider builder mutation API as public API: + +```txt +remove/avoid Models.provider(id) +remove setModel/upsertModel/patchModel public lifecycle +``` + +A `MutableModels` implementation may still use internal maps, but the public object is provider-oriented. + +## Provider + +A provider is the concrete runtime unit. It owns: + +- id/name/base metadata +- auth behavior +- model listing +- stream behavior + +Full stream options are API-specific. The generic `Model` only pays off if the stream option type is derived from `TApi`. + +```ts +export type ApiStreamOptions = StreamOptionsForApi; + +export interface Provider { + readonly id: string; + readonly name: string; + + /** Default model API metadata/diagnostics, not Models dispatch. */ + readonly api?: Api; + + readonly baseUrl?: string; + readonly headers?: Record; + + /** Required. Use {} for no-auth providers. */ + readonly auth: ProviderAuth; + + getModels(options?: { forceRefresh?: boolean }): Promise[]>; + + stream( + model: Model, + context: Context, + options?: ApiStreamOptions, + ): AssistantMessageEventStream; + + streamSimple( + model: Model, + context: Context, + options?: SimpleStreamOptions, + ): AssistantMessageEventStream; +} +``` + +`Model.api` should remain for now because: + +- existing metadata and tests use it +- it is useful for diagnostics +- custom provider helpers may use it for API implementation selection + +But `Models` no longer dispatches through `model.api`. The provider does. + +## Provider model sources + +Provider model listing is async. + +```ts +export type ProviderModelSource = + | readonly Model[] + | ((options?: { forceRefresh?: boolean }) => Promise[]>); +``` + +Provider helpers can accept `ProviderModel[]` and resolve provider defaults, but public `Provider.getModels()` returns full `Model` objects. + +Dynamic model sources must be side-effect-free discovery: + +```txt +OK: fetch /v1/models, enumerate local catalog, refresh cached remote model list +Not OK: load model, download model, mutate server state, run request probe +``` + +Provider-specific model lifecycle belongs in app/provider-management commands, not in `getModels()`. + +## Streaming path + +`Models.stream()` finds the provider by `model.provider`, resolves request auth, applies request-scoped auth, and delegates to the provider. + +```ts +async function stream(model, context, options) { + const provider = getProvider(model.provider); + if (!provider) throw new ModelsError(...); + + const auth = await getAuth(model); + const requestModel = auth?.baseUrl ? { ...model, baseUrl: auth.baseUrl } : model; + const requestOptions = mergeAuthIntoOptions(options, auth); + + return provider.stream(requestModel, context, requestOptions); +} +``` + +`stream()` still returns `AssistantMessageEventStream` synchronously. Async setup happens inside the returned stream, as today. + +No request hot-path model canonicalization. If an app wants fresh model metadata after refresh, it must call: + +```ts +const model = await models.getModel(provider, id, { forceRefresh: true }); +``` + +before starting the turn. + +## API implementations under `src/ai` + +An API implementation is reusable stream behavior. It is not a provider. + +Example real implementation: + +```ts +// src/ai/openai-compatible.ts +import OpenAI from "openai"; + +export function streamOpenAICompatible(...) { ... } +export function streamSimpleOpenAICompatible(...) { ... } +``` + +Example lazy wrapper: + +```ts +// src/ai/openai-compatible-lazy.ts +export function openAICompatibleApi(): ProviderStreams { + return { + stream(model, context, options) { + return lazyStream(() => + import("./openai-compatible.ts").then((m) => + m.streamOpenAICompatible(model, context, options), + ), + ); + }, + + streamSimple(model, context, options) { + return lazyStream(() => + import("./openai-compatible.ts").then((m) => + m.streamSimpleOpenAICompatible(model, context, options), + ), + ); + }, + }; +} +``` + +Provider modules import lazy API wrappers, never real SDK-heavy implementation modules. + +```txt +provider module -> lazy API wrapper -> dynamic import(real API impl) -> SDK deps +``` + +This preserves both: + +- provider-owned stream behavior +- lazy SDK loading + +## Shared API implementations across concrete providers + +Many concrete providers share an API implementation. Example: + +- OpenAI +- OpenRouter +- Groq +- Together +- DeepSeek +- Cloudflare AI Gateway OpenAI-compatible models + +They should share lazy API objects by reference, not through `Models` stream registry. + +```ts +import { openAICompatibleApi } from "../ai/openai-compatible-lazy.ts"; + +const api = openAICompatibleApi(); + +export function openrouterProvider(): Provider { + return { + id: "openrouter", + name: "OpenRouter", + api: "openai-completions", + baseUrl: "https://openrouter.ai/api/v1", + auth: { local: envLocalAuth(["OPENROUTER_API_KEY"]) }, + getModels: staticModels(OPENROUTER_MODELS), + stream: api.stream, + streamSimple: api.streamSimple, + }; +} +``` + +This copies Vercel AI SDK’s useful property: users import concrete providers, while shared protocol implementation is internal. + +## Auth + +Request auth output stays small. + +```ts +export interface ModelAuth { + apiKey?: string; + headers?: Record; + baseUrl?: string; +} +``` + +No `streamOptions` in auth. If a value cannot be expressed as `apiKey`, `headers`, or `baseUrl`, it is provider config, not auth. + +Provider auth: + +```ts +export interface ProviderAuth { + local?: LocalAuthProvider; + oauth?: OAuthProvider; +} +``` + +`auth` is required on `Provider`; no-auth providers use `{}`. + +### Local auth + +Local auth covers non-OAuth credentials: + +- env API keys +- files on disk +- ambient SDK credentials +- AuthStorage local credentials +- models.json local credentials +- provider-specific credential metadata + +```ts +export interface ProviderAuthContext { + env(name: string): Promise; + fileExists(path: string): Promise; // supports leading ~ +} + +export interface LocalCredential { + type: "local"; + key?: string; + metadata?: Record; +} + +export interface OAuthCredential extends OAuthCredentials { + type: "oauth"; +} + +export type Credential = LocalCredential | OAuthCredential; + +export interface LocalAuthProvider { + id: string; + name: string; + + login?(callbacks: AuthLoginCallbacks): Promise; + + resolve(input: { + model: Model; + ctx: ProviderAuthContext; + credential?: LocalCredential; + }): Promise; +} + +export interface AuthResolution { + auth: ModelAuth; + sources: readonly ProviderAuthSource[]; +} + +export type ProviderAuthSource = + | { type: "env"; name: string } + | { type: "file"; path: string; label?: string } + | { type: "ambient"; label: string }; +``` + +Local auth receives an optional credential from the app. It does not read AuthStorage itself. + +Examples: + +- OpenAI: `credential.key ?? env("OPENAI_API_KEY")` -> `{ apiKey }` +- Bedrock: bearer token -> `{ apiKey }`; AWS profile/IAM/ECS/IRSA -> `{}` +- Vertex: API key -> `{ apiKey }`; ADC files -> `{}` +- Cloudflare: key + account/gateway metadata/env -> `{ apiKey, baseUrl }` + +### OAuth + +```ts +export interface OAuthProvider { + id: string; + name: string; + usesCallbackServer?: boolean; + + login(callbacks: AuthLoginCallbacks): Promise; + + resolve(credentials: OAuthCredential): Promise<{ + credentials: OAuthCredential; + auth: ModelAuth; + }>; +} +``` + +OAuth receives stored OAuth credentials, may refresh them, and returns updated credentials for the app to persist. + +### Login callbacks + +One callback interface serves local and OAuth login. Use the nicer `prompt()` / `notify()` shape now instead of carrying forward the ad hoc OAuth callback bag. + +```ts +export interface AuthLoginCallbacks { + signal?: AbortSignal; + + prompt( + prompt: TPrompt, + options?: { signal?: AbortSignal }, + ): Promise>; + + notify(event: AuthEvent): void; +} + +export type AuthPrompt = + | { + type: "text"; + id: string; + message: string; + placeholder?: string; + allowEmpty?: boolean; + required?: boolean; + } + | { + type: "secret"; + id: string; + message: string; + placeholder?: string; + required?: boolean; + } + | { + type: "select"; + id: string; + message: string; + options: readonly { id: string; label: string; description?: string }[]; + } + | { + type: "manual_code"; + id: string; + message: string; + placeholder?: string; + }; + +export type AuthPromptResult = string; + +export type AuthEvent = + | { type: "auth_url"; url: string; instructions?: string } + | { + type: "device_code"; + userCode: string; + verificationUri: string; + intervalSeconds?: number; + expiresInSeconds?: number; + } + | { type: "progress"; message: string }; +``` + +Codex browser login can race a `manual_code` prompt against a callback server by passing an abort signal to `prompt(..., { signal })` and aborting the prompt when the callback wins. + +### OAuth implementation target + +OAuth providers must not force Node-only code into browser bundles. Keep OAuth lazy, and let each concrete provider factory decide whether to attach a Node OAuth implementation, a web OAuth implementation, or no OAuth implementation. + +Do not build a universal OAuth runtime abstraction in this refactor. The provider factory option is enough: + +```ts +export type OAuthTarget = "node" | "web" | false; + +export interface AnthropicProviderOptions { + oauth?: OAuthTarget; +} + +export function anthropicProvider(options: AnthropicProviderOptions = {}): Provider { + return { + id: "anthropic", + name: "Anthropic", + api: "anthropic", + baseUrl: "https://api.anthropic.com/v1", + auth: { + local: envLocalAuth("anthropic-api-key", "Anthropic API key", ["ANTHROPIC_API_KEY"]), + oauth: + options.oauth === "node" + ? lazyOAuthProvider({ + id: "anthropic", + name: "Anthropic (Claude Pro/Max)", + usesCallbackServer: true, + load: () => import("../oauth/anthropic-node.ts").then((m) => m.anthropicOAuthProvider), + }) + : options.oauth === "web" + ? lazyOAuthProvider({ + id: "anthropic", + name: "Anthropic (Claude Pro/Max)", + load: () => import("../oauth/anthropic-web.ts").then((m) => m.anthropicOAuthProvider), + }) + : undefined, + }, + getModels: staticModels(ANTHROPIC_MODELS), + stream: anthropicApi().stream, + streamSimple: anthropicApi().streamSimple, + }; +} +``` + +Recommended defaults: + +- individual provider factories default to `oauth: false` unless we intentionally want Node defaults +- `providers/all` for pi CLI/coding-agent calls providers with `oauth: "node"` +- browser users call providers with `oauth: "web"` +- users that only want API-key/env auth leave OAuth disabled + +Sitegeist demonstrates that browser-compatible OAuth is practical for Anthropic, OpenAI Codex, GitHub Copilot, and Gemini CLI. The browser implementations use Web Crypto, auth tabs, localhost redirect URL watching through extension tab APIs, `fetch` for token exchange, CORS permissions/proxies where needed, and device-code polling for Copilot. + +So the target is not “OAuth is Node-only”. The target is: provider factories attach the right lazy OAuth module for the runtime the caller asked for. + +Use a lazy wrapper so provider definitions can advertise OAuth without importing the actual implementation: + +```ts +export function lazyOAuthProvider(input: { + id: string; + name: string; + usesCallbackServer?: boolean; + load: () => Promise; +}): OAuthProvider { + return { + id: input.id, + name: input.name, + usesCallbackServer: input.usesCallbackServer, + async login(callbacks) { + return (await input.load()).login(callbacks); + }, + async resolve(credentials) { + return (await input.load()).resolve(credentials); + }, + }; +} +``` + +## Auth resolution policy + +`pi-ai` can ship a default resolver using injected context/store. Applications can replace it. + +Recommended default order, low to high precedence: + +```txt +provider local auth defaults +-> CredentialStore local/OAuth credential +-> explicit request auth +``` + +coding-agent later adds models.json and CLI policy: + +```txt +provider local auth defaults +-> AuthStorage credential +-> models.json auth sidecar +-> CLI/runtime explicit request auth +``` + +Auth values merge: + +- later `apiKey` wins +- later `baseUrl` wins +- headers shallow-merge; later wins per header + +Cloudflare requires merge, not early return. It may need env account/gateway + stored key, or stored metadata + env token. + +## Provider wrappers and models.json + +`models.json` is naturally a provider wrapper layer. + +It should not mutate a provider in place. It should wrap: + +```ts +function withProviderOverrides(base: Provider, overrides: ProviderOverrides): Provider { + return { + ...base, + name: overrides.name ?? base.name, + baseUrl: overrides.baseUrl ?? base.baseUrl, + headers: mergeHeaders(base.headers, overrides.headers), + + async getModels(options) { + const models = await base.getModels(options); + return applyModelOverrides(models, overrides.models); + }, + + stream: base.stream, + streamSimple: base.streamSimple, + }; +} +``` + +This composes with dynamic providers because `getModels()` delegates to the base provider source. + +Request-auth config from models.json remains app-owned sidecar state. It is not stored in `Provider` unless it is true provider metadata such as base URL or headers. + +## Custom providers from models.json + +A models.json custom provider must become a concrete `Provider` object. + +### Single API custom provider + +If all models use one known API: + +```json +{ + "providers": { + "my-openai-proxy": { + "api": "openai-completions", + "baseUrl": "https://proxy.example/v1", + "models": [ ... ] + } + } +} +``` + +coding-agent/pi-ai helper can build: + +```ts +createApiBackedProvider({ + id: "my-openai-proxy", + name: "my-openai-proxy", + api: "openai-completions", + baseUrl: "https://proxy.example/v1", + auth: {}, + models, + apiImplementation: openAICompatibleApi(), +}); +``` + +This helper lives outside `Models`; it is provider construction sugar. + +### Mixed API custom provider + +Custom providers with mixed APIs must be supported. Existing providers such as opencode-go/zen can expose models backed by different APIs under one provider id. In this design that means the provider dispatches internally. + +```ts +createDispatchProvider({ + id, + models, + apis: { + "openai-completions": openAICompatibleApi(), + "anthropic": anthropicApi(), + }, +}); +``` + +The returned provider still exposes only: + +```ts +stream(model, context, options) +streamSimple(model, context, options) +``` + +Internally it switches on `model.api` and calls the right lazy API implementation. + +This preserves the rule that `Models` has no stream registry while supporting required mixed-API providers. + +## Tree-shaking and lazy imports + +Rules: + +1. Main `@earendil-works/pi-ai` import is core-only. +2. Provider modules import metadata, model catalog, auth helpers, and lazy API wrappers only. +3. Lazy API wrappers dynamically import real API implementations. +4. Real API implementations import SDK dependencies. +5. OAuth providers are selected by provider factory option (`oauth: "node" | "web" | false`) and lazy-loaded; provider metadata must not eagerly import Node-only OAuth code. +6. `providers/all` is explicit and allowed to import all provider metadata, but still no eager SDK imports. +7. Provider modules are side-effect-free; importing a provider does not register it globally. +8. `package.json` should set `sideEffects: false` if all entrypoints are side-effect-free. + +Example exports: + +```json +{ + "exports": { + ".": "./dist/index.js", + "./providers/openai": "./dist/providers/openai.js", + "./providers/anthropic": "./dist/providers/anthropic.js", + "./providers/openrouter": "./dist/providers/openrouter.js", + "./providers/all": "./dist/providers/all.js", + "./ai/openai-compatible": "./dist/ai/openai-compatible-lazy.js" + } +} +``` + +To avoid metadata bloat for minimal users, generated model catalogs should be split per provider. Until then, any provider module importing the monolithic generated catalog can pull more metadata than necessary. + +## Static typed helpers + +The old global sync helpers are incompatible with dynamic providers: + +```ts +getModel(...) +getModels(...) +getProviders(...) +``` + +If they mean runtime lookup, they must be async. If they remain sync and read only built-ins, they are misleading. + +Target: + +- remove old global runtime helpers, or make them async and default-instance backed only in a compatibility entrypoint +- add explicit static catalog helpers if type-safe built-in lookup is still desired + +```ts +getBuiltinModel(provider, id) // sync, generated catalog only +getBuiltinModels(provider) // sync, generated catalog only +getBuiltinProviders() // sync, generated catalog only +``` + +Runtime lookup is always: + +```ts +await models.getModel(provider, id) +await models.getModels(provider) +``` + +## AgentHarness integration + +`AgentHarness` receives a `Models` instance. + +Rules: + +- `AgentHarnessOptions.models` is required +- harness does not snapshot `Models` into turn state +- request path calls `models.streamSimple(model, context, options)` or equivalent +- request path does not call async `models.getModel()` to canonicalize +- if model metadata needs refresh, app updates the selected model before starting a turn + +## coding-agent next phase + +coding-agent should build providers in layers: + +```txt +built-in providers +-> models.json provider wrappers +-> extension provider wrappers/additions +``` + +Then: + +```ts +sessionModels.clearProviders(); +for (const provider of layeredProviders) sessionModels.setProvider(provider); +sessionModels.setAuthResolver(codingAgentResolver); +``` + +coding-agent owns: + +- AuthStorage local/OAuth files +- models.json auth sidecar +- `$ENV` and `!command` +- command execution policy +- provider status labels +- login/logout UI +- extension lifecycle +- provider-management slash commands + +## Migration TODOs + +1. Restore/remove half-implemented old auth/stream-registry changes before starting this design. +2. Redesign `packages/ai/src/models.ts` around provider-owned streams. +3. Remove `StreamFunctions` registry from `Models` public API. +4. Introduce `Provider` with required `auth`, async `getModels()`, `stream()`, and `streamSimple()`. +5. Add lazy API wrappers under `packages/ai/src/ai/`. +6. Move real API implementations under `packages/ai/src/ai/` or adapt existing stream files into that layout. +7. Add concrete provider factories under `packages/ai/src/providers/`. +8. Add `providers/all` explicit aggregate. +9. Add `lazyOAuthProvider()`, `OAuthTarget`, and provider factory options such as `anthropicProvider({ oauth: "node" | "web" | false })`. +10. Convert built-in OAuth attachment to lazy target-specific wrappers. +11. Split generated model catalogs per provider, or mark as follow-up if too large. +12. Replace old global `defaultModels()`/global helpers with explicit instance usage or compatibility entrypoint. +13. Add custom provider helpers: + - `createApiBackedProvider()` + - `createDispatchProvider()` for required mixed-API providers +14. Update `AgentHarness` to use provider-owned `models.streamSimple()` without stream registry lookups. +15. Keep coding-agent compatibility only as needed until the coding-agent `ModelManager` migration. +16. Update tests to construct explicit `Models` instances and install only needed providers/faux providers. + +## Error behavior + +`undefined` means not found or not configured. +Real failures reject or become stream errors. + +Recommended error codes: + +```ts +export type ModelsErrorCode = + | "model_source" + | "model_validation" + | "provider" + | "stream" + | "auth" + | "oauth"; +``` + +`Models.stream()` should produce stream errors for async setup failures. `getModels()` should isolate provider source failures when listing all providers if possible, so one dynamic provider failure does not prevent listing other providers. From 9f9705173a0967ca92f0a16180e6fbaa9b5ad5b3 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 10 Jun 2026 01:39:11 +0200 Subject: [PATCH 002/122] docs(agent): consolidate models architecture design Unified auth methods (api-key/oauth discriminated union), single createProvider() helper, src/api layout, compat entrypoint design, fixed auth resolution policy, prompt/notify login callbacks, and checkbox implementation TODOs. --- packages/agent/docs/models.md | 903 +++++++++++++++------------------- 1 file changed, 402 insertions(+), 501 deletions(-) diff --git a/packages/agent/docs/models.md b/packages/agent/docs/models.md index 0814068f..0a31b9d7 100644 --- a/packages/agent/docs/models.md +++ b/packages/agent/docs/models.md @@ -1,23 +1,25 @@ # Models architecture -This document describes the target design for the next `pi-ai` model/provider refactor. It intentionally describes the desired shape, not the current implementation. +This document describes the target design for the next `pi-ai` model/provider refactor. It describes the desired shape, not the current implementation. It is intended to be complete enough to start implementing from a fresh session. Goals: - `Models` is a dumb runtime collection of providers. - Concrete providers own metadata, auth, model listing, and stream behavior. -- API implementations live under `src/ai/` and are reusable/lazy. +- API implementations live under `src/api/` and are reusable/lazy. - Concrete provider factories live under `src/providers/`. - Users can import only the providers they need. -- Importing a provider should not eagerly import heavy SDKs. +- Importing a provider must not eagerly import heavy SDKs. - Dynamic model lists are first-class and side-effect-free. - `models.json` and extensions layer by wrapping providers, not by mutating provider internals ad hoc. +- Old global APIs survive only in an explicit, temporary `/compat` entrypoint. Non-goals for the immediate `pi-ai` pass: - Do not migrate coding-agent `ModelRegistry` yet. -- Do not preserve old process-global APIs unless as explicit temporary compatibility shims. - Do not keep the stream/API registry inside `Models`. +- Do not implement web OAuth flows yet (the factory option is reserved). +- Images (`images.ts`, `images-api-registry.ts`) are out of scope; leave untouched. ## Package layout @@ -26,43 +28,55 @@ Target source layout: ```txt packages/ai/src/ index.ts # core exports only; no built-in provider imports - models.ts # Models, Provider, auth, runtime types - auth/ # shared auth helpers, local/OAuth wrappers - ai/ # API implementations and lazy API wrappers - openai-compatible.ts # real implementation, imports SDKs - openai-compatible-lazy.ts # lightweight lazy wrapper - anthropic.ts - anthropic-lazy.ts - bedrock.ts - bedrock-lazy.ts - ... + models.ts # Models runtime, Provider, auth types + compat.ts # temporary old-API compatibility entrypoint + auth/ # auth method types, helpers, login callbacks + api/ # API implementations and lazy wrappers + openai-completions.ts # real implementation, imports SDKs, exports stream/streamSimple + openai-completions-lazy.ts + openai-responses.ts + openai-responses-lazy.ts + openai-codex-responses.ts + openai-codex-responses-lazy.ts + azure-openai-responses.ts + azure-openai-responses-lazy.ts + anthropic-messages.ts + anthropic-messages-lazy.ts + google-generative-ai.ts + google-generative-ai-lazy.ts + google-vertex.ts + google-vertex-lazy.ts + mistral-conversations.ts + mistral-conversations-lazy.ts + bedrock-converse-stream.ts + bedrock-converse-stream-lazy.ts + lazy.ts # lazyStream()/lazyApi() helpers + (shared helpers: openai-responses-shared, google-shared, transform-messages, ...) providers/ # concrete provider factories and per-provider catalogs openai.ts - openai.models.ts # OpenAI provider catalog + openai.models.ts # generated OpenAI catalog openai-codex.ts - openai-codex.models.ts # OpenAI Codex provider catalog - openrouter.ts - openrouter.models.ts + openai-codex.models.ts anthropic.ts anthropic.models.ts - google-vertex.ts - google-vertex.models.ts - bedrock.ts - bedrock.models.ts - cloudflare-ai-gateway.ts - cloudflare-ai-gateway.models.ts - all.ts # explicit aggregate for pi CLI/coding-agent + google.ts + google.models.ts + ...one pair per built-in provider... + faux.ts # test provider factory + all.ts # explicit aggregate: builtinModels(), getBuiltin*() + utils/oauth/ # OAuth flow implementations (node), lazy-loaded ``` `src/index.ts` must stay core-only. It must not import: -- all generated model metadata +- generated model catalogs - built-in provider factories - provider SDK implementations - Node-only OAuth modules - `providers/all` +- `compat` -Provider and API entrypoints are explicit subpath exports. +Provider, API, and compat entrypoints are explicit subpath exports. ## Public usage @@ -84,10 +98,6 @@ const response = await models.complete(model, context); Multiple providers: ```ts -import { createModels } from "@earendil-works/pi-ai"; -import { openaiProvider } from "@earendil-works/pi-ai/providers/openai"; -import { openrouterProvider } from "@earendil-works/pi-ai/providers/openrouter"; - const models = createModels(); models.setProvider(openaiProvider()); models.setProvider(openrouterProvider()); @@ -98,16 +108,18 @@ All built-ins, explicitly heavy metadata entrypoint: ```ts import { builtinModels } from "@earendil-works/pi-ai/providers/all"; -const models = builtinModels(); +const models = builtinModels({ oauth: "node" }); ``` -`providers/all` may import all provider metadata/catalogs. It still must not eagerly import heavy SDK implementations; provider streams use lazy wrappers. +`providers/all` may import all provider metadata/catalogs. It still must not eagerly import SDK implementations; provider streams use lazy wrappers. ## Core runtime: Models -`Models` is a provider collection plus auth application and stream convenience. It does not contain a stream registry. +`Models` is a provider collection plus auth application and stream convenience. No stream registry, no auth resolver strategy object. ```ts +export function createModels(options?: { credentials?: CredentialStore }): MutableModels; + export interface Models { getProviders(): readonly Provider[]; getProvider(id: string): Provider | undefined; @@ -115,7 +127,8 @@ export interface Models { getModels(provider?: string, options?: { forceRefresh?: boolean }): Promise[]>; getModel(provider: string, id: string, options?: { forceRefresh?: boolean }): Promise | undefined>; - getAuth(model: Model): Promise; + /** Resolve request auth for a model. Includes source label for status UI. */ + getAuth(model: Model): Promise; stream( model: Model, @@ -129,17 +142,8 @@ export interface Models { options?: ApiStreamOptions, ): Promise; - streamSimple( - model: Model, - context: Context, - options?: SimpleStreamOptions, - ): AssistantMessageEventStream; - - completeSimple( - model: Model, - context: Context, - options?: SimpleStreamOptions, - ): Promise; + streamSimple(model: Model, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream; + completeSimple(model: Model, context: Context, options?: SimpleStreamOptions): Promise; } export interface MutableModels extends Models { @@ -147,55 +151,34 @@ export interface MutableModels extends Models { setProvider(provider: Provider): void; deleteProvider(id: string): void; clearProviders(): void; - - getAuthResolver(): ModelAuthResolver; - setAuthResolver(resolver: ModelAuthResolver): void; } ``` -No stream registry: +Removed concepts: ```txt -remove Models.setStreamFunctions() -remove Models.getStreamFunctions() -remove api-registry as real API +no Models.setStreamFunctions() / getStreamFunctions() +no api-registry as a real dispatch mechanism +no Models.provider(id) builder, no setModel/upsertModel/patchModel lifecycle +no ModelAuthResolver / setAuthResolver — resolution policy is fixed, store is injected ``` -No provider builder mutation API as public API: - -```txt -remove/avoid Models.provider(id) -remove setModel/upsertModel/patchModel public lifecycle -``` - -A `MutableModels` implementation may still use internal maps, but the public object is provider-oriented. +If an app needs different auth policy, it wraps providers (wrap auth methods or `getModels`) or passes explicit request auth in stream options. ## Provider -A provider is the concrete runtime unit. It owns: - -- id/name/base metadata -- auth behavior -- model listing -- stream behavior - -Full stream options are API-specific. The generic `Model` only pays off if the stream option type is derived from `TApi`. +A provider is the concrete runtime unit. It owns id/name/base metadata, auth methods, model listing, and stream behavior. ```ts -export type ApiStreamOptions = StreamOptionsForApi; - export interface Provider { readonly id: string; readonly name: string; - /** Default model API metadata/diagnostics, not Models dispatch. */ - readonly api?: Api; - readonly baseUrl?: string; readonly headers?: Record; - /** Required. Use {} for no-auth providers. */ - readonly auth: ProviderAuth; + /** Required. Empty array for no-auth providers. */ + readonly auth: readonly AuthMethod[]; getModels(options?: { forceRefresh?: boolean }): Promise[]>; @@ -205,157 +188,146 @@ export interface Provider { options?: ApiStreamOptions, ): AssistantMessageEventStream; - streamSimple( - model: Model, - context: Context, - options?: SimpleStreamOptions, - ): AssistantMessageEventStream; + streamSimple(model: Model, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream; } ``` -`Model.api` should remain for now because: +There is no `Provider.api` field. `model.api` carries API identity; the provider dispatches internally (see `createProvider()`). -- existing metadata and tests use it -- it is useful for diagnostics -- custom provider helpers may use it for API implementation selection +`Model.api` remains: existing metadata and tests use it, it is useful for diagnostics, and provider construction uses it for API implementation selection. But `Models` never dispatches on it; the provider does. -But `Models` no longer dispatches through `model.api`. The provider does. +### Typed stream options -## Provider model sources - -Provider model listing is async. +Full stream options are API-specific. `Model` pays off by deriving the option type from the API: ```ts -export type ProviderModelSource = - | readonly Model[] - | ((options?: { forceRefresh?: boolean }) => Promise[]>); +// types.ts — type-only imports from API impl modules are erased, so this is tree-shake safe +export interface ApiOptionsMap { + "anthropic-messages": AnthropicOptions; + "openai-completions": OpenAICompletionsOptions; + "openai-responses": OpenAIResponsesOptions; + "openai-codex-responses": OpenAICodexResponsesOptions; + "azure-openai-responses": AzureOpenAIResponsesOptions; + "google-generative-ai": GoogleOptions; + "google-vertex": GoogleVertexOptions; + "mistral-conversations": MistralOptions; + "bedrock-converse-stream": BedrockOptions; +} + +export type ApiStreamOptions = TApi extends keyof ApiOptionsMap + ? ApiOptionsMap[TApi] + : StreamOptions & Record; ``` -Provider helpers can accept `ProviderModel[]` and resolve provider defaults, but public `Provider.getModels()` returns full `Model` objects. +Custom api strings fall back to the generic shape. -Dynamic model sources must be side-effect-free discovery: +### Name collision + +`types.ts` currently exports `type Provider = KnownProvider | string` (a provider id). Rename that alias to `ProviderId` and fix call sites. The `Provider` interface above takes the name. + +## Provider model listing + +`Provider.getModels()` is async and returns full `Model` objects. Static providers wrap their catalog; dynamic providers (llama.cpp, OpenRouter live listing) fetch and cache, honoring `forceRefresh`. + +Dynamic model listing must be side-effect-free discovery: ```txt OK: fetch /v1/models, enumerate local catalog, refresh cached remote model list Not OK: load model, download model, mutate server state, run request probe ``` -Provider-specific model lifecycle belongs in app/provider-management commands, not in `getModels()`. +Provider-specific model lifecycle (load/unload) belongs in app/provider-management commands, not in `getModels()`. ## Streaming path -`Models.stream()` finds the provider by `model.provider`, resolves request auth, applies request-scoped auth, and delegates to the provider. +`Models.stream()` finds the provider by `model.provider`, resolves auth, merges it into request options, and delegates: ```ts -async function stream(model, context, options) { - const provider = getProvider(model.provider); - if (!provider) throw new ModelsError(...); +function stream(model, context, options) { + const provider = this.getProvider(model.provider); + if (!provider) { + // produce an error stream, not a throw — see Error behavior + } - const auth = await getAuth(model); - const requestModel = auth?.baseUrl ? { ...model, baseUrl: auth.baseUrl } : model; - const requestOptions = mergeAuthIntoOptions(options, auth); + // async setup happens inside the returned stream (lazyStream pattern) + const resolution = await this.getAuth(model); + const requestModel = resolution?.auth.baseUrl ? { ...model, baseUrl: resolution.auth.baseUrl } : model; + const requestOptions = mergeAuth(options, resolution?.auth); // explicit options win per-field return provider.stream(requestModel, context, requestOptions); } ``` -`stream()` still returns `AssistantMessageEventStream` synchronously. Async setup happens inside the returned stream, as today. +`stream()` returns `AssistantMessageEventStream` synchronously; async setup (auth resolution, lazy module load) happens inside the returned stream. The forwarding pattern already exists in today's `register-builtins.ts` (`createLazyStream`); extract it as `lazyStream()` in `src/api/lazy.ts`. -No request hot-path model canonicalization. If an app wants fresh model metadata after refresh, it must call: +No request hot-path model canonicalization: `stream()` uses the supplied model object as-is. If an app wants fresh model metadata, it calls `models.getModel(provider, id, { forceRefresh: true })` before starting the turn. -```ts -const model = await models.getModel(provider, id, { forceRefresh: true }); -``` - -before starting the turn. - -## API implementations under `src/ai` +## API implementations under `src/api` An API implementation is reusable stream behavior. It is not a provider. -Example real implementation: +Uniform export contract — every real implementation module exports exactly: ```ts -// src/ai/openai-compatible.ts -import OpenAI from "openai"; - -export function streamOpenAICompatible(...) { ... } -export function streamSimpleOpenAICompatible(...) { ... } +// src/api/anthropic-messages.ts — imports SDKs +export function stream(model, context, options) { ... } +export function streamSimple(model, context, options) { ... } ``` -Example lazy wrapper: +This makes the module itself satisfy `ProviderStreams`, so the lazy wrapper is one generic helper instead of bespoke per-API plumbing: ```ts -// src/ai/openai-compatible-lazy.ts -export function openAICompatibleApi(): ProviderStreams { - return { - stream(model, context, options) { - return lazyStream(() => - import("./openai-compatible.ts").then((m) => - m.streamOpenAICompatible(model, context, options), - ), - ); - }, - - streamSimple(model, context, options) { - return lazyStream(() => - import("./openai-compatible.ts").then((m) => - m.streamSimpleOpenAICompatible(model, context, options), - ), - ); - }, - }; +export interface ProviderStreams { + stream( + model: Model, + context: Context, + options?: ApiStreamOptions, + ): AssistantMessageEventStream; + streamSimple(model: Model, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream; } + +// src/api/lazy.ts +export function lazyApi(load: () => Promise): ProviderStreams; + +// src/api/anthropic-messages-lazy.ts +export const anthropicMessagesApi = (): ProviderStreams => lazyApi(() => import("./anthropic-messages.ts")); ``` -Provider modules import lazy API wrappers, never real SDK-heavy implementation modules. +Import chain: ```txt provider module -> lazy API wrapper -> dynamic import(real API impl) -> SDK deps ``` -This preserves both: +Notes: -- provider-owned stream behavior -- lazy SDK loading +- Bedrock keeps the node-only dynamic import trick (`importNodeOnlyProvider`, `.ts`/`.js` specifier rewrite) inside its lazy wrapper. `setBedrockProviderModule()` (used by the Bun build) moves into the bedrock lazy wrapper module. +- Shared helper modules (`openai-responses-shared.ts`, `google-shared.ts`, `transform-messages.ts`, prompt-cache, copilot headers) move to `src/api/` alongside the implementations. ## Shared API implementations across concrete providers -Many concrete providers share an API implementation. Example: - -- OpenAI -- OpenRouter -- Groq -- Together -- DeepSeek -- Cloudflare AI Gateway OpenAI-compatible models - -They should share lazy API objects by reference, not through `Models` stream registry. +Many concrete providers share an API implementation (OpenAI-completions: OpenRouter, Groq, Cerebras, xAI, ZAI, ...). They share lazy API objects by reference: ```ts -import { openAICompatibleApi } from "../ai/openai-compatible-lazy.ts"; - -const api = openAICompatibleApi(); +import { openAICompletionsApi } from "../api/openai-completions-lazy.ts"; export function openrouterProvider(): Provider { - return { + return createProvider({ id: "openrouter", name: "OpenRouter", - api: "openai-completions", baseUrl: "https://openrouter.ai/api/v1", - auth: { local: envLocalAuth(["OPENROUTER_API_KEY"]) }, - getModels: staticModels(OPENROUTER_MODELS), - stream: api.stream, - streamSimple: api.streamSimple, - }; + auth: [envApiKeyMethod({ id: "api-key", name: "OpenRouter API key", env: ["OPENROUTER_API_KEY"] })], + models: OPENROUTER_MODELS, + api: openAICompletionsApi(), + }); } ``` -This copies Vercel AI SDK’s useful property: users import concrete providers, while shared protocol implementation is internal. +This copies Vercel AI SDK's useful property: users import concrete providers; shared protocol implementation is internal. ## Auth -Request auth output stays small. +Request auth output stays small: ```ts export interface ModelAuth { @@ -365,52 +337,19 @@ export interface ModelAuth { } ``` -No `streamOptions` in auth. If a value cannot be expressed as `apiKey`, `headers`, or `baseUrl`, it is provider config, not auth. +If a value cannot be expressed as `apiKey`, `headers`, or `baseUrl`, it is provider config, not auth (Vertex project/location, Bedrock region/profile, Azure apiVersion are provider factory options). -Provider auth: +### Auth methods + +`Provider.auth` is a list of uniform auth methods. The `kind` discriminant keeps the UI's oauth-vs-api-key split and types the credential: ```ts -export interface ProviderAuth { - local?: LocalAuthProvider; - oauth?: OAuthProvider; -} -``` - -`auth` is required on `Provider`; no-auth providers use `{}`. - -### Local auth - -Local auth covers non-OAuth credentials: - -- env API keys -- files on disk -- ambient SDK credentials -- AuthStorage local credentials -- models.json local credentials -- provider-specific credential metadata - -```ts -export interface ProviderAuthContext { - env(name: string): Promise; - fileExists(path: string): Promise; // supports leading ~ -} - -export interface LocalCredential { - type: "local"; - key?: string; - metadata?: Record; -} - -export interface OAuthCredential extends OAuthCredentials { - type: "oauth"; -} - -export type Credential = LocalCredential | OAuthCredential; - -export interface LocalAuthProvider { - id: string; - name: string; +export interface ApiKeyAuthMethod { + kind: "api-key"; + id: string; // unique within provider, e.g. "api-key" + name: string; // "Anthropic API key" + /** Interactive setup (prompt for key/metadata). Absent = ambient-only (env, ADC, IAM). */ login?(callbacks: AuthLoginCallbacks): Promise; resolve(input: { @@ -420,218 +359,166 @@ export interface LocalAuthProvider { }): Promise; } -export interface AuthResolution { - auth: ModelAuth; - sources: readonly ProviderAuthSource[]; -} - -export type ProviderAuthSource = - | { type: "env"; name: string } - | { type: "file"; path: string; label?: string } - | { type: "ambient"; label: string }; -``` - -Local auth receives an optional credential from the app. It does not read AuthStorage itself. - -Examples: - -- OpenAI: `credential.key ?? env("OPENAI_API_KEY")` -> `{ apiKey }` -- Bedrock: bearer token -> `{ apiKey }`; AWS profile/IAM/ECS/IRSA -> `{}` -- Vertex: API key -> `{ apiKey }`; ADC files -> `{}` -- Cloudflare: key + account/gateway metadata/env -> `{ apiKey, baseUrl }` - -### OAuth - -```ts -export interface OAuthProvider { - id: string; - name: string; - usesCallbackServer?: boolean; +export interface OAuthAuthMethod { + kind: "oauth"; + id: string; // e.g. "oauth" + name: string; // "Anthropic (Claude Pro/Max)" login(callbacks: AuthLoginCallbacks): Promise; - resolve(credentials: OAuthCredential): Promise<{ - credentials: OAuthCredential; - auth: ModelAuth; - }>; + resolve(input: { + model: Model; + ctx: ProviderAuthContext; + credential?: OAuthCredential; + }): Promise; +} + +export type AuthMethod = ApiKeyAuthMethod | OAuthAuthMethod; + +export interface AuthResolution { + auth: ModelAuth; + /** Human-readable label for status UI: "ANTHROPIC_API_KEY", "OAuth", "~/.aws/credentials". */ + source?: string; + /** Present when the method refreshed/updated the credential; Models persists it via the store. */ + credential?: Credential; +} + +export interface ProviderAuthContext { + env(name: string): Promise; + fileExists(path: string): Promise; // supports leading ~ } ``` -OAuth receives stored OAuth credentials, may refresh them, and returns updated credentials for the app to persist. +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. + +### Credentials + +```ts +export interface LocalCredential { + type: "local"; + key?: string; + metadata?: Record; // e.g. Cloudflare accountId/gatewayId +} + +export interface OAuthCredential extends OAuthCredentials { + type: "oauth"; +} + +export type Credential = LocalCredential | OAuthCredential; +``` + +`LocalCredential.metadata` exists for providers like Cloudflare that store non-key values (account id, gateway id) alongside or instead of a key. The method's `resolve()` merges per field: `credential.key ?? env("CLOUDFLARE_API_TOKEN")`, `credential.metadata?.accountId ?? env("CLOUDFLARE_ACCOUNT_ID")`, etc. + +### Credential store + +The app injects storage; `pi-ai` ships an in-memory default. + +```ts +export interface CredentialStore { + get(providerId: string, methodId: string): Promise; + set(providerId: string, methodId: string, credential: Credential): Promise; + delete(providerId: string, methodId: string): Promise; +} +``` + +coding-agent later implements this over AuthStorage. Login/logout orchestration is app-owned: the app calls `method.login(callbacks)` and persists the returned credential itself. `Models` only *reads* the store during resolution, and *writes* refreshed credentials when `resolve()` returns an updated one (OAuth token refresh). + +### Resolution policy (fixed) + +`Models.getAuth(model)` resolves with a fixed policy. Precedence, highest first: + +```txt +1. explicit request auth (stream options apiKey/headers) — merged per-field on top, in stream() +2. methods with a stored credential, in provider auth list order +3. methods resolving without a credential (ambient/env), in provider auth list order +``` + +Two-pass over `provider.auth`: + +- Pass 1: for each method with a credential in the store, call `resolve({ model, ctx, credential })`; first non-undefined resolution wins. +- Pass 2: for each method, call `resolve({ model, ctx })`; first non-undefined resolution wins. + +So an explicit login (stored credential) beats ambient env vars regardless of list order; list order breaks ties. Per-field merging *within* one method (stored key + env account id) happens inside that method's `resolve()`. + +If a resolution carries an updated `credential`, `Models` persists it via the store before returning. ### Login callbacks -One callback interface serves local and OAuth login. Use the nicer `prompt()` / `notify()` shape now instead of carrying forward the ad hoc OAuth callback bag. +One interface serves api-key and OAuth login: ```ts export interface AuthLoginCallbacks { signal?: AbortSignal; - prompt( - prompt: TPrompt, - options?: { signal?: AbortSignal }, - ): Promise>; - + prompt(prompt: AuthPrompt, options?: { signal?: AbortSignal }): Promise; notify(event: AuthEvent): void; } export type AuthPrompt = - | { - type: "text"; - id: string; - message: string; - placeholder?: string; - allowEmpty?: boolean; - required?: boolean; - } - | { - type: "secret"; - id: string; - message: string; - placeholder?: string; - required?: boolean; - } - | { - type: "select"; - id: string; - message: string; - options: readonly { id: string; label: string; description?: string }[]; - } - | { - type: "manual_code"; - id: string; - message: string; - placeholder?: string; - }; - -export type AuthPromptResult = string; + | { type: "text"; message: string; placeholder?: string } + | { type: "secret"; message: string; placeholder?: string } + | { type: "select"; message: string; options: readonly { id: string; label: string; description?: string }[] } + | { type: "manual_code"; message: string; placeholder?: string }; export type AuthEvent = | { type: "auth_url"; url: string; instructions?: string } - | { - type: "device_code"; - userCode: string; - verificationUri: string; - intervalSeconds?: number; - expiresInSeconds?: number; - } + | { type: "device_code"; userCode: string; verificationUri: string; intervalSeconds?: number; expiresInSeconds?: number } | { type: "progress"; message: string }; ``` -Codex browser login can race a `manual_code` prompt against a callback server by passing an abort signal to `prompt(..., { signal })` and aborting the prompt when the callback wins. +`prompt()` returns the entered/selected string (`select` returns the option id). Flows race a `manual_code` prompt against a callback server by passing a per-prompt abort signal and aborting when the callback wins. ### OAuth implementation target -OAuth providers must not force Node-only code into browser bundles. Keep OAuth lazy, and let each concrete provider factory decide whether to attach a Node OAuth implementation, a web OAuth implementation, or no OAuth implementation. - -Do not build a universal OAuth runtime abstraction in this refactor. The provider factory option is enough: +OAuth must not force Node-only code (`node:http`, `node:crypto`) into browser bundles. Keep OAuth lazy; the provider factory decides which implementation to attach: ```ts export type OAuthTarget = "node" | "web" | false; export interface AnthropicProviderOptions { - oauth?: OAuthTarget; + oauth?: OAuthTarget; // default false } export function anthropicProvider(options: AnthropicProviderOptions = {}): Provider { - return { + return createProvider({ id: "anthropic", name: "Anthropic", - api: "anthropic", baseUrl: "https://api.anthropic.com/v1", - auth: { - local: envLocalAuth("anthropic-api-key", "Anthropic API key", ["ANTHROPIC_API_KEY"]), - oauth: - options.oauth === "node" - ? lazyOAuthProvider({ - id: "anthropic", - name: "Anthropic (Claude Pro/Max)", - usesCallbackServer: true, - load: () => import("../oauth/anthropic-node.ts").then((m) => m.anthropicOAuthProvider), - }) - : options.oauth === "web" - ? lazyOAuthProvider({ - id: "anthropic", - name: "Anthropic (Claude Pro/Max)", - load: () => import("../oauth/anthropic-web.ts").then((m) => m.anthropicOAuthProvider), - }) - : undefined, - }, - getModels: staticModels(ANTHROPIC_MODELS), - stream: anthropicApi().stream, - streamSimple: anthropicApi().streamSimple, - }; + auth: [ + ...(options.oauth === "node" + ? [lazyOAuthMethod({ + id: "oauth", + name: "Anthropic (Claude Pro/Max)", + load: () => import("../utils/oauth/anthropic.ts").then((m) => m.anthropicOAuthMethod), + })] + : []), + envApiKeyMethod({ id: "api-key", name: "Anthropic API key", env: ["ANTHROPIC_API_KEY"] }), + ], + models: ANTHROPIC_MODELS, + api: anthropicMessagesApi(), + }); } ``` -Recommended defaults: +- Individual factories default to `oauth: false`. +- `builtinModels({ oauth: "node" })` for pi CLI/coding-agent. +- `"web"` is reserved; web flows (sitegeist-style: Web Crypto PKCE, auth tab, extension tab APIs watching the localhost redirect, fetch token exchange, device-code polling for Copilot) are a follow-up. Until implemented, passing `"web"` throws at login time with a clear message. -- individual provider factories default to `oauth: false` unless we intentionally want Node defaults -- `providers/all` for pi CLI/coding-agent calls providers with `oauth: "node"` -- browser users call providers with `oauth: "web"` -- users that only want API-key/env auth leave OAuth disabled - -Sitegeist demonstrates that browser-compatible OAuth is practical for Anthropic, OpenAI Codex, GitHub Copilot, and Gemini CLI. The browser implementations use Web Crypto, auth tabs, localhost redirect URL watching through extension tab APIs, `fetch` for token exchange, CORS permissions/proxies where needed, and device-code polling for Copilot. - -So the target is not “OAuth is Node-only”. The target is: provider factories attach the right lazy OAuth module for the runtime the caller asked for. - -Use a lazy wrapper so provider definitions can advertise OAuth without importing the actual implementation: +`lazyOAuthMethod()` wraps a dynamically imported `OAuthAuthMethod` so provider definitions can advertise OAuth without importing the implementation: ```ts -export function lazyOAuthProvider(input: { +export function lazyOAuthMethod(input: { id: string; name: string; - usesCallbackServer?: boolean; - load: () => Promise; -}): OAuthProvider { - return { - id: input.id, - name: input.name, - usesCallbackServer: input.usesCallbackServer, - async login(callbacks) { - return (await input.load()).login(callbacks); - }, - async resolve(credentials) { - return (await input.load()).resolve(credentials); - }, - }; -} + load: () => Promise; +}): OAuthAuthMethod; ``` -## Auth resolution policy - -`pi-ai` can ship a default resolver using injected context/store. Applications can replace it. - -Recommended default order, low to high precedence: - -```txt -provider local auth defaults --> CredentialStore local/OAuth credential --> explicit request auth -``` - -coding-agent later adds models.json and CLI policy: - -```txt -provider local auth defaults --> AuthStorage credential --> models.json auth sidecar --> CLI/runtime explicit request auth -``` - -Auth values merge: - -- later `apiKey` wins -- later `baseUrl` wins -- headers shallow-merge; later wins per header - -Cloudflare requires merge, not early return. It may need env account/gateway + stored key, or stored metadata + env token. +The existing flows in `src/utils/oauth/` (anthropic, openai-codex, github-copilot) are adapted to `OAuthAuthMethod` with the new callbacks, staying Node-targeted and lazy-loaded. ## Provider wrappers and models.json -`models.json` is naturally a provider wrapper layer. - -It should not mutate a provider in place. It should wrap: +`models.json` is a provider wrapper layer. It does not mutate providers in place: ```ts function withProviderOverrides(base: Provider, overrides: ProviderOverrides): Provider { @@ -652,17 +539,35 @@ function withProviderOverrides(base: Provider, overrides: ProviderOverrides): Pr } ``` -This composes with dynamic providers because `getModels()` delegates to the base provider source. +This composes with dynamic providers because `getModels()` delegates to the base source. -Request-auth config from models.json remains app-owned sidecar state. It is not stored in `Provider` unless it is true provider metadata such as base URL or headers. +Request-auth config from models.json (`$ENV`, `!command`, inline keys) remains app-owned sidecar state, surfaced either as explicit request auth or as a custom `ApiKeyAuthMethod` the app prepends to the wrapped provider's auth list. -## Custom providers from models.json +## Custom providers: createProvider() -A models.json custom provider must become a concrete `Provider` object. +One helper builds providers from parts; it handles both single-API and mixed-API providers: -### Single API custom provider +```ts +export function createProvider(input: { + id: string; + name?: string; // default: id + baseUrl?: string; + headers?: Record; + auth?: readonly AuthMethod[]; // default: [] + models: + | readonly Model[] + | ((options?: { forceRefresh?: boolean }) => Promise[]>); + /** Single implementation, or map keyed by model.api for mixed-API providers. */ + api: ProviderStreams | Record; +}): Provider; +``` -If all models use one known API: +- Single `api`: all models stream through it. +- Map `api`: `stream()`/`streamSimple()` dispatch on `model.api`; unknown api produces a stream error. + +Mixed-API custom providers must be supported (opencode Go/Zen-style providers expose models backed by different APIs under one provider id). + +Built-in provider factories use `createProvider()` internally. models.json custom providers map onto it directly: ```json { @@ -676,185 +581,181 @@ If all models use one known API: } ``` -coding-agent/pi-ai helper can build: +## Compat entrypoint + +`@earendil-works/pi-ai/compat` preserves the old global API surface until the coding-agent migration deletes it. New code never imports it. + +Old semantics being preserved: global `stream()` dispatched purely on `model.api` via the api-registry, with env API key injection. The compat module reproduces this: + +- Lazily creates a default `Models` singleton from `builtinModels({ oauth: "node" })` on first use. +- `stream/complete/streamSimple/completeSimple(model, ctx, opts)`: look up `getProvider(model.provider)`; if found, route through the singleton (auth resolution included). If not found (custom models.json/extension models), fall back to api-dispatch through a hidden `createProvider()` map containing all builtin API implementations plus anything registered via compat `registerApiProvider()`. +- `registerApiProvider()/unregisterApiProviders()` feed that fallback dispatch map. `api-registry.ts` dies as a real mechanism. +- Sync `getModel/getModels/getProviders` become deprecated aliases of `getBuiltinModel/getBuiltinModels/getBuiltinProviders` (they were always pure generated-catalog reads — verified: nothing ever mutated the old `modelRegistry`). +- Re-exports `setBedrockProviderModule` from the bedrock lazy wrapper. +- `getEnvApiKey`/`env-api-keys.ts` stays available from compat only; provider auth methods own env lookup in the new design. + +coding-agent switches imports of these symbols from `@earendil-works/pi-ai` to `@earendil-works/pi-ai/compat` (import-path-only change) and is otherwise untouched until the ModelManager migration. + +## Builtin static helpers + +Typed, sync, generated-catalog-only helpers live with the catalogs (exported from `providers/all`): ```ts -createApiBackedProvider({ - id: "my-openai-proxy", - name: "my-openai-proxy", - api: "openai-completions", - baseUrl: "https://proxy.example/v1", - auth: {}, - models, - apiImplementation: openAICompatibleApi(), -}); +getBuiltinModel(provider, id) // sync, typed overloads from generated catalog +getBuiltinModels(provider) // sync +getBuiltinProviders() // sync ``` -This helper lives outside `Models`; it is provider construction sugar. +Runtime lookup is always the async instance API: `await models.getModel(...)`. -### Mixed API custom provider - -Custom providers with mixed APIs must be supported. Existing providers such as opencode-go/zen can expose models backed by different APIs under one provider id. In this design that means the provider dispatches internally. - -```ts -createDispatchProvider({ - id, - models, - apis: { - "openai-completions": openAICompatibleApi(), - "anthropic": anthropicApi(), - }, -}); -``` - -The returned provider still exposes only: - -```ts -stream(model, context, options) -streamSimple(model, context, options) -``` - -Internally it switches on `model.api` and calls the right lazy API implementation. - -This preserves the rule that `Models` has no stream registry while supporting required mixed-API providers. +Generated catalogs are split per provider (`providers/.models.ts`) by updating `packages/ai/scripts/generate-models.ts`. If the generator change turns out too large for this pass, splitting may be deferred; `providers/all` and provider factories may temporarily import the monolithic `models.generated.ts`, relying on `sideEffects: false` for pruning. ## Tree-shaking and lazy imports Rules: 1. Main `@earendil-works/pi-ai` import is core-only. -2. Provider modules import metadata, model catalog, auth helpers, and lazy API wrappers only. +2. Provider modules import their catalog, auth helpers, and lazy API wrappers only. 3. Lazy API wrappers dynamically import real API implementations. 4. Real API implementations import SDK dependencies. -5. OAuth providers are selected by provider factory option (`oauth: "node" | "web" | false`) and lazy-loaded; provider metadata must not eagerly import Node-only OAuth code. -6. `providers/all` is explicit and allowed to import all provider metadata, but still no eager SDK imports. -7. Provider modules are side-effect-free; importing a provider does not register it globally. -8. `package.json` should set `sideEffects: false` if all entrypoints are side-effect-free. +5. OAuth implementations are selected by factory option (`oauth: "node" | "web" | false`) and lazy-loaded; provider metadata never eagerly imports Node-only OAuth code. +6. `providers/all` may import all provider metadata, but no eager SDK imports. +7. Provider modules are side-effect-free; importing a provider does not register anything globally. +8. `package.json` sets `sideEffects: false`. -Example exports: +Exports map sketch: ```json { "exports": { ".": "./dist/index.js", + "./compat": "./dist/compat.js", + "./providers/all": "./dist/providers/all.js", "./providers/openai": "./dist/providers/openai.js", "./providers/anthropic": "./dist/providers/anthropic.js", - "./providers/openrouter": "./dist/providers/openrouter.js", - "./providers/all": "./dist/providers/all.js", - "./ai/openai-compatible": "./dist/ai/openai-compatible-lazy.js" + "./providers/*": "./dist/providers/*.js", + "./api/*": "./dist/api/*.js" } } ``` -To avoid metadata bloat for minimal users, generated model catalogs should be split per provider. Until then, any provider module importing the monolithic generated catalog can pull more metadata than necessary. - -## Static typed helpers - -The old global sync helpers are incompatible with dynamic providers: - -```ts -getModel(...) -getModels(...) -getProviders(...) -``` - -If they mean runtime lookup, they must be async. If they remain sync and read only built-ins, they are misleading. - -Target: - -- remove old global runtime helpers, or make them async and default-instance backed only in a compatibility entrypoint -- add explicit static catalog helpers if type-safe built-in lookup is still desired - -```ts -getBuiltinModel(provider, id) // sync, generated catalog only -getBuiltinModels(provider) // sync, generated catalog only -getBuiltinProviders() // sync, generated catalog only -``` - -Runtime lookup is always: - -```ts -await models.getModel(provider, id) -await models.getModels(provider) -``` +Browser smoke check (`scripts/check-browser-smoke.mjs`) must keep passing: bundling the core entrypoint (and any non-node provider entrypoint) must not pull `node:http`/`node:crypto`. ## AgentHarness integration `AgentHarness` receives a `Models` instance. -Rules: +- `AgentHarnessOptions.models` is required. +- The harness does not snapshot `Models` into turn state. +- Request path calls `this.models.streamSimple(model, context, options)`; same for compaction/branch-summarization paths. +- Request path never calls async `models.getModel()` to canonicalize; if model metadata needs refresh, the app updates the selected model before starting a turn. +- Harness tests build `createModels()` and install the faux provider (`fauxProvider()` factory from `providers/faux`). -- `AgentHarnessOptions.models` is required -- harness does not snapshot `Models` into turn state -- request path calls `models.streamSimple(model, context, options)` or equivalent -- request path does not call async `models.getModel()` to canonicalize -- if model metadata needs refresh, app updates the selected model before starting a turn +## coding-agent next phase (not this pass) -## coding-agent next phase - -coding-agent should build providers in layers: +coding-agent builds providers in layers and binds them per session: ```txt -built-in providers --> models.json provider wrappers +built-in providers (builtinModels) +-> models.json provider wrappers / custom providers (createProvider) -> extension provider wrappers/additions ``` -Then: - ```ts sessionModels.clearProviders(); for (const provider of layeredProviders) sessionModels.setProvider(provider); -sessionModels.setAuthResolver(codingAgentResolver); ``` -coding-agent owns: +coding-agent owns: AuthStorage-backed `CredentialStore`, models.json auth sidecar (`$ENV`, `!command`), command execution policy, provider status labels (from `AuthResolution.source`), login/logout UI (driving `method.login()` with `prompt()/notify()`), extension lifecycle, provider-management slash commands. -- AuthStorage local/OAuth files -- models.json auth sidecar -- `$ENV` and `!command` -- command execution policy -- provider status labels -- login/logout UI -- extension lifecycle -- provider-management slash commands +Until then, the only coding-agent changes in this pass are: -## Migration TODOs +- construct a `Models` instance for `AgentHarness` (builtins + legacy api-dispatch fallback bridging `ModelRegistry` custom providers) +- switch old-global imports to `@earendil-works/pi-ai/compat` +- adapt the login dialog to `prompt()/notify()` callbacks (thin UI adapter; replaces the `usesCallbackServer` special-casing) -1. Restore/remove half-implemented old auth/stream-registry changes before starting this design. -2. Redesign `packages/ai/src/models.ts` around provider-owned streams. -3. Remove `StreamFunctions` registry from `Models` public API. -4. Introduce `Provider` with required `auth`, async `getModels()`, `stream()`, and `streamSimple()`. -5. Add lazy API wrappers under `packages/ai/src/ai/`. -6. Move real API implementations under `packages/ai/src/ai/` or adapt existing stream files into that layout. -7. Add concrete provider factories under `packages/ai/src/providers/`. -8. Add `providers/all` explicit aggregate. -9. Add `lazyOAuthProvider()`, `OAuthTarget`, and provider factory options such as `anthropicProvider({ oauth: "node" | "web" | false })`. -10. Convert built-in OAuth attachment to lazy target-specific wrappers. -11. Split generated model catalogs per provider, or mark as follow-up if too large. -12. Replace old global `defaultModels()`/global helpers with explicit instance usage or compatibility entrypoint. -13. Add custom provider helpers: - - `createApiBackedProvider()` - - `createDispatchProvider()` for required mixed-API providers -14. Update `AgentHarness` to use provider-owned `models.streamSimple()` without stream registry lookups. -15. Keep coding-agent compatibility only as needed until the coding-agent `ModelManager` migration. -16. Update tests to construct explicit `Models` instances and install only needed providers/faux providers. +## Implementation TODOs + +Check items off as they land. Keep this list current; it is the working state for resumed sessions. + +### Phase 1 — core types/runtime + +- [ ] Rename `types.ts` `Provider` alias to `ProviderId`; fix call sites. +- [ ] Add `ApiOptionsMap` and `ApiStreamOptions` to `types.ts` (type-only imports). +- [ ] New `models.ts`: `Provider` interface, `AuthMethod` union (`ApiKeyAuthMethod`/`OAuthAuthMethod`), `LocalCredential`/`OAuthCredential`/`Credential`, `CredentialStore` (+ in-memory default), `AuthResolution`, `ProviderAuthContext`, `ModelAuth`, `ModelsError` + codes. +- [ ] `Models`/`MutableModels`/`createModels({ credentials? })` with provider map, async `getModel(s)` (per-provider failure isolation), `getAuth` (two-pass fixed policy, persists refreshed credentials), `stream/complete/streamSimple/completeSimple` with per-field auth merge. +- [ ] Keep metadata helpers: `calculateCost`, `getSupportedThinkingLevels`, `clampThinkingLevel`, `modelsAreEqual`. + +### Phase 2 — `src/api/` + +- [ ] Move stream implementations from `src/providers/` to `src/api/`, renamed by API id (`anthropic.ts` -> `api/anthropic-messages.ts`, etc.). +- [ ] Normalize each implementation module to export exactly `stream` and `streamSimple`. +- [ ] Move shared helpers (`openai-responses-shared`, `google-shared`, `transform-messages`, `openai-prompt-cache`, `github-copilot-headers`) to `src/api/`. +- [ ] Extract `lazyStream()`/`lazyApi()` into `src/api/lazy.ts`. +- [ ] Add `*-lazy.ts` wrappers per API; bedrock keeps node-only import trick and `setBedrockProviderModule()`. +- [ ] Delete `providers/register-builtins.ts`. + +### Phase 3 — provider factories + catalogs + +- [ ] Auth helpers in `src/auth/`: `envApiKeyMethod()`, `lazyOAuthMethod()`, `OAuthTarget`, `AuthLoginCallbacks`/`AuthPrompt`/`AuthEvent`. +- [ ] `createProvider()` (single + mixed `api` map, dispatch on `model.api`). +- [ ] Per-provider factories under `src/providers/` for all built-in catalog providers, `oauth` factory options where applicable. +- [ ] `providers/all.ts`: `builtinModels({ oauth? })`, `getBuiltinModel/getBuiltinModels/getBuiltinProviders`. +- [ ] Faux provider factory (`providers/faux.ts`) for tests. +- [ ] Split generated catalogs per provider via `scripts/generate-models.ts` (`providers/.models.ts`) — or record explicitly that this is deferred. + +### Phase 4 — OAuth adaptation + +- [ ] Adapt `utils/oauth/anthropic.ts`, `openai-codex.ts`, `github-copilot.ts` to `OAuthAuthMethod` + `prompt()/notify()`. +- [ ] Remove `usesCallbackServer`; callback-server flows race a `manual_code` prompt instead. +- [ ] `oauth: "web"` reserved: throws at login with clear message. + +### Phase 5 — packaging + +- [ ] `index.ts` core-only (no catalogs, no provider factories, no OAuth, no compat). +- [ ] `compat.ts`: default builtin singleton, `stream/complete/streamSimple/completeSimple` with api-dispatch fallback, `registerApiProvider`/`unregisterApiProviders`, deprecated `getModel/getModels/getProviders` aliases, `setBedrockProviderModule` re-export, `getEnvApiKey`. +- [ ] Subpath exports map; `sideEffects: false`. +- [ ] Browser smoke + shrinkwrap checks green. + +### Phase 6 — AgentHarness + +- [ ] `AgentHarnessOptions.models` required; harness stream path uses `models.streamSimple()`. +- [ ] Compaction/branch-summarization paths use the harness `Models` instance. +- [ ] Harness tests use `createModels()` + faux provider. + +### Phase 7 — coding-agent bridge (minimal) + +- [ ] Construct `Models` for the harness (builtins + legacy api-dispatch fallback for ModelRegistry custom providers). +- [ ] Switch old-global imports to `@earendil-works/pi-ai/compat`. +- [ ] Login dialog adapter for `prompt()/notify()` callbacks. + +### Phase 8 — wrap-up + +- [ ] Update/add tests; run affected suites (`./test.sh` or per-package vitest). +- [ ] `packages/ai/CHANGELOG.md`: `### Breaking Changes` entry with a migration guide (old global `stream/streamSimple/complete/completeSimple`, `getModel/getModels/getProviders`, `registerApiProvider`, `Provider` -> `ProviderId` rename, OAuth callback changes; old API -> `createModels()`/provider factories or `/compat` as interim). +- [ ] `packages/coding-agent/CHANGELOG.md`: `### Breaking Changes` entry with a migration guide for extension authors who work directly with pi-ai through coding-agent (e.g. custom providers via `registerApiProvider`, model access, login/auth hooks): what changed, what to import now, compat timeline. +- [ ] `packages/agent/CHANGELOG.md`: `### Breaking Changes` entry for required `AgentHarnessOptions.models`. +- [ ] `npm run check` clean. + +### Deferred / follow-ups + +- [ ] Web OAuth implementations (sitegeist-style) behind `oauth: "web"`. +- [ ] coding-agent `ModelRegistry` -> session `ModelManager` migration; delete `/compat`. +- [ ] Images API registry redesign (untouched in this pass). ## Error behavior -`undefined` means not found or not configured. -Real failures reject or become stream errors. - -Recommended error codes: +`undefined` means not found or not configured. Real failures reject or become stream errors. ```ts export type ModelsErrorCode = - | "model_source" - | "model_validation" - | "provider" - | "stream" - | "auth" - | "oauth"; + | "model_source" // provider getModels() failed + | "model_validation" // model object invalid + | "provider" // unknown provider, dispatch failure + | "stream" // stream setup failure + | "auth" // auth resolution failure + | "oauth"; // oauth login/refresh failure ``` -`Models.stream()` should produce stream errors for async setup failures. `getModels()` should isolate provider source failures when listing all providers if possible, so one dynamic provider failure does not prevent listing other providers. +- `Models.stream()` produces stream errors (error event + error result) for async setup failures; it does not throw after returning the stream. +- `Models.getModels()` with no provider filter isolates per-provider source failures so one dynamic provider failure does not prevent listing others. From 7498b216d9ed74bc083e52e3cfc0f7a212c06f8c Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 10 Jun 2026 11:00:48 +0200 Subject: [PATCH 003/122] fix: bump shell-quote to 1.8.4 in lockfile (GHSA-w7jw-789q-3m8p) --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 65e743a5..1f0ebbc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4731,9 +4731,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", + "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", "license": "MIT", "engines": { "node": ">= 0.4" From f63095cfff0d4df5239eec8a789d3ad91078b8d5 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 10 Jun 2026 18:49:10 +0200 Subject: [PATCH 004/122] feat(ai): add Models runtime with provider-owned auth (phase 1) New Models/MutableModels/createModels collection: provider map, async model listing (best-effort aggregation), getAuth decision tree with double-checked locked OAuth refresh, stream/complete with per-field auth merge over lazyStream. Auth substrate: ProviderAuth { apiKey?, oauth? }, one type-tagged credential per provider, CredentialStore (read/modify/delete; modify is the only write path, serialized RMW), OAuthAuth login/refresh/toAuth split, prompt()/notify() login callbacks, browser-safe default AuthContext. types.ts: Provider alias renamed to ProviderId; ApiOptionsMap and ApiStreamOptions for typed per-API stream options; hasApi() runtime narrowing guard. --- packages/agent/docs/models.md | 296 ++++++++++++----- packages/ai/src/api/lazy.ts | 56 ++++ packages/ai/src/auth/context.ts | 45 +++ packages/ai/src/auth/credential-store.ts | 47 +++ packages/ai/src/auth/types.ts | 179 ++++++++++ packages/ai/src/index.ts | 4 + packages/ai/src/models.ts | 366 ++++++++++++++++++++- packages/ai/src/types.ts | 40 ++- packages/ai/test/models-runtime.test.ts | 399 +++++++++++++++++++++++ packages/ai/test/scratch.ts | 87 +++++ 10 files changed, 1427 insertions(+), 92 deletions(-) create mode 100644 packages/ai/src/api/lazy.ts create mode 100644 packages/ai/src/auth/context.ts create mode 100644 packages/ai/src/auth/credential-store.ts create mode 100644 packages/ai/src/auth/types.ts create mode 100644 packages/ai/test/models-runtime.test.ts create mode 100644 packages/ai/test/scratch.ts diff --git a/packages/agent/docs/models.md b/packages/agent/docs/models.md index 0a31b9d7..89b89629 100644 --- a/packages/agent/docs/models.md +++ b/packages/agent/docs/models.md @@ -118,17 +118,31 @@ const models = builtinModels({ oauth: "node" }); `Models` is a provider collection plus auth application and stream convenience. No stream registry, no auth resolver strategy object. ```ts -export function createModels(options?: { credentials?: CredentialStore }): MutableModels; +export function createModels(options?: { + /** App-owned credential storage. Default: in-memory store. */ + credentials?: CredentialStore; + /** Environment access for auth resolution (env vars, file existence). Default: process.env/node:fs backed; injectable for tests and non-Node hosts. */ + authContext?: AuthContext; +}): MutableModels; export interface Models { getProviders(): readonly Provider[]; getProvider(id: string): Provider | undefined; + /** Best-effort aggregation: provider source failures yield the models that did list. */ + getModels(options?: { forceRefresh?: boolean }): Promise[]>; getModels(provider?: string, options?: { forceRefresh?: boolean }): Promise[]>; + /** Dynamic lists are honestly Model; narrow with the hasApi() guard. */ getModel(provider: string, id: string, options?: { forceRefresh?: boolean }): Promise | undefined>; - /** Resolve request auth for a model. Includes source label for status UI. */ - getAuth(model: Model): Promise; + /** + * Resolve request auth for a model. Includes source label for status UI. + * Resolves undefined when the provider is unknown or unconfigured. Rejects + * with ModelsError ("oauth" on refresh failure, "auth" on api-key/store + * failure); status/availability UIs catch rejections and render + * "needs re-login" instead of treating them as unconfigured. + */ + getAuth(model: Model): Promise; stream( model: Model, @@ -169,26 +183,30 @@ If an app needs different auth policy, it wraps providers (wrap auth methods or A provider is the concrete runtime unit. It owns id/name/base metadata, auth methods, model listing, and stream behavior. +`Provider` is generic over the APIs its models use. Concrete factories declare what they emit (`openaiProvider(): Provider<"openai-responses" | "openai-completions">`), giving typed model lists to direct factory users. A `Models` collection holds providers as `Provider`. + ```ts -export interface Provider { +export interface Provider { readonly id: string; readonly name: string; readonly baseUrl?: string; readonly headers?: Record; - /** Required. Empty array for no-auth providers. */ - readonly auth: readonly AuthMethod[]; + /** + * Required: at least one of apiKey/oauth. Even ambient-credential providers + * (env vars, AWS profiles, ADC) and keyless local servers provide apiKey + * auth whose resolve() reports whether the provider is configured. + * getAuth() returning undefined = not configured. + */ + readonly auth: ProviderAuth; - getModels(options?: { forceRefresh?: boolean }): Promise[]>; + /** Sync return suits static catalogs; Models always exposes a Promise. */ + getModels(options?: { forceRefresh?: boolean }): Promise[]> | readonly Model[]; - stream( - model: Model, - context: Context, - options?: ApiStreamOptions, - ): AssistantMessageEventStream; + stream(model: Model, context: Context, options?: ApiStreamOptions): AssistantMessageEventStream; - streamSimple(model: Model, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream; + streamSimple(model: Model, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream; } ``` @@ -221,6 +239,29 @@ export type ApiStreamOptions = TApi extends keyof ApiOptionsMa Custom api strings fall back to the generic shape. +### Typed model narrowing + +Runtime model lists are dynamic, so `models.getModel()`/`getModels()` honestly return `Model`. Typing improves at three points: + +1. **`hasApi()` type guard** — runtime-checked narrowing for dynamic lookups (no blind casts): + + ```ts + export function hasApi(model: Model, api: TApi): model is Model; + + const model = await models.getModel("anthropic", "claude-opus-4-7"); + if (model && hasApi(model, "anthropic-messages")) { + // model: Model<"anthropic-messages">, stream options fully typed + } + ``` + +2. **`getBuiltinModel()`** — sync, generated-catalog lookup with typed overloads: `(provider, id) -> Model`. The path for hardcoded known models. + +3. **`Provider` factories** — typed model lists when using a provider directly, without a `Models` collection. + +Deliberately not done: tying `models.getModel(provider, ...)` to typed provider/model ids would require statically knowing which providers are installed in a mutable runtime collection. The harness path (`streamSimple` + `SimpleStreamOptions`) is API-agnostic and unaffected. + +For comparison: Vercel AI SDK attaches the implementation to the model object, which dissolves dispatch typing but makes models non-serializable (no sessions/RPC/catalogs as plain data), and its `providerOptions` bag is `Record` checked only by `satisfies` convention. Plain-data models + provider-owned behavior keeps stronger typing where it matters. + ### Name collision `types.ts` currently exports `type Provider = KnownProvider | string` (a provider id). Rename that alias to `ProviderId` and fix call sites. The `Provider` interface above takes the name. @@ -316,7 +357,7 @@ export function openrouterProvider(): Provider { id: "openrouter", name: "OpenRouter", baseUrl: "https://openrouter.ai/api/v1", - auth: [envApiKeyMethod({ id: "api-key", name: "OpenRouter API key", env: ["OPENROUTER_API_KEY"] })], + auth: { apiKey: envApiKeyAuth("OpenRouter API key", ["OPENROUTER_API_KEY"]) }, models: OPENROUTER_MODELS, api: openAICompletionsApi(), }); @@ -339,108 +380,181 @@ export interface ModelAuth { If a value cannot be expressed as `apiKey`, `headers`, or `baseUrl`, it is provider config, not auth (Vertex project/location, Bedrock region/profile, Azure apiVersion are provider factory options). -### Auth methods +### Provider auth -`Provider.auth` is a list of uniform auth methods. The `kind` discriminant keeps the UI's oauth-vs-api-key split and types the credential: +`Provider.auth` has exactly two slots; real providers have at most one api-key path and at most one OAuth path, and the slot names carry the UI's oauth-vs-api-key split without a `kind` discriminant or method ids: ```ts -export interface ApiKeyAuthMethod { - kind: "api-key"; - id: string; // unique within provider, e.g. "api-key" +export interface ProviderAuth { + apiKey?: ApiKeyAuth; // stored key/metadata + ambient env/files/ADC/IAM + oauth?: OAuthAuth; // login flow + refresh +} + +export interface ApiKeyAuth { name: string; // "Anthropic API key" /** Interactive setup (prompt for key/metadata). Absent = ambient-only (env, ADC, IAM). */ - login?(callbacks: AuthLoginCallbacks): Promise; + login?(callbacks: AuthLoginCallbacks): Promise; + /** + * Resolve auth from the stored credential and/or ambient sources, merging + * per field (credential.key ?? env("..."), metadata.accountId ?? env("...")). + * undefined = not configured. + */ resolve(input: { model: Model; - ctx: ProviderAuthContext; - credential?: LocalCredential; - }): Promise; + ctx: AuthContext; + credential?: ApiKeyCredential; + }): Promise; } -export interface OAuthAuthMethod { - kind: "oauth"; - id: string; // e.g. "oauth" +export interface OAuthAuth { name: string; // "Anthropic (Claude Pro/Max)" login(callbacks: AuthLoginCallbacks): Promise; - resolve(input: { - model: Model; - ctx: ProviderAuthContext; - credential?: OAuthCredential; - }): Promise; + /** Exchange the refresh token. Network call; throws on failure (invalid_grant etc.). Runs under the store lock. */ + refresh(credential: OAuthCredential): Promise; + + /** Side-effect-free derivation of request auth from a valid credential. Covers Copilot-style per-credential baseUrl. Async so lazy wrappers can load the implementation. */ + toAuth(credential: OAuthCredential): Promise; } -export type AuthMethod = ApiKeyAuthMethod | OAuthAuthMethod; - -export interface AuthResolution { +export interface AuthResult { auth: ModelAuth; /** Human-readable label for status UI: "ANTHROPIC_API_KEY", "OAuth", "~/.aws/credentials". */ source?: string; - /** Present when the method refreshed/updated the credential; Models persists it via the store. */ - credential?: Credential; } -export interface ProviderAuthContext { +export interface AuthContext { env(name: string): Promise; fileExists(path: string): Promise; // supports leading ~ } ``` +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. + 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. ### Credentials +One credential per provider, type-tagged — exactly the shape of today's auth.json (`type: "api_key" | "oauth"` per provider id): + ```ts -export interface LocalCredential { - type: "local"; +export interface ApiKeyCredential { + type: "api-key"; key?: string; metadata?: Record; // e.g. Cloudflare accountId/gatewayId } export interface OAuthCredential extends OAuthCredentials { - type: "oauth"; + type: "oauth"; // access, refresh, expires from OAuthCredentials } -export type Credential = LocalCredential | OAuthCredential; +export type Credential = ApiKeyCredential | OAuthCredential; ``` -`LocalCredential.metadata` exists for providers like Cloudflare that store non-key values (account id, gateway id) alongside or instead of a key. The method's `resolve()` merges per field: `credential.key ?? env("CLOUDFLARE_API_TOKEN")`, `credential.metadata?.accountId ?? env("CLOUDFLARE_ACCOUNT_ID")`, etc. +`ApiKeyCredential.metadata` exists for providers like Cloudflare that store non-key values (account id, gateway id) alongside or instead of a key. `ApiKeyAuth.resolve()` merges per field: `credential.key ?? env("CLOUDFLARE_API_TOKEN")`, `credential.metadata?.accountId ?? env("CLOUDFLARE_ACCOUNT_ID")`, etc. ### Credential store -The app injects storage; `pi-ai` ships an in-memory default. +The app injects storage; `pi-ai` ships an in-memory default. Keyed by provider id, one credential per provider: ```ts export interface CredentialStore { - get(providerId: string, methodId: string): Promise; - set(providerId: string, methodId: string, credential: Credential): Promise; - delete(providerId: string, methodId: string): Promise; + /** Read the stored credential, possibly expired. Display/status use; request auth comes from Models.getAuth(). */ + read(providerId: string): Promise; + + /** + * Serialized write — the only write path. fn sees the current credential + * because correct writes (refresh, login-during-refresh) depend on it; + * return the new credential, or undefined to leave the entry unchanged. + * Mutual exclusion per provider id, cross-process too where the backing + * store supports it (file lock). Resolves with the post-write credential. + */ + modify( + providerId: string, + fn: (current: Credential | undefined) => Promise, + ): Promise; + + /** Remove (logout). Serialized against modify. */ + delete(providerId: string): Promise; } ``` -coding-agent later implements this over AuthStorage. Login/logout orchestration is app-owned: the app calls `method.login(callbacks)` and persists the returned credential itself. `Models` only *reads* the store during resolution, and *writes* refreshed credentials when `resolve()` returns an updated one (OAuth token refresh). +There is deliberately no `set`: an unserialized write path invites read-modify-write races (login-during-refresh clobbering a fresh credential, double token refresh). Call sites: + +```ts +await store.modify(pid, async () => credential); // login: store this +await store.read(pid); // status UI ("logged in via OAuth") +await store.delete(pid); // logout +// refresh RMW happens inside Models.getAuth +``` + +Error semantics: `read` resolves `undefined` for missing entries; methods reject only on storage failure, and `Models` wraps such rejections in `ModelsError` code `"auth"`. Best-effort stores that serve an in-memory view and record persistence errors internally (today's AuthStorage behavior) are valid implementations. ### Resolution policy (fixed) -`Models.getAuth(model)` resolves with a fixed policy. Precedence, highest first: +`Models.getAuth(model)` is a decision tree, not a loop. A stored credential owns the provider — ambient/env is consulted only when nothing is stored (AuthStorage parity: no silent env fallback after a failed refresh or for an unmatched credential type): -```txt -1. explicit request auth (stream options apiKey/headers) — merged per-field on top, in stream() -2. methods with a stored credential, in provider auth list order -3. methods resolving without a credential (ambient/env), in provider auth list order +```ts +const stored = await store.read(provider.id); +if (stored) { + if (stored.type === "oauth" && provider.auth.oauth) { + const oauth = provider.auth.oauth; + let credential = stored; + if (Date.now() >= credential.expires) { // optimistic check, lock-free + const post = await store.modify(provider.id, async (current) => { + if (current?.type !== "oauth") return undefined; // logged out meanwhile + return Date.now() >= current.expires // authoritative check, under lock + ? oauth.refresh(current) // throws -> ModelsError("oauth") + : undefined; // another process/request refreshed + }); + if (post?.type !== "oauth") return undefined; + credential = post; + } + return { auth: await oauth.toAuth(credential), source: "OAuth" }; + } + if (stored.type === "api-key" && provider.auth.apiKey) { + return provider.auth.apiKey.resolve({ model, ctx, credential: stored }); + } + return undefined; // stored credential without matching handler blocks ambient +} +return provider.auth.apiKey?.resolve({ model, ctx, credential: undefined }); // ambient ``` -Two-pass over `provider.auth`: +Properties: -- Pass 1: for each method with a credential in the store, call `resolve({ model, ctx, credential })`; first non-undefined resolution wins. -- Pass 2: for each method, call `resolve({ model, ctx })`; first non-undefined resolution wins. +- Double-checked locking, same as today's `refreshOAuthTokenWithLock`: valid tokens cost one `read` and zero locks; expired tokens lock, re-check under the lock, refresh once globally, persist before release. +- Explicit request auth (stream options `apiKey`/`headers`) is merged per-field on top in `stream()`, winning over everything. +- Refresh failure rejects with `ModelsError("oauth")`; the stored credential is untouched (preserved for retry). Request paths surface this as a stream error with the real cause ("run /login"); status/availability UIs catch the rejection and render "needs re-login" — documented contract on `getAuth`. -So an explicit login (stored credential) beats ambient env vars regardless of list order; list order breaks ties. Per-field merging *within* one method (stored key + env account id) happens inside that method's `resolve()`. +### Replacing AuthStorage -If a resolution carries an updated `credential`, `Models` persists it via the store before returning. +The end state for coding-agent: AuthStorage is deleted; its capabilities map onto a `CredentialStore` implementation plus composition. + +Today's `getApiKey` priority and its new home: + +| AuthStorage today | New design | +|---|---| +| runtime override (CLI `--api-key`) | `withRuntimeOverrides(store, overrides)` decorator: `read` returns the override as an `ApiKeyCredential`; never persisted | +| stored `api_key` (with `$ENV`/`!command` via `resolveConfigValue`) | stored `ApiKeyCredential`; config-value resolution happens at `read` in coding-agent's adapter/decorator (command execution stays app policy) | +| stored `oauth` + locked refresh, undefined on failure | `getAuth` decision tree above; failure rejects with cause instead of silently unconfiguring | +| env var (only when nothing stored) | ambient branch of `apiKey.resolve` | +| `fallbackResolver` (models.json custom providers) | gone — custom providers carry their own `auth.apiKey` | + +```txt +FileCredentialStore ports AuthStorage's lock backend: read = memory snapshot, + modify = withLockAsync(re-read, fn, merge-write), delete, + internal error recording (drainErrors equivalent) +└─ withConfigValues $ENV / !command at read + └─ withRuntimeOverrides --api-key + └─ createModels({ credentials: store }) + +login/logout UI provider.auth.{oauth,apiKey}.login(callbacks) + store.modify/delete +status UI store.read(pid) + getAuth try/catch ("needs /login" on rejection) +getOAuthProviders presence of provider.auth.oauth across registered providers +``` ### Login callbacks @@ -448,17 +562,20 @@ One interface serves api-key and OAuth login: ```ts export interface AuthLoginCallbacks { + /** Aborts the whole login flow. Per-prompt cancellation uses AuthPrompt.signal. */ signal?: AbortSignal; - prompt(prompt: AuthPrompt, options?: { signal?: AbortSignal }): Promise; + prompt(prompt: AuthPrompt): Promise; notify(event: AuthEvent): void; } -export type AuthPrompt = +/** `signal` lets the flow cancel a pending prompt when an out-of-band event resolves the step. */ +export type AuthPrompt = { signal?: AbortSignal } & ( | { type: "text"; message: string; placeholder?: string } | { type: "secret"; message: string; placeholder?: string } | { type: "select"; message: string; options: readonly { id: string; label: string; description?: string }[] } - | { type: "manual_code"; message: string; placeholder?: string }; + | { type: "manual_code"; message: string; placeholder?: string } +); export type AuthEvent = | { type: "auth_url"; url: string; instructions?: string } @@ -466,7 +583,7 @@ export type AuthEvent = | { type: "progress"; message: string }; ``` -`prompt()` returns the entered/selected string (`select` returns the option id). Flows race a `manual_code` prompt against a callback server by passing a per-prompt abort signal and aborting when the callback wins. +`prompt()` returns the entered/selected string (`select` returns the option id). Flows race a `manual_code` prompt against a callback server by setting `AuthPrompt.signal` and aborting the prompt when the callback wins. ### OAuth implementation target @@ -484,16 +601,16 @@ export function anthropicProvider(options: AnthropicProviderOptions = {}): Provi id: "anthropic", name: "Anthropic", baseUrl: "https://api.anthropic.com/v1", - auth: [ - ...(options.oauth === "node" - ? [lazyOAuthMethod({ - id: "oauth", - name: "Anthropic (Claude Pro/Max)", - load: () => import("../utils/oauth/anthropic.ts").then((m) => m.anthropicOAuthMethod), - })] - : []), - envApiKeyMethod({ id: "api-key", name: "Anthropic API key", env: ["ANTHROPIC_API_KEY"] }), - ], + auth: { + apiKey: envApiKeyAuth("Anthropic API key", ["ANTHROPIC_API_KEY"]), + oauth: + options.oauth === "node" + ? lazyOAuth({ + name: "Anthropic (Claude Pro/Max)", + load: () => import("../utils/oauth/anthropic.ts").then((m) => m.anthropicOAuth), + }) + : undefined, + }, models: ANTHROPIC_MODELS, api: anthropicMessagesApi(), }); @@ -504,17 +621,16 @@ export function anthropicProvider(options: AnthropicProviderOptions = {}): Provi - `builtinModels({ oauth: "node" })` for pi CLI/coding-agent. - `"web"` is reserved; web flows (sitegeist-style: Web Crypto PKCE, auth tab, extension tab APIs watching the localhost redirect, fetch token exchange, device-code polling for Copilot) are a follow-up. Until implemented, passing `"web"` throws at login time with a clear message. -`lazyOAuthMethod()` wraps a dynamically imported `OAuthAuthMethod` so provider definitions can advertise OAuth without importing the implementation: +`lazyOAuth()` wraps a dynamically imported `OAuthAuth` so provider definitions can advertise OAuth without importing the implementation (`toAuth` is async for exactly this reason): ```ts -export function lazyOAuthMethod(input: { - id: string; +export function lazyOAuth(input: { name: string; - load: () => Promise; -}): OAuthAuthMethod; + load: () => Promise; +}): OAuthAuth; ``` -The existing flows in `src/utils/oauth/` (anthropic, openai-codex, github-copilot) are adapted to `OAuthAuthMethod` with the new callbacks, staying Node-targeted and lazy-loaded. +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`. ## Provider wrappers and models.json @@ -541,7 +657,7 @@ function withProviderOverrides(base: Provider, overrides: ProviderOverrides): Pr This composes with dynamic providers because `getModels()` delegates to the base source. -Request-auth config from models.json (`$ENV`, `!command`, inline keys) remains app-owned sidecar state, surfaced either as explicit request auth or as a custom `ApiKeyAuthMethod` the app prepends to the wrapped provider's auth list. +Request-auth config from models.json (`$ENV`, `!command`, inline keys) remains app-owned sidecar state, surfaced either as explicit request auth or as a custom `ApiKeyAuth` the app sets on the wrapped provider's `auth.apiKey`. ## Custom providers: createProvider() @@ -553,7 +669,7 @@ export function createProvider(input: { name?: string; // default: id baseUrl?: string; headers?: Record; - auth?: readonly AuthMethod[]; // default: [] + auth: ProviderAuth; // required, at least one of apiKey/oauth (no "no-auth" providers) models: | readonly Model[] | ((options?: { forceRefresh?: boolean }) => Promise[]>); @@ -666,7 +782,7 @@ sessionModels.clearProviders(); for (const provider of layeredProviders) sessionModels.setProvider(provider); ``` -coding-agent owns: AuthStorage-backed `CredentialStore`, models.json auth sidecar (`$ENV`, `!command`), command execution policy, provider status labels (from `AuthResolution.source`), login/logout UI (driving `method.login()` with `prompt()/notify()`), extension lifecycle, provider-management slash commands. +coding-agent owns: `FileCredentialStore` + decorators replacing AuthStorage (see "Replacing AuthStorage"), models.json auth sidecar (`$ENV`, `!command`), command execution policy, provider status labels (from `AuthResult.source`), login/logout UI (driving `auth.{apiKey,oauth}.login()` with `prompt()/notify()`), extension lifecycle, provider-management slash commands. Until then, the only coding-agent changes in this pass are: @@ -680,11 +796,11 @@ Check items off as they land. Keep this list current; it is the working state fo ### Phase 1 — core types/runtime -- [ ] Rename `types.ts` `Provider` alias to `ProviderId`; fix call sites. -- [ ] Add `ApiOptionsMap` and `ApiStreamOptions` to `types.ts` (type-only imports). -- [ ] New `models.ts`: `Provider` interface, `AuthMethod` union (`ApiKeyAuthMethod`/`OAuthAuthMethod`), `LocalCredential`/`OAuthCredential`/`Credential`, `CredentialStore` (+ in-memory default), `AuthResolution`, `ProviderAuthContext`, `ModelAuth`, `ModelsError` + codes. -- [ ] `Models`/`MutableModels`/`createModels({ credentials? })` with provider map, async `getModel(s)` (per-provider failure isolation), `getAuth` (two-pass fixed policy, persists refreshed credentials), `stream/complete/streamSimple/completeSimple` with per-field auth merge. -- [ ] Keep metadata helpers: `calculateCost`, `getSupportedThinkingLevels`, `clampThinkingLevel`, `modelsAreEqual`. +- [x] Rename `types.ts` `Provider` alias to `ProviderId`; fix call sites. +- [x] Add `ApiOptionsMap` and `ApiStreamOptions` to `types.ts` (type-only imports). +- [x] New `models.ts`: `Provider` interface, `hasApi()` guard, `ModelsError` + codes. Auth types live in `src/auth/types.ts` (`ProviderAuth` = `{ apiKey?, oauth? }`, credentials, `CredentialStore` (`read`/`modify`/`delete`, one credential per provider), `AuthResult`, `AuthContext`, `ModelAuth`, login callbacks), in-memory store in `src/auth/credential-store.ts`, default context in `src/auth/context.ts` (browser-safe node:fs trick), `lazyStream()` in `src/api/lazy.ts`. +- [x] `Models`/`MutableModels`/`createModels({ credentials?, authContext? })` with provider map, async `getModel(s)` (per-provider failure isolation), `getAuth` (decision tree, double-checked locked refresh), `stream/complete/streamSimple/completeSimple` with per-field auth merge. Tests: `packages/ai/test/models-runtime.test.ts`. +- [x] Keep metadata helpers: `calculateCost`, `getSupportedThinkingLevels`, `clampThinkingLevel`, `modelsAreEqual`. ### Phase 2 — `src/api/` @@ -697,7 +813,7 @@ Check items off as they land. Keep this list current; it is the working state fo ### Phase 3 — provider factories + catalogs -- [ ] Auth helpers in `src/auth/`: `envApiKeyMethod()`, `lazyOAuthMethod()`, `OAuthTarget`, `AuthLoginCallbacks`/`AuthPrompt`/`AuthEvent`. +- [ ] Auth helpers in `src/auth/`: `envApiKeyAuth()`, `lazyOAuth()`, `OAuthTarget`. - [ ] `createProvider()` (single + mixed `api` map, dispatch on `model.api`). - [ ] Per-provider factories under `src/providers/` for all built-in catalog providers, `oauth` factory options where applicable. - [ ] `providers/all.ts`: `builtinModels({ oauth? })`, `getBuiltinModel/getBuiltinModels/getBuiltinProviders`. @@ -706,7 +822,7 @@ Check items off as they land. Keep this list current; it is the working state fo ### Phase 4 — OAuth adaptation -- [ ] Adapt `utils/oauth/anthropic.ts`, `openai-codex.ts`, `github-copilot.ts` to `OAuthAuthMethod` + `prompt()/notify()`. +- [ ] Adapt `utils/oauth/anthropic.ts`, `openai-codex.ts`, `github-copilot.ts` to `OAuthAuth` (`login`/`refresh`/`toAuth`) + `prompt()/notify()`; `modifyModels` baseUrl rewriting becomes `toAuth().baseUrl`. - [ ] Remove `usesCallbackServer`; callback-server flows race a `manual_code` prompt instead. - [ ] `oauth: "web"` reserved: throws at login with clear message. @@ -729,6 +845,8 @@ Check items off as they land. Keep this list current; it is the working state fo - [ ] Switch old-global imports to `@earendil-works/pi-ai/compat`. - [ ] Login dialog adapter for `prompt()/notify()` callbacks. +The full AuthStorage deletion (`FileCredentialStore` + decorators, see "Replacing AuthStorage") happens in the later ModelManager migration, not this pass. + ### Phase 8 — wrap-up - [ ] Update/add tests; run affected suites (`./test.sh` or per-package vitest). @@ -758,4 +876,6 @@ export type ModelsErrorCode = ``` - `Models.stream()` produces stream errors (error event + error result) for async setup failures; it does not throw after returning the stream. -- `Models.getModels()` with no provider filter isolates per-provider source failures so one dynamic provider failure does not prevent listing others. +- `Models.getModels()` is best-effort aggregation in all forms: provider source failures yield the models that did list (empty for a single failing provider). Apps that need the concrete failure call `getProvider(id).getModels()` directly. +- Auth resolution and credential store failures reject loudly (`ModelsError` codes `auth`/`oauth`); silent fallback to a different auth path after a failure risks billing surprises. A stored credential always blocks ambient/env fallback, including after a failed refresh. +- Status/availability UIs catch `getAuth` rejections and render "needs re-login"; they do not treat rejection as "unconfigured". diff --git a/packages/ai/src/api/lazy.ts b/packages/ai/src/api/lazy.ts new file mode 100644 index 00000000..8cfd2ede --- /dev/null +++ b/packages/ai/src/api/lazy.ts @@ -0,0 +1,56 @@ +import type { Api, AssistantMessage, AssistantMessageEvent, Model } from "../types.ts"; +import { AssistantMessageEventStream } from "../utils/event-stream.ts"; + +function createSetupErrorMessage(model: Model, error: unknown): AssistantMessage { + return { + role: "assistant", + content: [], + api: model.api, + provider: model.provider, + model: model.id, + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "error", + errorMessage: error instanceof Error ? error.message : String(error), + timestamp: Date.now(), + }; +} + +function forwardStream(target: AssistantMessageEventStream, source: AsyncIterable): void { + (async () => { + for await (const event of source) { + target.push(event); + } + target.end(); + })(); +} + +/** + * Returns a stream synchronously while running async setup (auth resolution, + * lazy module loading) behind it. Setup failures terminate the stream with an + * error event. + */ +export function lazyStream( + model: Model, + setup: () => Promise>, +): AssistantMessageEventStream { + const outer = new AssistantMessageEventStream(); + + setup() + .then((inner) => { + forwardStream(outer, inner); + }) + .catch((error) => { + const message = createSetupErrorMessage(model, error); + outer.push({ type: "error", reason: "error", error: message }); + outer.end(message); + }); + + return outer; +} diff --git a/packages/ai/src/auth/context.ts b/packages/ai/src/auth/context.ts new file mode 100644 index 00000000..30e088bf --- /dev/null +++ b/packages/ai/src/auth/context.ts @@ -0,0 +1,45 @@ +import type { AuthContext } from "./types.ts"; + +interface NodeFsModule { + access(path: string): Promise; +} + +interface NodeOsModule { + homedir(): string; +} + +// Variable specifier so browser bundlers do not try to resolve node builtins. +const importNodeModule = (specifier: string): Promise => import(specifier); + +function getProcessEnv(): Record | undefined { + const proc = (globalThis as { process?: { env?: Record } }).process; + return proc?.env; +} + +/** + * Default auth context: env vars from `process.env` (undefined in browsers), + * file existence via node:fs (always false in browsers). + */ +export function defaultProviderAuthContext(): AuthContext { + return { + async env(name: string): Promise { + const value = getProcessEnv()?.[name]; + return typeof value === "string" && value.trim().length > 0 ? value : undefined; + }, + + async fileExists(path: string): Promise { + try { + const fs = (await importNodeModule("node:fs/promises")) as NodeFsModule; + let resolved = path; + if (resolved.startsWith("~")) { + const os = (await importNodeModule("node:os")) as NodeOsModule; + resolved = os.homedir() + resolved.slice(1); + } + await fs.access(resolved); + return true; + } catch { + return false; + } + }, + }; +} diff --git a/packages/ai/src/auth/credential-store.ts b/packages/ai/src/auth/credential-store.ts new file mode 100644 index 00000000..beeb9d85 --- /dev/null +++ b/packages/ai/src/auth/credential-store.ts @@ -0,0 +1,47 @@ +import type { Credential, CredentialStore } from "./types.ts"; + +/** + * Default in-memory credential store. Apps inject persistent stores. + * Keyed by `Provider.id`, one credential per provider; see `CredentialStore`. + * Writes are serialized per provider through a promise chain. + */ +export class InMemoryCredentialStore implements CredentialStore { + private credentials = new Map(); + private chains = new Map>(); + + /** Serialize tasks per provider id. */ + private enqueue(providerId: string, task: () => Promise): Promise { + const previous = this.chains.get(providerId) ?? Promise.resolve(); + const next = (async () => { + await previous.catch(() => {}); + return task(); + })(); + this.chains.set( + providerId, + next.catch(() => {}), + ); + return next; + } + + async read(providerId: string): Promise { + return this.credentials.get(providerId); + } + + modify( + providerId: string, + fn: (current: Credential | undefined) => Promise, + ): Promise { + return this.enqueue(providerId, async () => { + const current = this.credentials.get(providerId); + const next = await fn(current); + if (next !== undefined) this.credentials.set(providerId, next); + return next ?? current; + }); + } + + delete(providerId: string): Promise { + return this.enqueue(providerId, async () => { + this.credentials.delete(providerId); + }); + } +} diff --git a/packages/ai/src/auth/types.ts b/packages/ai/src/auth/types.ts new file mode 100644 index 00000000..b308b90d --- /dev/null +++ b/packages/ai/src/auth/types.ts @@ -0,0 +1,179 @@ +import type { Api, Model } from "../types.ts"; +import type { OAuthCredentials } from "../utils/oauth/types.ts"; + +/** + * Request auth for a single model request. If a value cannot be expressed as + * `apiKey`, `headers`, or `baseUrl`, it is provider config, not auth. + */ +export interface ModelAuth { + apiKey?: string; + headers?: Record; + baseUrl?: string; +} + +/** + * Stored api-key credential. `metadata` holds non-key values such as + * Cloudflare account/gateway ids. + */ +export interface ApiKeyCredential { + type: "api-key"; + key?: string; + metadata?: Record; +} + +/** Stored OAuth credential (`access`, `refresh`, `expires` from OAuthCredentials). */ +export interface OAuthCredential extends OAuthCredentials { + type: "oauth"; +} + +/** One type-tagged credential per provider — the shape of today's auth.json. */ +export type Credential = ApiKeyCredential | OAuthCredential; + +/** + * App-owned credential storage, keyed by `Provider.id`, one credential per + * provider. `modify` is the only write path, so every mutation is a + * serialized read-modify-write; `Models.getAuth()` runs OAuth refresh inside + * `modify` so concurrent requests cannot double-refresh a rotated token. The + * app persists a credential after login via + * `modify(provider.id, async () => credential)`. Login/logout orchestration + * is app-owned. + * + * Error semantics: `read` resolves `undefined` for missing entries. Methods + * reject only on storage failure; `Models` wraps such rejections in + * `ModelsError` with code "auth". Best-effort stores that serve an in-memory + * view and record persistence errors internally (like coding-agent's + * AuthStorage) are valid implementations. + */ +export interface CredentialStore { + /** + * Read the stored credential, possibly expired. Display/status use; + * resolved request auth comes from `Models.getAuth()`. + */ + read(providerId: string): Promise; + + /** + * Serialized write — the only write path. `fn` sees the current credential + * because correct writes (refresh, login-during-refresh) depend on it; + * return the new credential, or undefined to leave the entry unchanged. + * Mutual exclusion per provider id, cross-process too where the backing + * store supports it (e.g. a file lock). Resolves with the post-write + * credential. Rejections from `fn` propagate. + */ + modify( + providerId: string, + fn: (current: Credential | undefined) => Promise, + ): Promise; + + /** Remove a credential (logout). Implementations serialize this against `modify`. */ + delete(providerId: string): Promise; +} + +/** Environment access for auth resolution. Injectable for tests and browsers. */ +export interface AuthContext { + env(name: string): Promise; + /** Check whether a file exists. Supports a leading `~`. Always false in browsers. */ + fileExists(path: string): Promise; +} + +/** Result of resolving auth for a model. */ +export interface AuthResult { + auth: ModelAuth; + /** Human-readable label for status UI: "ANTHROPIC_API_KEY", "OAuth", "~/.aws/credentials". */ + source?: string; +} + +/** + * Prompt shown to the user during login. `signal` lets the flow cancel a + * pending prompt when an out-of-band event resolves the step, e.g. a + * `manual_code` prompt raced against a callback server, aborted when the + * callback wins. + */ +export type AuthPrompt = { signal?: AbortSignal } & ( + | { type: "text"; message: string; placeholder?: string } + | { type: "secret"; message: string; placeholder?: string } + | { type: "select"; message: string; options: readonly { id: string; label: string; description?: string }[] } + | { type: "manual_code"; message: string; placeholder?: string } +); + +export type AuthEvent = + | { type: "auth_url"; url: string; instructions?: string } + | { + type: "device_code"; + userCode: string; + verificationUri: string; + intervalSeconds?: number; + expiresInSeconds?: number; + } + | { type: "progress"; message: string }; + +/** + * Login interaction callbacks serving both api-key and OAuth flows. + * + * `prompt()` returns the entered/selected string (`select` returns the option + * id). Rejects on cancel/abort. `signal` aborts the whole login flow; + * per-prompt cancellation uses `AuthPrompt.signal`. + */ +export interface AuthLoginCallbacks { + signal?: AbortSignal; + + prompt(prompt: AuthPrompt): Promise; + notify(event: AuthEvent): void; +} + +/** + * Api-key auth: stored key/metadata plus ambient sources (env vars, AWS + * profiles, ADC files). Ambient-only providers omit `login`. + */ +export interface ApiKeyAuth { + /** Display name, e.g. "Anthropic API key". */ + name: string; + + /** Interactive setup (prompt for key/metadata). Absent = ambient-only. */ + login?(callbacks: AuthLoginCallbacks): Promise; + + /** + * Resolve auth from the stored credential and/or ambient sources, merging + * per field (`credential.key ?? env("...")`, `metadata.accountId ?? env("...")`). + * undefined = not configured. + */ + resolve(input: { + model: Model; + ctx: AuthContext; + credential?: ApiKeyCredential; + }): Promise; +} + +/** + * OAuth auth. The `refresh`/`toAuth` split lets `Models` own the locked + * refresh pattern: `refresh` produces a credential, `toAuth` derives request + * auth from whatever credential ends up stored. + */ +export interface OAuthAuth { + /** Display name, e.g. "Anthropic (Claude Pro/Max)". */ + name: string; + + login(callbacks: AuthLoginCallbacks): Promise; + + /** + * Exchange the refresh token. Network call; throws on failure + * (invalid_grant etc.). `Models` runs this under the store lock. + */ + refresh(credential: OAuthCredential): Promise; + + /** + * Side-effect-free derivation of request auth from a valid credential. + * Covers per-credential baseUrl (GitHub Copilot). Async so lazy wrappers + * can load the implementation on first use. + */ + toAuth(credential: OAuthCredential): Promise; +} + +/** + * Provider auth. At least one of `apiKey`/`oauth` must be present: even + * ambient-credential providers and keyless local servers provide `apiKey` + * auth whose `resolve()` reports whether the provider is configured. + */ +export interface ProviderAuth { + apiKey?: ApiKeyAuth; + oauth?: OAuthAuth; +} diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index ed7aeaa8..dd584249 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -1,7 +1,11 @@ export type { Static, TSchema } from "typebox"; export { Type } from "typebox"; +export * from "./api/lazy.ts"; export * from "./api-registry.ts"; +export * from "./auth/context.ts"; +export * from "./auth/credential-store.ts"; +export * from "./auth/types.ts"; export * from "./env-api-keys.ts"; export * from "./image-models.ts"; export * from "./images.ts"; diff --git a/packages/ai/src/models.ts b/packages/ai/src/models.ts index e14a6c2f..ddfdcc02 100644 --- a/packages/ai/src/models.ts +++ b/packages/ai/src/models.ts @@ -1,5 +1,369 @@ +import { lazyStream } from "./api/lazy.ts"; +import { defaultProviderAuthContext as defaultAuthContext } from "./auth/context.ts"; +import { InMemoryCredentialStore } from "./auth/credential-store.ts"; +import type { + ApiKeyAuth, + ApiKeyCredential, + AuthContext, + AuthResult, + Credential, + CredentialStore, + OAuthAuth, + OAuthCredential, + ProviderAuth, +} from "./auth/types.ts"; import { MODELS } from "./models.generated.ts"; -import type { Api, KnownProvider, Model, ModelThinkingLevel, Usage } from "./types.ts"; +import type { + Api, + ApiStreamOptions, + AssistantMessage, + AssistantMessageEventStream, + Context, + KnownProvider, + Model, + ModelThinkingLevel, + SimpleStreamOptions, + StreamOptions, + Usage, +} from "./types.ts"; + +export type ModelsErrorCode = "model_source" | "model_validation" | "provider" | "stream" | "auth" | "oauth"; + +export class ModelsError extends Error { + readonly code: ModelsErrorCode; + + constructor(code: ModelsErrorCode, message: string, options?: { cause?: unknown }) { + super(message, options); + this.name = "ModelsError"; + this.code = code; + } +} + +/** + * A provider is the concrete runtime unit. It owns id/name/base metadata, + * auth methods, model listing, and stream behavior. + * + * `TApi` lets concrete provider factories declare which APIs their models + * use (e.g. `openaiProvider(): Provider<"openai-responses" | "openai-completions">`), + * giving typed model lists to direct factory users. Inside a `Models` + * collection providers are held as `Provider`. + */ +export interface Provider { + readonly id: string; + readonly name: string; + + readonly baseUrl?: string; + readonly headers?: Record; + + /** + * Required: at least one of `apiKey`/`oauth`. Every provider has auth + * semantics — even providers with only ambient credentials (env vars, AWS + * profiles, ADC files) and keyless local servers provide `apiKey` auth + * whose `resolve()` reports whether the provider is configured. + * `Models.getAuth()` returns undefined when the provider is unconfigured. + */ + readonly auth: ProviderAuth; + + /** + * List models. Async and side-effect-free discovery only; provider-specific + * model lifecycle (load/unload) belongs in app commands. + */ + getModels(options?: { forceRefresh?: boolean }): Promise[]> | readonly Model[]; + + stream( + model: Model, + context: Context, + options?: ApiStreamOptions, + ): AssistantMessageEventStream; + + streamSimple(model: Model, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream; +} + +/** + * Runtime collection of providers plus auth application and stream + * convenience. Providers own stream behavior; `Models` resolves auth and + * delegates each request to the provider that owns the model. + */ +export interface Models { + getProviders(): readonly Provider[]; + getProvider(id: string): Provider | undefined; + + /** + * List models from one provider or all providers. Best-effort aggregation: + * provider source failures yield the models that did list (empty for a + * single failing provider). Apps that need the failure call + * `getProvider(id).getModels()` directly. + */ + getModels(options?: { forceRefresh?: boolean }): Promise[]>; + getModels(provider?: string, options?: { forceRefresh?: boolean }): Promise[]>; + + /** + * Runtime model lookup. Dynamic model lists are typed as `Model`; + * narrow with the `hasApi()` type guard. + */ + getModel(provider: string, id: string, options?: { forceRefresh?: boolean }): Promise | undefined>; + + /** + * Resolve request auth for a model. Includes a source label for status UI. + * Resolves `undefined` when the provider is unknown or unconfigured. + * Rejects with `ModelsError`: code "oauth" when a token refresh fails (the + * stored credential is preserved for retry; re-login fixes it), code "auth" + * when api-key resolution or the credential store fails. Request paths + * surface rejections as stream errors; status/availability UIs catch them + * and render "needs re-login" instead of treating them as unconfigured. + */ + getAuth(model: Model): Promise; + + stream( + model: Model, + context: Context, + options?: ApiStreamOptions, + ): AssistantMessageEventStream; + + complete( + model: Model, + context: Context, + options?: ApiStreamOptions, + ): Promise; + + streamSimple(model: Model, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream; + completeSimple(model: Model, context: Context, options?: SimpleStreamOptions): Promise; +} + +export interface MutableModels extends Models { + /** Upsert/replace by provider.id. Provider ids are unique. */ + setProvider(provider: Provider): void; + deleteProvider(id: string): void; + clearProviders(): void; +} + +export interface CreateModelsOptions { + credentials?: CredentialStore; + authContext?: AuthContext; +} + +class ModelsImpl implements MutableModels { + private providers = new Map(); + private credentials: CredentialStore; + private authContext: AuthContext; + + constructor(options?: CreateModelsOptions) { + this.credentials = options?.credentials ?? new InMemoryCredentialStore(); + this.authContext = options?.authContext ?? defaultAuthContext(); + } + + setProvider(provider: Provider): void { + this.providers.set(provider.id, provider); + } + + deleteProvider(id: string): void { + this.providers.delete(id); + } + + clearProviders(): void { + this.providers.clear(); + } + + getProviders(): readonly Provider[] { + return Array.from(this.providers.values()); + } + + getProvider(id: string): Provider | undefined { + return this.providers.get(id); + } + + async getModels( + providerOrOptions?: string | { forceRefresh?: boolean }, + maybeOptions?: { forceRefresh?: boolean }, + ): Promise[]> { + const provider = typeof providerOrOptions === "string" ? providerOrOptions : undefined; + const options = typeof providerOrOptions === "string" ? maybeOptions : providerOrOptions; + + if (provider !== undefined) { + const entry = this.providers.get(provider); + if (!entry) return []; + try { + return await entry.getModels(options); + } catch { + return []; + } + } + + // Async wrapper turns sync throws from ill-behaved providers into rejections. + const results = await Promise.allSettled( + Array.from(this.providers.values(), async (entry) => entry.getModels(options)), + ); + const models: Model[] = []; + for (const result of results) { + if (result.status === "fulfilled") models.push(...result.value); + } + return models; + } + + async getModel(provider: string, id: string, options?: { forceRefresh?: boolean }): Promise | undefined> { + const models = await this.getModels(provider, options); + return models.find((model) => model.id === id); + } + + async getAuth(model: Model): Promise { + const provider = this.providers.get(model.provider); + if (!provider) return undefined; + + // A stored credential owns the provider: ambient/env is consulted only + // when nothing is stored. No silent env fallback after a failed refresh + // or for a credential type without a matching handler. + const stored = await this.readCredential(provider.id); + if (stored) { + if (stored.type === "oauth" && provider.auth.oauth) { + return this.resolveOAuth(provider.id, provider.auth.oauth, stored); + } + if (stored.type === "api-key" && provider.auth.apiKey) { + return this.resolveApiKey(provider.auth.apiKey, model, stored); + } + return undefined; + } + + // Ambient (env vars, AWS profiles, ADC files). + return provider.auth.apiKey ? this.resolveApiKey(provider.auth.apiKey, model, undefined) : undefined; + } + + /** + * OAuth resolution with double-checked locking (same pattern as today's + * AuthStorage): valid tokens cost zero locks; expired tokens lock, + * re-check expiry under the lock, refresh once globally, and persist the + * rotated credential before release. + */ + private async resolveOAuth( + providerId: string, + oauth: OAuthAuth, + stored: OAuthCredential, + ): Promise { + let credential = stored; + + if (Date.now() >= credential.expires) { + // Optimistic check said expired; the authoritative check runs under the lock. + let post: Credential | undefined; + try { + post = await this.credentials.modify(providerId, async (current) => { + if (current?.type !== "oauth") return undefined; // logged out meanwhile + if (Date.now() < current.expires) return undefined; // another process/request refreshed + try { + return await oauth.refresh(current); + } catch (error) { + throw new ModelsError("oauth", `OAuth refresh failed for ${providerId}`, { cause: error }); + } + }); + } catch (error) { + if (error instanceof ModelsError) throw error; + throw new ModelsError("auth", `Credential store modify failed for ${providerId}`, { cause: error }); + } + if (post?.type !== "oauth") return undefined; // logged out meanwhile + credential = post; + } + + try { + return { auth: await oauth.toAuth(credential), source: "OAuth" }; + } catch (error) { + throw new ModelsError("oauth", `OAuth auth derivation failed for ${providerId}`, { cause: error }); + } + } + + private async resolveApiKey( + apiKey: ApiKeyAuth, + model: Model, + credential: ApiKeyCredential | undefined, + ): Promise { + try { + return await apiKey.resolve({ model, ctx: this.authContext, credential }); + } catch (error) { + throw new ModelsError("auth", `API key auth failed for provider ${model.provider}`, { cause: error }); + } + } + + private async readCredential(providerId: string): Promise { + try { + return await this.credentials.read(providerId); + } catch (error) { + throw new ModelsError("auth", `Credential store read failed for ${providerId}`, { cause: error }); + } + } + + private requireProvider(model: Model): Provider { + const provider = this.providers.get(model.provider); + if (!provider) { + throw new ModelsError("provider", `Unknown provider: ${model.provider}`); + } + return provider; + } + + private async applyAuth( + model: Model, + options: TOptions | undefined, + ): Promise<{ requestModel: Model; requestOptions: TOptions | undefined }> { + const resolution = await this.getAuth(model); + const auth = resolution?.auth; + if (!auth) return { requestModel: model, requestOptions: options }; + + const requestModel = auth.baseUrl ? { ...model, baseUrl: auth.baseUrl } : model; + + // Explicit request options win per-field; headers merge per header. + const apiKey = options?.apiKey ?? auth.apiKey; + const headers = auth.headers || options?.headers ? { ...auth.headers, ...options?.headers } : undefined; + const requestOptions = { ...options, apiKey, headers } as TOptions; + + return { requestModel, requestOptions }; + } + + stream( + model: Model, + context: Context, + options?: ApiStreamOptions, + ): AssistantMessageEventStream { + return lazyStream(model, async () => { + const provider = this.requireProvider(model); + const { requestModel, requestOptions } = await this.applyAuth(model, options as StreamOptions | undefined); + return provider.stream(requestModel as Model, context, requestOptions as ApiStreamOptions); + }); + } + + async complete( + model: Model, + context: Context, + options?: ApiStreamOptions, + ): Promise { + return this.stream(model, context, options).result(); + } + + streamSimple(model: Model, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream { + return lazyStream(model, async () => { + const provider = this.requireProvider(model); + const { requestModel, requestOptions } = await this.applyAuth(model, options); + return provider.streamSimple(requestModel, context, requestOptions); + }); + } + + async completeSimple(model: Model, context: Context, options?: SimpleStreamOptions): Promise { + return this.streamSimple(model, context, options).result(); + } +} + +export function createModels(options?: CreateModelsOptions): MutableModels { + return new ModelsImpl(options); +} + +/** + * Runtime-checked narrowing for dynamically looked-up models: + * + * ```ts + * const model = await models.getModel("anthropic", "claude-opus-4-7"); + * if (model && hasApi(model, "anthropic-messages")) { + * // model: Model<"anthropic-messages">, stream options fully typed + * } + * ``` + */ +export function hasApi(model: Model, api: TApi): model is Model { + return model.api === api; +} const modelRegistry: Map>> = new Map(); diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index 802b8b39..e14c493f 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -1,3 +1,12 @@ +import type { BedrockOptions } from "./providers/amazon-bedrock.ts"; +import type { AnthropicOptions } from "./providers/anthropic.ts"; +import type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses.ts"; +import type { GoogleOptions } from "./providers/google.ts"; +import type { GoogleVertexOptions } from "./providers/google-vertex.ts"; +import type { MistralOptions } from "./providers/mistral.ts"; +import type { OpenAICodexResponsesOptions } from "./providers/openai-codex-responses.ts"; +import type { OpenAICompletionsOptions } from "./providers/openai-completions.ts"; +import type { OpenAIResponsesOptions } from "./providers/openai-responses.ts"; import type { AssistantMessageDiagnostic } from "./utils/diagnostics.ts"; import type { AssistantMessageEventStream } from "./utils/event-stream.ts"; @@ -56,7 +65,7 @@ export type KnownProvider = | "xiaomi-token-plan-cn" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-sgp"; -export type Provider = KnownProvider | string; +export type ProviderId = KnownProvider | string; export type KnownImagesProvider = "openrouter"; @@ -157,6 +166,31 @@ export interface StreamOptions { export type ProviderStreamOptions = StreamOptions & Record; +/** + * Maps known APIs to their full provider-specific stream option types. + * Type-only imports from API implementation modules are erased at emit, so + * this is tree-shake safe. + */ +export interface ApiOptionsMap { + "anthropic-messages": AnthropicOptions; + "openai-completions": OpenAICompletionsOptions; + "openai-responses": OpenAIResponsesOptions; + "openai-codex-responses": OpenAICodexResponsesOptions; + "azure-openai-responses": AzureOpenAIResponsesOptions; + "google-generative-ai": GoogleOptions; + "google-vertex": GoogleVertexOptions; + "mistral-conversations": MistralOptions; + "bedrock-converse-stream": BedrockOptions; +} + +/** + * Full stream options for an API. Known APIs resolve to their concrete option + * type; custom API strings fall back to the generic shape. + */ +export type ApiStreamOptions = TApi extends keyof ApiOptionsMap + ? ApiOptionsMap[TApi] + : StreamOptions & Record; + export interface ImagesOptions { signal?: AbortSignal; apiKey?: string; @@ -289,7 +323,7 @@ export interface AssistantMessage { role: "assistant"; content: (TextContent | ThinkingContent | ToolCall)[]; api: Api; - provider: Provider; + provider: ProviderId; model: string; responseModel?: string; // Concrete `chunk.model` when different from the requested `model` (e.g. OpenRouter `auto` -> `anthropic/...`) responseId?: string; // Provider-specific response/message identifier when the upstream API exposes one @@ -569,7 +603,7 @@ export interface Model { id: string; name: string; api: TApi; - provider: Provider; + provider: ProviderId; baseUrl: string; reasoning: boolean; /** diff --git a/packages/ai/test/models-runtime.test.ts b/packages/ai/test/models-runtime.test.ts new file mode 100644 index 00000000..7f94ccb7 --- /dev/null +++ b/packages/ai/test/models-runtime.test.ts @@ -0,0 +1,399 @@ +import { describe, expect, it } from "vitest"; +import { InMemoryCredentialStore } from "../src/auth/credential-store.ts"; +import type { ApiKeyAuth, CredentialStore, OAuthAuth, ProviderAuth } from "../src/auth/types.ts"; +import { createModels, hasApi, type Provider } from "../src/models.ts"; +import type { Api, AssistantMessage, Context, Model, SimpleStreamOptions, StreamOptions } from "../src/types.ts"; +import { AssistantMessageEventStream } from "../src/utils/event-stream.ts"; + +function testModel(provider: string, id: string): Model { + return { + id, + name: id, + api: "test-api", + provider, + baseUrl: "https://example.test/v1", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 10000, + maxTokens: 1000, + }; +} + +function doneMessage(model: Model, text: string): AssistantMessage { + return { + role: "assistant", + content: [{ type: "text", text }], + api: model.api, + provider: model.provider, + model: model.id, + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "stop", + timestamp: Date.now(), + }; +} + +interface ProviderCall { + model: Model; + options: StreamOptions | undefined; +} + +/** Ambient auth for keyless test providers; reports "configured" with no auth values. */ +const ambientAuth: ApiKeyAuth = { + name: "Ambient", + resolve: async () => ({ auth: {} }), +}; + +function testProvider(input: { + id: string; + models?: Model[]; + auth?: ProviderAuth; + getModels?: () => Promise[]>; + calls?: ProviderCall[]; +}): Provider { + const models = input.models ?? [testModel(input.id, "model-a")]; + const respond = (model: Model, options: StreamOptions | undefined) => { + input.calls?.push({ model, options }); + const stream = new AssistantMessageEventStream(); + const message = doneMessage(model, "ok"); + stream.push({ type: "start", partial: message }); + stream.push({ type: "done", reason: "stop", message }); + stream.end(message); + return stream; + }; + return { + id: input.id, + name: input.id, + auth: input.auth ?? { apiKey: ambientAuth }, + getModels: input.getModels ?? (async () => models), + stream: (model, _context, options) => respond(model, options as StreamOptions | undefined), + streamSimple: (model, _context, options) => respond(model, options as SimpleStreamOptions | undefined), + }; +} + +const context: Context = { messages: [{ role: "user", content: "hi", timestamp: Date.now() }] }; + +function envKeyAuth(key: string | undefined): ApiKeyAuth { + return { + name: "Test API key", + resolve: async ({ credential }) => { + const resolved = credential?.key ?? key; + if (!resolved) return undefined; + return { auth: { apiKey: resolved }, source: credential ? "stored" : "env" }; + }, + }; +} + +function testOAuth(overrides?: Partial): OAuthAuth { + return { + name: "Test OAuth", + login: async () => { + throw new Error("not used"); + }, + refresh: async (credential) => credential, + toAuth: async (credential) => ({ apiKey: credential.access }), + ...overrides, + }; +} + +describe("Models runtime", () => { + it("registers, replaces, and deletes providers", () => { + const models = createModels(); + models.setProvider(testProvider({ id: "p1" })); + models.setProvider(testProvider({ id: "p2" })); + expect(models.getProviders().map((p) => p.id)).toEqual(["p1", "p2"]); + + const replacement = testProvider({ id: "p1" }); + models.setProvider(replacement); + expect(models.getProvider("p1")).toBe(replacement); + expect(models.getProviders()).toHaveLength(2); + + models.deleteProvider("p1"); + expect(models.getProvider("p1")).toBeUndefined(); + + models.clearProviders(); + expect(models.getProviders()).toHaveLength(0); + }); + + it("lists and finds models per provider", async () => { + const models = createModels(); + models.setProvider(testProvider({ id: "p1", models: [testModel("p1", "m1"), testModel("p1", "m2")] })); + models.setProvider(testProvider({ id: "p2", models: [testModel("p2", "m3")] })); + + expect((await models.getModels()).map((m) => m.id)).toEqual(["m1", "m2", "m3"]); + expect((await models.getModels("p1")).map((m) => m.id)).toEqual(["m1", "m2"]); + expect((await models.getModels("nope")).length).toBe(0); + expect((await models.getModel("p2", "m3"))?.id).toBe("m3"); + expect(await models.getModel("p2", "missing")).toBeUndefined(); + + // hasApi() narrows dynamically looked-up models with a runtime check + const found = await models.getModel("p2", "m3"); + expect(found && hasApi(found, "openai-completions")).toBe(false); + expect(found && hasApi(found, "test-api")).toBe(true); + if (found && hasApi(found, "test-api")) { + const _typed: Model<"test-api"> = found; + expect(_typed.id).toBe("m3"); + } + }); + + it("swallows provider source failures for both all-provider and single-provider listing", async () => { + const models = createModels(); + models.setProvider( + testProvider({ + id: "broken", + getModels: async () => { + throw new Error("boom"); + }, + }), + ); + models.setProvider(testProvider({ id: "ok", models: [testModel("ok", "m1")] })); + + expect((await models.getModels()).map((m) => m.id)).toEqual(["m1"]); + expect(await models.getModels("broken")).toEqual([]); + // precise failures come from the provider directly + await expect(models.getProvider("broken")?.getModels()).rejects.toThrow("boom"); + + // even sync-throwing (non-async) provider implementations are isolated + models.setProvider({ + ...testProvider({ id: "sync-broken" }), + getModels: () => { + throw new Error("sync boom"); + }, + }); + expect((await models.getModels()).map((m) => m.id)).toEqual(["m1"]); + }); + + it("supports getModels(options) without a provider id", async () => { + const seen: ({ forceRefresh?: boolean } | undefined)[] = []; + const models = createModels(); + models.setProvider(testProvider({ id: "p1", models: [testModel("p1", "m1")] })); + models.setProvider({ + ...testProvider({ id: "p2" }), + getModels: async (options) => { + seen.push(options); + return [testModel("p2", "m2")]; + }, + }); + + const all = await models.getModels({ forceRefresh: true }); + expect(all.map((m) => m.id)).toEqual(["m1", "m2"]); + expect(seen).toEqual([{ forceRefresh: true }]); + }); + + it("resolves auth: stored credential owns the provider, ambient only when nothing stored", async () => { + const credentials = new InMemoryCredentialStore(); + const models = createModels({ credentials }); + models.setProvider(testProvider({ id: "p1", auth: { apiKey: envKeyAuth("env-key"), oauth: testOAuth() } })); + const model = testModel("p1", "model-a"); + + // nothing stored: ambient env resolves + expect((await models.getAuth(model))?.auth.apiKey).toBe("env-key"); + + // stored oauth credential (persisted via the single write path): beats ambient env + await credentials.modify("p1", async () => ({ + type: "oauth", + access: "oauth-token", + refresh: "r", + expires: Date.now() + 100000, + })); + const resolution = await models.getAuth(model); + expect(resolution?.auth.apiKey).toBe("oauth-token"); + expect(resolution?.source).toBe("OAuth"); + + // stored api-key credential resolves through apiKey auth, beats env + await credentials.modify("p1", async () => ({ type: "api-key", key: "stored-key" })); + const apiKeyResolution = await models.getAuth(model); + expect(apiKeyResolution?.auth.apiKey).toBe("stored-key"); + expect(apiKeyResolution?.source).toBe("stored"); + }); + + it("a stored credential without a matching handler blocks ambient fallback", async () => { + const credentials = new InMemoryCredentialStore(); + const models = createModels({ credentials }); + // provider has only apiKey auth, but an oauth credential is stored (stale config) + models.setProvider(testProvider({ id: "p1", auth: { apiKey: envKeyAuth("env-key") } })); + await credentials.modify("p1", async () => ({ type: "oauth", access: "a", refresh: "r", expires: 0 })); + + expect(await models.getAuth(testModel("p1", "model-a"))).toBeUndefined(); + }); + + it("refreshes expired oauth credentials and persists the rotated credential", async () => { + const credentials = new InMemoryCredentialStore(); + const oauth = testOAuth({ + refresh: async (credential) => ({ ...credential, access: "new-token", expires: Date.now() + 60_000 }), + }); + const models = createModels({ credentials }); + models.setProvider(testProvider({ id: "p1", auth: { oauth } })); + await credentials.modify("p1", async () => ({ + type: "oauth", + access: "old-token", + refresh: "r", + expires: 0, + })); + + const resolution = await models.getAuth(testModel("p1", "model-a")); + expect(resolution?.auth.apiKey).toBe("new-token"); + expect(((await credentials.read("p1")) as { access: string }).access).toBe("new-token"); + }); + + it("rejects with code oauth when refresh fails, preserving the stored credential", async () => { + const credentials = new InMemoryCredentialStore(); + const oauth = testOAuth({ + refresh: async () => { + throw new Error("invalid_grant"); + }, + }); + const models = createModels({ credentials }); + models.setProvider(testProvider({ id: "p1", auth: { oauth } })); + await credentials.modify("p1", async () => ({ type: "oauth", access: "old", refresh: "r", expires: 0 })); + + await expect(models.getAuth(testModel("p1", "model-a"))).rejects.toMatchObject({ code: "oauth" }); + // credential preserved for retry / re-login + expect(((await credentials.read("p1")) as { access: string }).access).toBe("old"); + }); + + it("serializes concurrent OAuth refreshes through store.modify (no double refresh)", async () => { + const credentials = new InMemoryCredentialStore(); + await credentials.modify("p1", async () => ({ type: "oauth", access: "old", refresh: "r1", expires: 0 })); + + let refreshes = 0; + const oauth = testOAuth({ + refresh: async () => { + refreshes++; + await new Promise((resolve) => setTimeout(resolve, 10)); + return { type: "oauth", access: `new-${refreshes}`, refresh: "r2", expires: Date.now() + 60_000 }; + }, + }); + const models = createModels({ credentials }); + models.setProvider(testProvider({ id: "p1", auth: { oauth } })); + const model = testModel("p1", "model-a"); + + const [a, b] = await Promise.all([models.getAuth(model), models.getAuth(model)]); + expect(refreshes).toBe(1); + expect(a?.auth.apiKey).toBe("new-1"); + expect(b?.auth.apiKey).toBe("new-1"); + }); + + it("valid oauth tokens resolve without touching modify", async () => { + let modifies = 0; + const base = new InMemoryCredentialStore(); + const credentials: CredentialStore = { + read: (pid) => base.read(pid), + modify: (pid, fn) => { + modifies++; + return base.modify(pid, fn); + }, + delete: (pid) => base.delete(pid), + }; + await base.modify("p1", async () => ({ + type: "oauth", + access: "valid", + refresh: "r", + expires: Date.now() + 60_000, + })); + const models = createModels({ credentials }); + models.setProvider(testProvider({ id: "p1", auth: { oauth: testOAuth() } })); + + expect((await models.getAuth(testModel("p1", "model-a")))?.auth.apiKey).toBe("valid"); + expect(modifies).toBe(0); + }); + + it("wraps credential store failures in ModelsError", async () => { + // read failure + const readFailing: CredentialStore = { + read: async () => { + throw new Error("disk on fire"); + }, + modify: async () => undefined, + delete: async () => {}, + }; + const models = createModels({ credentials: readFailing }); + models.setProvider(testProvider({ id: "p1", auth: { apiKey: envKeyAuth("env-key") } })); + await expect(models.getAuth(testModel("p1", "model-a"))).rejects.toMatchObject({ code: "auth" }); + + // modify failure during refresh + const modifyFailing: CredentialStore = { + read: async () => ({ type: "oauth", access: "old", refresh: "r", expires: 0 }), + modify: async () => { + throw new Error("disk on fire"); + }, + delete: async () => {}, + }; + const oauthModels = createModels({ credentials: modifyFailing }); + oauthModels.setProvider(testProvider({ id: "p1", auth: { oauth: testOAuth() } })); + await expect(oauthModels.getAuth(testModel("p1", "model-a"))).rejects.toMatchObject({ code: "auth" }); + }); + + it("wraps api-key auth failures in ModelsError", async () => { + const failing: ApiKeyAuth = { + name: "Failing", + resolve: async () => { + throw new Error("nope"); + }, + }; + const models = createModels(); + models.setProvider(testProvider({ id: "p1", auth: { apiKey: failing } })); + await expect(models.getAuth(testModel("p1", "model-a"))).rejects.toMatchObject({ code: "auth" }); + }); + + it("merges resolved auth into stream options; explicit options win per field", async () => { + const calls: ProviderCall[] = []; + const apiKey: ApiKeyAuth = { + name: "Test", + resolve: async () => ({ + auth: { + apiKey: "resolved-key", + headers: { "x-a": "auth", "x-b": "auth" }, + baseUrl: "https://auth.test/v1", + }, + }), + }; + const models = createModels(); + models.setProvider(testProvider({ id: "p1", auth: { apiKey }, calls })); + const model = testModel("p1", "model-a"); + + const result = await models.completeSimple(model, context, { + apiKey: "explicit-key", + headers: { "x-b": "explicit" }, + }); + expect(result.stopReason).toBe("stop"); + expect(calls).toHaveLength(1); + expect(calls[0].options?.apiKey).toBe("explicit-key"); + expect(calls[0].options?.headers).toEqual({ "x-a": "auth", "x-b": "explicit" }); + expect(calls[0].model.baseUrl).toBe("https://auth.test/v1"); + + // without explicit options, resolved auth applies + const result2 = await models.completeSimple(model, context); + expect(result2.stopReason).toBe("stop"); + expect(calls[1].options?.apiKey).toBe("resolved-key"); + }); + + it("produces an error stream for unknown providers instead of throwing", async () => { + const models = createModels(); + const result = await models.completeSimple(testModel("ghost", "model-a"), context); + expect(result.stopReason).toBe("error"); + expect(result.errorMessage).toContain("Unknown provider: ghost"); + }); + + it("streams through the provider", async () => { + const models = createModels(); + models.setProvider(testProvider({ id: "p1" })); + const model = testModel("p1", "model-a"); + + const events: string[] = []; + const stream = models.streamSimple(model, context); + for await (const event of stream) { + events.push(event.type); + } + expect(events).toEqual(["start", "done"]); + const message = await stream.result(); + expect(message.stopReason).toBe("stop"); + }); +}); diff --git a/packages/ai/test/scratch.ts b/packages/ai/test/scratch.ts new file mode 100644 index 00000000..949dccbe --- /dev/null +++ b/packages/ai/test/scratch.ts @@ -0,0 +1,87 @@ +// Scratch script showing real-world use of the new Models API. +// Run from packages/ai: node test/scratch.ts +// Requires ANTHROPIC_API_KEY. + +import { createModels, getModels, type Provider } from "../src/models.ts"; +import { streamAnthropic, streamSimpleAnthropic } from "../src/providers/register-builtins.ts"; +import type { Context } from "../src/types.ts"; + +// --------------------------------------------------------------------------- +// 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: streamAnthropic, + streamSimple: streamSimpleAnthropic, +}; + +// --------------------------------------------------------------------------- +// 2. Build a Models runtime and register the provider. +// --------------------------------------------------------------------------- + +const models = createModels(); +models.setProvider(anthropic); + +// --------------------------------------------------------------------------- +// 3. Look up a model and check auth. +// --------------------------------------------------------------------------- + +const model = await models.getModel("anthropic", "claude-haiku-4-5"); +if (!model) throw new Error("model not found"); + +const auth = await models.getAuth(model); +console.log(`model: ${model.provider}/${model.id}`); +console.log(`auth: ${auth ? `configured via ${auth.source}` : "not configured"}\n`); +if (!auth) process.exit(1); + +const context: Context = { + systemPrompt: "You are terse.", + messages: [{ role: "user", content: "Say exactly: ok", timestamp: Date.now() }], +}; + +// --------------------------------------------------------------------------- +// 4. 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. +// --------------------------------------------------------------------------- + +context.messages.push(message, { + role: "user", + content: "Now count from 1 to 5, one number per line.", + timestamp: Date.now(), +}); + +process.stdout.write("streamSimple -> "); +const stream = models.streamSimple(model, context); +for await (const event of stream) { + if (event.type === "text_delta") process.stdout.write(event.delta.replaceAll("\n", " ")); +} +const final = await stream.result(); +console.log(`[${final.stopReason}] cost: $${final.usage.cost.total.toFixed(6)}`); From ba93da9a93c08557628d293c1601df235517314d Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 10 Jun 2026 20:08:59 +0200 Subject: [PATCH 005/122] feat(ai): move API implementations to src/api with lazy wrappers (phase 2) Stream implementations move from src/providers/ to src/api/, renamed by API id (anthropic.ts -> anthropic-messages.ts, google.ts -> google-generative-ai.ts, mistral.ts -> mistral-conversations.ts, amazon-bedrock.ts -> bedrock-converse-stream.ts). Every module now exports exactly stream/streamSimple; shared helpers move alongside. New ProviderStreams dispatch contract in types.ts, lazyApi() wrapper in api/lazy.ts, and one .lazy.ts wrapper per API. Bedrock's wrapper keeps the node-only variable-specifier import and setBedrockProviderModule() (now taking ProviderStreams). providers/register-builtins.ts deleted; interim until the compat entrypoint lands, builtin api-registry registration lives in stream.ts and lazy wrappers are exported from the root barrel. Old per-API lazy exports (streamAnthropic, ...) are gone; package.json subpaths retarget to dist/api/. --- packages/agent/docs/models.md | 42 +- packages/ai/package.json | 32 +- packages/ai/scripts/generate-models.ts | 2 +- .../ai/src/api/anthropic-messages.lazy.ts | 4 + .../anthropic-messages.ts} | 14 +- .../ai/src/api/azure-openai-responses.lazy.ts | 4 + .../azure-openai-responses.ts | 6 +- .../src/api/bedrock-converse-stream.lazy.ts | 30 ++ .../bedrock-converse-stream.ts} | 12 +- .../ai/src/{providers => api}/cloudflare.ts | 0 .../github-copilot-headers.ts | 0 .../ai/src/api/google-generative-ai.lazy.ts | 4 + .../google.ts => api/google-generative-ai.ts} | 10 +- .../src/{providers => api}/google-shared.ts | 0 packages/ai/src/api/google-vertex.lazy.ts | 4 + .../src/{providers => api}/google-vertex.ts | 10 +- packages/ai/src/api/lazy.ts | 16 +- .../ai/src/api/mistral-conversations.lazy.ts | 4 + .../mistral-conversations.ts} | 6 +- .../ai/src/api/openai-codex-responses.lazy.ts | 4 + .../openai-codex-responses.ts | 6 +- .../ai/src/api/openai-completions.lazy.ts | 4 + .../{providers => api}/openai-completions.ts | 6 +- .../{providers => api}/openai-prompt-cache.ts | 0 .../openai-responses-shared.ts | 0 packages/ai/src/api/openai-responses.lazy.ts | 4 + .../{providers => api}/openai-responses.ts | 6 +- .../src/{providers => api}/simple-options.ts | 0 .../{providers => api}/transform-messages.ts | 0 packages/ai/src/bedrock-provider.ts | 6 +- packages/ai/src/index.ts | 33 +- .../ai/src/providers/register-builtins.ts | 406 ------------------ packages/ai/src/stream.ts | 39 +- packages/ai/src/types.ts | 31 +- .../anthropic-eager-tool-input-compat.test.ts | 2 +- .../ai/test/anthropic-sse-parsing.test.ts | 2 +- .../ai/test/azure-openai-base-url.test.ts | 2 +- .../ai/test/bedrock-convert-messages.test.ts | 2 +- .../ai/test/bedrock-custom-headers.test.ts | 4 +- .../test/bedrock-endpoint-resolution.test.ts | 2 +- .../ai/test/bedrock-thinking-payload.test.ts | 2 +- packages/ai/test/cache-retention.test.ts | 6 +- .../ai/test/codex-websocket-cached-probe.ts | 6 +- packages/ai/test/fireworks-models.test.ts | 2 +- .../ai/test/github-copilot-anthropic.test.ts | 2 +- .../test/google-shared-convert-tools.test.ts | 2 +- ...-shared-gemini3-unsigned-tool-call.test.ts | 2 +- ...e-shared-image-tool-result-routing.test.ts | 2 +- .../ai/test/google-thinking-signature.test.ts | 2 +- .../google-vertex-api-key-resolution.test.ts | 2 +- packages/ai/test/lazy-module-load.test.ts | 4 +- packages/ai/test/openai-codex-stream.test.ts | 6 +- ...i-completions-cache-control-format.test.ts | 2 +- .../openai-completions-prompt-cache.test.ts | 2 +- .../ai/test/openai-completions-retry.test.ts | 2 +- ...penai-completions-thinking-as-text.test.ts | 2 +- ...nai-completions-tool-result-images.test.ts | 2 +- .../openai-responses-copilot-provider.test.ts | 2 +- ...enai-responses-foreign-toolcall-id.test.ts | 2 +- .../test/openai-responses-message-id.test.ts | 2 +- ...nai-responses-partial-json-cleanup.test.ts | 2 +- packages/ai/test/scratch.ts | 8 +- ...ssages-copilot-openai-to-anthropic.test.ts | 2 +- .../custom-provider-anthropic/index.ts | 2 +- .../custom-provider-gitlab-duo/index.ts | 12 +- .../test/sdk-codex-cache-probe-tool-loop.ts | 4 +- 66 files changed, 283 insertions(+), 560 deletions(-) create mode 100644 packages/ai/src/api/anthropic-messages.lazy.ts rename packages/ai/src/{providers/anthropic.ts => api/anthropic-messages.ts} (98%) create mode 100644 packages/ai/src/api/azure-openai-responses.lazy.ts rename packages/ai/src/{providers => api}/azure-openai-responses.ts (97%) create mode 100644 packages/ai/src/api/bedrock-converse-stream.lazy.ts rename packages/ai/src/{providers/amazon-bedrock.ts => api/bedrock-converse-stream.ts} (98%) rename packages/ai/src/{providers => api}/cloudflare.ts (100%) rename packages/ai/src/{providers => api}/github-copilot-headers.ts (100%) create mode 100644 packages/ai/src/api/google-generative-ai.lazy.ts rename packages/ai/src/{providers/google.ts => api/google-generative-ai.ts} (97%) rename packages/ai/src/{providers => api}/google-shared.ts (100%) create mode 100644 packages/ai/src/api/google-vertex.lazy.ts rename packages/ai/src/{providers => api}/google-vertex.ts (98%) create mode 100644 packages/ai/src/api/mistral-conversations.lazy.ts rename packages/ai/src/{providers/mistral.ts => api/mistral-conversations.ts} (98%) create mode 100644 packages/ai/src/api/openai-codex-responses.lazy.ts rename packages/ai/src/{providers => api}/openai-codex-responses.ts (99%) create mode 100644 packages/ai/src/api/openai-completions.lazy.ts rename packages/ai/src/{providers => api}/openai-completions.ts (99%) rename packages/ai/src/{providers => api}/openai-prompt-cache.ts (100%) rename packages/ai/src/{providers => api}/openai-responses-shared.ts (100%) create mode 100644 packages/ai/src/api/openai-responses.lazy.ts rename packages/ai/src/{providers => api}/openai-responses.ts (97%) rename packages/ai/src/{providers => api}/simple-options.ts (100%) rename packages/ai/src/{providers => api}/transform-messages.ts (100%) delete mode 100644 packages/ai/src/providers/register-builtins.ts diff --git a/packages/agent/docs/models.md b/packages/agent/docs/models.md index 89b89629..67f87549 100644 --- a/packages/agent/docs/models.md +++ b/packages/agent/docs/models.md @@ -33,23 +33,23 @@ packages/ai/src/ auth/ # auth method types, helpers, login callbacks api/ # API implementations and lazy wrappers openai-completions.ts # real implementation, imports SDKs, exports stream/streamSimple - openai-completions-lazy.ts + openai-completions.lazy.ts openai-responses.ts - openai-responses-lazy.ts + openai-responses.lazy.ts openai-codex-responses.ts - openai-codex-responses-lazy.ts + openai-codex-responses.lazy.ts azure-openai-responses.ts - azure-openai-responses-lazy.ts + azure-openai-responses.lazy.ts anthropic-messages.ts - anthropic-messages-lazy.ts + anthropic-messages.lazy.ts google-generative-ai.ts - google-generative-ai-lazy.ts + google-generative-ai.lazy.ts google-vertex.ts - google-vertex-lazy.ts + google-vertex.lazy.ts mistral-conversations.ts - mistral-conversations-lazy.ts + mistral-conversations.lazy.ts bedrock-converse-stream.ts - bedrock-converse-stream-lazy.ts + bedrock-converse-stream.lazy.ts lazy.ts # lazyStream()/lazyApi() helpers (shared helpers: openai-responses-shared, google-shared, transform-messages, ...) providers/ # concrete provider factories and per-provider catalogs @@ -315,22 +315,18 @@ export function stream(model, context, options) { ... } export function streamSimple(model, context, options) { ... } ``` -This makes the module itself satisfy `ProviderStreams`, so the lazy wrapper is one generic helper instead of bespoke per-API plumbing: +This makes the module itself satisfy `ProviderStreams`, so the lazy wrapper is one generic helper instead of bespoke per-API plumbing. `ProviderStreams` is the untyped dispatch shape (implementation modules export concretely typed functions, which would not be assignable to a generic method); per-API option typing lives on the modules themselves and on `Provider.stream()` via `ApiStreamOptions`: ```ts export interface ProviderStreams { - stream( - model: Model, - context: Context, - options?: ApiStreamOptions, - ): AssistantMessageEventStream; + stream(model: Model, context: Context, options?: StreamOptions): AssistantMessageEventStream; streamSimple(model: Model, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream; } // src/api/lazy.ts export function lazyApi(load: () => Promise): ProviderStreams; -// src/api/anthropic-messages-lazy.ts +// src/api/anthropic-messages.lazy.ts export const anthropicMessagesApi = (): ProviderStreams => lazyApi(() => import("./anthropic-messages.ts")); ``` @@ -350,7 +346,7 @@ Notes: Many concrete providers share an API implementation (OpenAI-completions: OpenRouter, Groq, Cerebras, xAI, ZAI, ...). They share lazy API objects by reference: ```ts -import { openAICompletionsApi } from "../api/openai-completions-lazy.ts"; +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; export function openrouterProvider(): Provider { return createProvider({ @@ -804,12 +800,12 @@ Check items off as they land. Keep this list current; it is the working state fo ### Phase 2 — `src/api/` -- [ ] Move stream implementations from `src/providers/` to `src/api/`, renamed by API id (`anthropic.ts` -> `api/anthropic-messages.ts`, etc.). -- [ ] Normalize each implementation module to export exactly `stream` and `streamSimple`. -- [ ] Move shared helpers (`openai-responses-shared`, `google-shared`, `transform-messages`, `openai-prompt-cache`, `github-copilot-headers`) to `src/api/`. -- [ ] Extract `lazyStream()`/`lazyApi()` into `src/api/lazy.ts`. -- [ ] Add `*-lazy.ts` wrappers per API; bedrock keeps node-only import trick and `setBedrockProviderModule()`. -- [ ] Delete `providers/register-builtins.ts`. +- [x] Move stream implementations from `src/providers/` to `src/api/`, renamed by API id (`anthropic.ts` -> `api/anthropic-messages.ts`, etc.). +- [x] Normalize each implementation module to export exactly `stream` and `streamSimple`. +- [x] Move shared helpers (`openai-responses-shared`, `google-shared`, `transform-messages`, `openai-prompt-cache`, `github-copilot-headers`, `cloudflare`, `simple-options`) to `src/api/`. +- [x] Extract `lazyStream()`/`lazyApi()` into `src/api/lazy.ts`. +- [x] Add `*.lazy.ts` wrappers per API; bedrock keeps node-only import trick and `setBedrockProviderModule()`. +- [x] Delete `providers/register-builtins.ts`. Interim until Phase 5 compat: builtin api-registry registration lives in `stream.ts`; lazy API wrappers are exported from the root barrel. ### Phase 3 — provider factories + catalogs diff --git a/packages/ai/package.json b/packages/ai/package.json index 9a54de2f..7b361b9a 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -11,36 +11,36 @@ "import": "./dist/index.js" }, "./anthropic": { - "types": "./dist/providers/anthropic.d.ts", - "import": "./dist/providers/anthropic.js" + "types": "./dist/api/anthropic-messages.d.ts", + "import": "./dist/api/anthropic-messages.js" }, "./azure-openai-responses": { - "types": "./dist/providers/azure-openai-responses.d.ts", - "import": "./dist/providers/azure-openai-responses.js" + "types": "./dist/api/azure-openai-responses.d.ts", + "import": "./dist/api/azure-openai-responses.js" }, "./google": { - "types": "./dist/providers/google.d.ts", - "import": "./dist/providers/google.js" + "types": "./dist/api/google-generative-ai.d.ts", + "import": "./dist/api/google-generative-ai.js" }, "./google-vertex": { - "types": "./dist/providers/google-vertex.d.ts", - "import": "./dist/providers/google-vertex.js" + "types": "./dist/api/google-vertex.d.ts", + "import": "./dist/api/google-vertex.js" }, "./mistral": { - "types": "./dist/providers/mistral.d.ts", - "import": "./dist/providers/mistral.js" + "types": "./dist/api/mistral-conversations.d.ts", + "import": "./dist/api/mistral-conversations.js" }, "./openai-codex-responses": { - "types": "./dist/providers/openai-codex-responses.d.ts", - "import": "./dist/providers/openai-codex-responses.js" + "types": "./dist/api/openai-codex-responses.d.ts", + "import": "./dist/api/openai-codex-responses.js" }, "./openai-completions": { - "types": "./dist/providers/openai-completions.d.ts", - "import": "./dist/providers/openai-completions.js" + "types": "./dist/api/openai-completions.d.ts", + "import": "./dist/api/openai-completions.js" }, "./openai-responses": { - "types": "./dist/providers/openai-responses.d.ts", - "import": "./dist/providers/openai-responses.js" + "types": "./dist/api/openai-responses.d.ts", + "import": "./dist/api/openai-responses.js" }, "./oauth": { "types": "./dist/oauth.d.ts", diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 5e5fff94..9744961d 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -8,7 +8,7 @@ import { CLOUDFLARE_AI_GATEWAY_COMPAT_BASE_URL, CLOUDFLARE_AI_GATEWAY_OPENAI_BASE_URL, CLOUDFLARE_WORKERS_AI_BASE_URL, -} from "../src/providers/cloudflare.ts"; +} from "../src/api/cloudflare.ts"; import type { AnthropicMessagesCompat, Api, KnownProvider, Model, OpenAICompletionsCompat } from "../src/types.ts"; const __filename = fileURLToPath(import.meta.url); diff --git a/packages/ai/src/api/anthropic-messages.lazy.ts b/packages/ai/src/api/anthropic-messages.lazy.ts new file mode 100644 index 00000000..1da2172e --- /dev/null +++ b/packages/ai/src/api/anthropic-messages.lazy.ts @@ -0,0 +1,4 @@ +import type { ProviderStreams } from "../types.ts"; +import { lazyApi } from "./lazy.ts"; + +export const anthropicMessagesApi = (): ProviderStreams => lazyApi(() => import("./anthropic-messages.ts")); diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/api/anthropic-messages.ts similarity index 98% rename from packages/ai/src/providers/anthropic.ts rename to packages/ai/src/api/anthropic-messages.ts index efe4bd75..1e3ef0be 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/api/anthropic-messages.ts @@ -186,7 +186,7 @@ export interface AnthropicOptions extends StreamOptions { * Enable extended thinking. * For adaptive thinking models: the model decides when/how much to think. * For older models: uses budget-based thinking with thinkingBudgetTokens. - * Default: undefined (thinking is omitted unless `streamSimpleAnthropic()` maps + * Default: undefined (thinking is omitted unless `streamSimple()` maps * a simple reasoning level to this option, or callers set it explicitly). */ thinkingEnabled?: boolean; @@ -205,7 +205,7 @@ export interface AnthropicOptions extends StreamOptions { * - "medium": Moderate thinking, may skip for simple queries * - "low": Minimal thinking, skips for simple tasks * Ignored for older models. - * Default: omitted unless `streamSimpleAnthropic()` maps a simple reasoning + * Default: omitted unless `streamSimple()` maps a simple reasoning * level to this option. */ effort?: AnthropicEffort; @@ -445,7 +445,7 @@ async function* iterateAnthropicEvents( } } -export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOptions> = ( +export const stream: StreamFunction<"anthropic-messages", AnthropicOptions> = ( model: Model<"anthropic-messages">, context: Context, options?: AnthropicOptions, @@ -733,7 +733,7 @@ function mapThinkingLevelToEffort( } } -export const streamSimpleAnthropic: StreamFunction<"anthropic-messages", SimpleStreamOptions> = ( +export const streamSimple: StreamFunction<"anthropic-messages", SimpleStreamOptions> = ( model: Model<"anthropic-messages">, context: Context, options?: SimpleStreamOptions, @@ -745,14 +745,14 @@ export const streamSimpleAnthropic: StreamFunction<"anthropic-messages", SimpleS const base = buildBaseOptions(model, options, apiKey); if (!options?.reasoning) { - return streamAnthropic(model, context, { ...base, thinkingEnabled: false } satisfies AnthropicOptions); + return stream(model, context, { ...base, thinkingEnabled: false } satisfies AnthropicOptions); } // For models with adaptive thinking: use an effort level. // For older models: use budget-based thinking. if (model.compat?.forceAdaptiveThinking === true) { const effort = mapThinkingLevelToEffort(model, options.reasoning); - return streamAnthropic(model, context, { + return stream(model, context, { ...base, thinkingEnabled: true, effort, @@ -768,7 +768,7 @@ export const streamSimpleAnthropic: StreamFunction<"anthropic-messages", SimpleS options.thinkingBudgets, ); - return streamAnthropic(model, context, { + return stream(model, context, { ...base, maxTokens: adjusted.maxTokens, thinkingEnabled: true, diff --git a/packages/ai/src/api/azure-openai-responses.lazy.ts b/packages/ai/src/api/azure-openai-responses.lazy.ts new file mode 100644 index 00000000..5921e10f --- /dev/null +++ b/packages/ai/src/api/azure-openai-responses.lazy.ts @@ -0,0 +1,4 @@ +import type { ProviderStreams } from "../types.ts"; +import { lazyApi } from "./lazy.ts"; + +export const azureOpenAIResponsesApi = (): ProviderStreams => lazyApi(() => import("./azure-openai-responses.ts")); diff --git a/packages/ai/src/providers/azure-openai-responses.ts b/packages/ai/src/api/azure-openai-responses.ts similarity index 97% rename from packages/ai/src/providers/azure-openai-responses.ts rename to packages/ai/src/api/azure-openai-responses.ts index ecb4c7e6..93660334 100644 --- a/packages/ai/src/providers/azure-openai-responses.ts +++ b/packages/ai/src/api/azure-openai-responses.ts @@ -69,7 +69,7 @@ export interface AzureOpenAIResponsesOptions extends StreamOptions { /** * Generate function for Azure OpenAI Responses API */ -export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses", AzureOpenAIResponsesOptions> = ( +export const stream: StreamFunction<"azure-openai-responses", AzureOpenAIResponsesOptions> = ( model: Model<"azure-openai-responses">, context: Context, options?: AzureOpenAIResponsesOptions, @@ -147,7 +147,7 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses" return stream; }; -export const streamSimpleAzureOpenAIResponses: StreamFunction<"azure-openai-responses", SimpleStreamOptions> = ( +export const streamSimple: StreamFunction<"azure-openai-responses", SimpleStreamOptions> = ( model: Model<"azure-openai-responses">, context: Context, options?: SimpleStreamOptions, @@ -161,7 +161,7 @@ export const streamSimpleAzureOpenAIResponses: StreamFunction<"azure-openai-resp const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined; const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning; - return streamAzureOpenAIResponses(model, context, { + return stream(model, context, { ...base, reasoningEffort, } satisfies AzureOpenAIResponsesOptions); diff --git a/packages/ai/src/api/bedrock-converse-stream.lazy.ts b/packages/ai/src/api/bedrock-converse-stream.lazy.ts new file mode 100644 index 00000000..f9b30b35 --- /dev/null +++ b/packages/ai/src/api/bedrock-converse-stream.lazy.ts @@ -0,0 +1,30 @@ +import type { ProviderStreams } from "../types.ts"; +import { lazyApi } from "./lazy.ts"; + +/** + * Loads the bedrock implementation through a variable specifier so bundlers + * (browser smoke, Bun compile) cannot follow the import into the Node-only + * AWS SDK. The `.ts`/`.js` rewrite keeps the trick working from both source + * and built output. + */ +const importNodeOnlyApi = (specifier: string): Promise => { + const runtimeSpecifier = import.meta.url.endsWith(".js") ? specifier.replace(/\.ts$/, ".js") : specifier; + return import(runtimeSpecifier); +}; + +let bedrockModuleOverride: ProviderStreams | undefined; + +/** + * Overrides the dynamically imported bedrock implementation. Used by the Bun + * binary build, where the variable-specifier import cannot be bundled; the + * build registers a statically imported module instead. + */ +export function setBedrockProviderModule(module: ProviderStreams): void { + bedrockModuleOverride = module; +} + +export const bedrockConverseStreamApi = (): ProviderStreams => + lazyApi( + async () => + bedrockModuleOverride ?? ((await importNodeOnlyApi("./bedrock-converse-stream.ts")) as ProviderStreams), + ); diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/api/bedrock-converse-stream.ts similarity index 98% rename from packages/ai/src/providers/amazon-bedrock.ts rename to packages/ai/src/api/bedrock-converse-stream.ts index a4ace1c2..ac6e9f29 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/api/bedrock-converse-stream.ts @@ -90,7 +90,7 @@ type Block = (TextContent | ThinkingContent | ToolCall) & { index?: number; part const EMPTY_TEXT_PLACEHOLDER = ""; -export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOptions> = ( +export const stream: StreamFunction<"bedrock-converse-stream", BedrockOptions> = ( model: Model<"bedrock-converse-stream">, context: Context, options: BedrockOptions = {}, @@ -352,19 +352,19 @@ function addCustomHeadersMiddleware(client: BedrockRuntimeClient, headers: Recor client.middlewareStack.add(middleware, { step: "build", name: "pi-ai-custom-headers", priority: "low" }); } -export const streamSimpleBedrock: StreamFunction<"bedrock-converse-stream", SimpleStreamOptions> = ( +export const streamSimple: StreamFunction<"bedrock-converse-stream", SimpleStreamOptions> = ( model: Model<"bedrock-converse-stream">, context: Context, options?: SimpleStreamOptions, ): AssistantMessageEventStream => { const base = buildBaseOptions(model, options, undefined); if (!options?.reasoning) { - return streamBedrock(model, context, { ...base, reasoning: undefined } satisfies BedrockOptions); + return stream(model, context, { ...base, reasoning: undefined } satisfies BedrockOptions); } if (isAnthropicClaudeModel(model)) { if (supportsAdaptiveThinking(model.id, model.name)) { - return streamBedrock(model, context, { + return stream(model, context, { ...base, reasoning: options.reasoning, thinkingBudgets: options.thinkingBudgets, @@ -380,7 +380,7 @@ export const streamSimpleBedrock: StreamFunction<"bedrock-converse-stream", Simp options.thinkingBudgets, ); - return streamBedrock(model, context, { + return stream(model, context, { ...base, maxTokens: adjusted.maxTokens, reasoning: options.reasoning, @@ -391,7 +391,7 @@ export const streamSimpleBedrock: StreamFunction<"bedrock-converse-stream", Simp } satisfies BedrockOptions); } - return streamBedrock(model, context, { + return stream(model, context, { ...base, reasoning: options.reasoning, thinkingBudgets: options.thinkingBudgets, diff --git a/packages/ai/src/providers/cloudflare.ts b/packages/ai/src/api/cloudflare.ts similarity index 100% rename from packages/ai/src/providers/cloudflare.ts rename to packages/ai/src/api/cloudflare.ts diff --git a/packages/ai/src/providers/github-copilot-headers.ts b/packages/ai/src/api/github-copilot-headers.ts similarity index 100% rename from packages/ai/src/providers/github-copilot-headers.ts rename to packages/ai/src/api/github-copilot-headers.ts diff --git a/packages/ai/src/api/google-generative-ai.lazy.ts b/packages/ai/src/api/google-generative-ai.lazy.ts new file mode 100644 index 00000000..136c5043 --- /dev/null +++ b/packages/ai/src/api/google-generative-ai.lazy.ts @@ -0,0 +1,4 @@ +import type { ProviderStreams } from "../types.ts"; +import { lazyApi } from "./lazy.ts"; + +export const googleGenerativeAIApi = (): ProviderStreams => lazyApi(() => import("./google-generative-ai.ts")); diff --git a/packages/ai/src/providers/google.ts b/packages/ai/src/api/google-generative-ai.ts similarity index 97% rename from packages/ai/src/providers/google.ts rename to packages/ai/src/api/google-generative-ai.ts index d3f6f623..f959a6aa 100644 --- a/packages/ai/src/providers/google.ts +++ b/packages/ai/src/api/google-generative-ai.ts @@ -44,7 +44,7 @@ export interface GoogleOptions extends StreamOptions { // Counter for generating unique tool call IDs let toolCallCounter = 0; -export const streamGoogle: StreamFunction<"google-generative-ai", GoogleOptions> = ( +export const stream: StreamFunction<"google-generative-ai", GoogleOptions> = ( model: Model<"google-generative-ai">, context: Context, options?: GoogleOptions, @@ -277,7 +277,7 @@ export const streamGoogle: StreamFunction<"google-generative-ai", GoogleOptions> return stream; }; -export const streamSimpleGoogle: StreamFunction<"google-generative-ai", SimpleStreamOptions> = ( +export const streamSimple: StreamFunction<"google-generative-ai", SimpleStreamOptions> = ( model: Model<"google-generative-ai">, context: Context, options?: SimpleStreamOptions, @@ -289,7 +289,7 @@ export const streamSimpleGoogle: StreamFunction<"google-generative-ai", SimpleSt const base = buildBaseOptions(model, options, apiKey); if (!options?.reasoning) { - return streamGoogle(model, context, { ...base, thinking: { enabled: false } } satisfies GoogleOptions); + return stream(model, context, { ...base, thinking: { enabled: false } } satisfies GoogleOptions); } const clampedReasoning = clampThinkingLevel(model, options.reasoning); @@ -297,7 +297,7 @@ export const streamSimpleGoogle: StreamFunction<"google-generative-ai", SimpleSt const googleModel = model as Model<"google-generative-ai">; if (isGemini3ProModel(googleModel) || isGemini3FlashModel(googleModel) || isGemma4Model(googleModel)) { - return streamGoogle(model, context, { + return stream(model, context, { ...base, thinking: { enabled: true, @@ -306,7 +306,7 @@ export const streamSimpleGoogle: StreamFunction<"google-generative-ai", SimpleSt } satisfies GoogleOptions); } - return streamGoogle(model, context, { + return stream(model, context, { ...base, thinking: { enabled: true, diff --git a/packages/ai/src/providers/google-shared.ts b/packages/ai/src/api/google-shared.ts similarity index 100% rename from packages/ai/src/providers/google-shared.ts rename to packages/ai/src/api/google-shared.ts diff --git a/packages/ai/src/api/google-vertex.lazy.ts b/packages/ai/src/api/google-vertex.lazy.ts new file mode 100644 index 00000000..e79d4d0f --- /dev/null +++ b/packages/ai/src/api/google-vertex.lazy.ts @@ -0,0 +1,4 @@ +import type { ProviderStreams } from "../types.ts"; +import { lazyApi } from "./lazy.ts"; + +export const googleVertexApi = (): ProviderStreams => lazyApi(() => import("./google-vertex.ts")); diff --git a/packages/ai/src/providers/google-vertex.ts b/packages/ai/src/api/google-vertex.ts similarity index 98% rename from packages/ai/src/providers/google-vertex.ts rename to packages/ai/src/api/google-vertex.ts index 6feeabb6..1e122a88 100644 --- a/packages/ai/src/providers/google-vertex.ts +++ b/packages/ai/src/api/google-vertex.ts @@ -60,7 +60,7 @@ const THINKING_LEVEL_MAP: Record = { // Counter for generating unique tool call IDs let toolCallCounter = 0; -export const streamGoogleVertex: StreamFunction<"google-vertex", GoogleVertexOptions> = ( +export const stream: StreamFunction<"google-vertex", GoogleVertexOptions> = ( model: Model<"google-vertex">, context: Context, options?: GoogleVertexOptions, @@ -292,14 +292,14 @@ export const streamGoogleVertex: StreamFunction<"google-vertex", GoogleVertexOpt return stream; }; -export const streamSimpleGoogleVertex: StreamFunction<"google-vertex", SimpleStreamOptions> = ( +export const streamSimple: StreamFunction<"google-vertex", SimpleStreamOptions> = ( model: Model<"google-vertex">, context: Context, options?: SimpleStreamOptions, ): AssistantMessageEventStream => { const base = buildBaseOptions(model, options, undefined); if (!options?.reasoning) { - return streamGoogleVertex(model, context, { + return stream(model, context, { ...base, thinking: { enabled: false }, } satisfies GoogleVertexOptions); @@ -310,7 +310,7 @@ export const streamSimpleGoogleVertex: StreamFunction<"google-vertex", SimpleStr const geminiModel = model as unknown as Model<"google-generative-ai">; if (isGemini3ProModel(geminiModel) || isGemini3FlashModel(geminiModel)) { - return streamGoogleVertex(model, context, { + return stream(model, context, { ...base, thinking: { enabled: true, @@ -319,7 +319,7 @@ export const streamSimpleGoogleVertex: StreamFunction<"google-vertex", SimpleStr } satisfies GoogleVertexOptions); } - return streamGoogleVertex(model, context, { + return stream(model, context, { ...base, thinking: { enabled: true, diff --git a/packages/ai/src/api/lazy.ts b/packages/ai/src/api/lazy.ts index 8cfd2ede..fe1836ae 100644 --- a/packages/ai/src/api/lazy.ts +++ b/packages/ai/src/api/lazy.ts @@ -1,4 +1,4 @@ -import type { Api, AssistantMessage, AssistantMessageEvent, Model } from "../types.ts"; +import type { Api, AssistantMessage, AssistantMessageEvent, Model, ProviderStreams } from "../types.ts"; import { AssistantMessageEventStream } from "../utils/event-stream.ts"; function createSetupErrorMessage(model: Model, error: unknown): AssistantMessage { @@ -54,3 +54,17 @@ export function lazyStream( return outer; } + +/** + * Wraps a dynamically imported API implementation module as `ProviderStreams`. + * The module loads on first stream call; the host's import cache deduplicates + * loads. Load failures terminate the returned stream with an error event. + */ +export function lazyApi(load: () => Promise): ProviderStreams { + return { + stream: (model, context, options) => + lazyStream(model, async () => (await load()).stream(model, context, options)), + streamSimple: (model, context, options) => + lazyStream(model, async () => (await load()).streamSimple(model, context, options)), + }; +} diff --git a/packages/ai/src/api/mistral-conversations.lazy.ts b/packages/ai/src/api/mistral-conversations.lazy.ts new file mode 100644 index 00000000..84fd03ff --- /dev/null +++ b/packages/ai/src/api/mistral-conversations.lazy.ts @@ -0,0 +1,4 @@ +import type { ProviderStreams } from "../types.ts"; +import { lazyApi } from "./lazy.ts"; + +export const mistralConversationsApi = (): ProviderStreams => lazyApi(() => import("./mistral-conversations.ts")); diff --git a/packages/ai/src/providers/mistral.ts b/packages/ai/src/api/mistral-conversations.ts similarity index 98% rename from packages/ai/src/providers/mistral.ts rename to packages/ai/src/api/mistral-conversations.ts index 1bc7d4ce..b4873af7 100644 --- a/packages/ai/src/providers/mistral.ts +++ b/packages/ai/src/api/mistral-conversations.ts @@ -45,7 +45,7 @@ export interface MistralOptions extends StreamOptions { /** * Stream responses from Mistral using `chat.stream`. */ -export const streamMistral: StreamFunction<"mistral-conversations", MistralOptions> = ( +export const stream: StreamFunction<"mistral-conversations", MistralOptions> = ( model: Model<"mistral-conversations">, context: Context, options?: MistralOptions, @@ -107,7 +107,7 @@ export const streamMistral: StreamFunction<"mistral-conversations", MistralOptio /** * Maps provider-agnostic `SimpleStreamOptions` to Mistral options. */ -export const streamSimpleMistral: StreamFunction<"mistral-conversations", SimpleStreamOptions> = ( +export const streamSimple: StreamFunction<"mistral-conversations", SimpleStreamOptions> = ( model: Model<"mistral-conversations">, context: Context, options?: SimpleStreamOptions, @@ -122,7 +122,7 @@ export const streamSimpleMistral: StreamFunction<"mistral-conversations", Simple const reasoning = clampedReasoning === "off" ? undefined : clampedReasoning; const shouldUseReasoning = model.reasoning && reasoning !== undefined; - return streamMistral(model, context, { + return stream(model, context, { ...base, promptMode: shouldUseReasoning && usesPromptModeReasoning(model) ? "reasoning" : undefined, reasoningEffort: diff --git a/packages/ai/src/api/openai-codex-responses.lazy.ts b/packages/ai/src/api/openai-codex-responses.lazy.ts new file mode 100644 index 00000000..a8d0907d --- /dev/null +++ b/packages/ai/src/api/openai-codex-responses.lazy.ts @@ -0,0 +1,4 @@ +import type { ProviderStreams } from "../types.ts"; +import { lazyApi } from "./lazy.ts"; + +export const openAICodexResponsesApi = (): ProviderStreams => lazyApi(() => import("./openai-codex-responses.ts")); diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/api/openai-codex-responses.ts similarity index 99% rename from packages/ai/src/providers/openai-codex-responses.ts rename to packages/ai/src/api/openai-codex-responses.ts index 5836ee1b..fe7d8dc3 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/api/openai-codex-responses.ts @@ -191,7 +191,7 @@ function createSSEHeaderTimeout(): { signal: AbortSignal; clear: () => void; err // Main Stream Function // ============================================================================ -export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses", OpenAICodexResponsesOptions> = ( +export const stream: StreamFunction<"openai-codex-responses", OpenAICodexResponsesOptions> = ( model: Model<"openai-codex-responses">, context: Context, options?: OpenAICodexResponsesOptions, @@ -404,7 +404,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses" return stream; }; -export const streamSimpleOpenAICodexResponses: StreamFunction<"openai-codex-responses", SimpleStreamOptions> = ( +export const streamSimple: StreamFunction<"openai-codex-responses", SimpleStreamOptions> = ( model: Model<"openai-codex-responses">, context: Context, options?: SimpleStreamOptions, @@ -418,7 +418,7 @@ export const streamSimpleOpenAICodexResponses: StreamFunction<"openai-codex-resp const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined; const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning; - return streamOpenAICodexResponses(model, context, { + return stream(model, context, { ...base, reasoningEffort, } satisfies OpenAICodexResponsesOptions); diff --git a/packages/ai/src/api/openai-completions.lazy.ts b/packages/ai/src/api/openai-completions.lazy.ts new file mode 100644 index 00000000..6f6c6f61 --- /dev/null +++ b/packages/ai/src/api/openai-completions.lazy.ts @@ -0,0 +1,4 @@ +import type { ProviderStreams } from "../types.ts"; +import { lazyApi } from "./lazy.ts"; + +export const openAICompletionsApi = (): ProviderStreams => lazyApi(() => import("./openai-completions.ts")); diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/api/openai-completions.ts similarity index 99% rename from packages/ai/src/providers/openai-completions.ts rename to packages/ai/src/api/openai-completions.ts index 0f87c897..41f4c7a8 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/api/openai-completions.ts @@ -108,7 +108,7 @@ function resolveCacheRetention(cacheRetention?: CacheRetention): CacheRetention return "short"; } -export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenAICompletionsOptions> = ( +export const stream: StreamFunction<"openai-completions", OpenAICompletionsOptions> = ( model: Model<"openai-completions">, context: Context, options?: OpenAICompletionsOptions, @@ -425,7 +425,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA return stream; }; -export const streamSimpleOpenAICompletions: StreamFunction<"openai-completions", SimpleStreamOptions> = ( +export const streamSimple: StreamFunction<"openai-completions", SimpleStreamOptions> = ( model: Model<"openai-completions">, context: Context, options?: SimpleStreamOptions, @@ -440,7 +440,7 @@ export const streamSimpleOpenAICompletions: StreamFunction<"openai-completions", const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning; const toolChoice = (options as OpenAICompletionsOptions | undefined)?.toolChoice; - return streamOpenAICompletions(model, context, { + return stream(model, context, { ...base, reasoningEffort, toolChoice, diff --git a/packages/ai/src/providers/openai-prompt-cache.ts b/packages/ai/src/api/openai-prompt-cache.ts similarity index 100% rename from packages/ai/src/providers/openai-prompt-cache.ts rename to packages/ai/src/api/openai-prompt-cache.ts diff --git a/packages/ai/src/providers/openai-responses-shared.ts b/packages/ai/src/api/openai-responses-shared.ts similarity index 100% rename from packages/ai/src/providers/openai-responses-shared.ts rename to packages/ai/src/api/openai-responses-shared.ts diff --git a/packages/ai/src/api/openai-responses.lazy.ts b/packages/ai/src/api/openai-responses.lazy.ts new file mode 100644 index 00000000..066ca801 --- /dev/null +++ b/packages/ai/src/api/openai-responses.lazy.ts @@ -0,0 +1,4 @@ +import type { ProviderStreams } from "../types.ts"; +import { lazyApi } from "./lazy.ts"; + +export const openAIResponsesApi = (): ProviderStreams => lazyApi(() => import("./openai-responses.ts")); diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/api/openai-responses.ts similarity index 97% rename from packages/ai/src/providers/openai-responses.ts rename to packages/ai/src/api/openai-responses.ts index 9afeff29..dfd15866 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/api/openai-responses.ts @@ -78,7 +78,7 @@ export interface OpenAIResponsesOptions extends StreamOptions { /** * Generate function for OpenAI Responses API */ -export const streamOpenAIResponses: StreamFunction<"openai-responses", OpenAIResponsesOptions> = ( +export const stream: StreamFunction<"openai-responses", OpenAIResponsesOptions> = ( model: Model<"openai-responses">, context: Context, options?: OpenAIResponsesOptions, @@ -159,7 +159,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses", OpenAIRes return stream; }; -export const streamSimpleOpenAIResponses: StreamFunction<"openai-responses", SimpleStreamOptions> = ( +export const streamSimple: StreamFunction<"openai-responses", SimpleStreamOptions> = ( model: Model<"openai-responses">, context: Context, options?: SimpleStreamOptions, @@ -173,7 +173,7 @@ export const streamSimpleOpenAIResponses: StreamFunction<"openai-responses", Sim const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined; const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning; - return streamOpenAIResponses(model, context, { + return stream(model, context, { ...base, reasoningEffort, } satisfies OpenAIResponsesOptions); diff --git a/packages/ai/src/providers/simple-options.ts b/packages/ai/src/api/simple-options.ts similarity index 100% rename from packages/ai/src/providers/simple-options.ts rename to packages/ai/src/api/simple-options.ts diff --git a/packages/ai/src/providers/transform-messages.ts b/packages/ai/src/api/transform-messages.ts similarity index 100% rename from packages/ai/src/providers/transform-messages.ts rename to packages/ai/src/api/transform-messages.ts diff --git a/packages/ai/src/bedrock-provider.ts b/packages/ai/src/bedrock-provider.ts index cf08b33e..10430092 100644 --- a/packages/ai/src/bedrock-provider.ts +++ b/packages/ai/src/bedrock-provider.ts @@ -1,6 +1,6 @@ -import { streamBedrock, streamSimpleBedrock } from "./providers/amazon-bedrock.ts"; +import { stream, streamSimple } from "./api/bedrock-converse-stream.ts"; export const bedrockProviderModule = { - streamBedrock, - streamSimpleBedrock, + stream, + streamSimple, }; diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index dd584249..f98a7616 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -1,7 +1,26 @@ export type { Static, TSchema } from "typebox"; export { Type } from "typebox"; +export * from "./api/anthropic-messages.lazy.ts"; +export type { AnthropicEffort, AnthropicOptions, AnthropicThinkingDisplay } from "./api/anthropic-messages.ts"; +export * from "./api/azure-openai-responses.lazy.ts"; +export type { AzureOpenAIResponsesOptions } from "./api/azure-openai-responses.ts"; +export * from "./api/bedrock-converse-stream.lazy.ts"; +export type { BedrockOptions, BedrockThinkingDisplay } from "./api/bedrock-converse-stream.ts"; +export * from "./api/google-generative-ai.lazy.ts"; +export type { GoogleOptions } from "./api/google-generative-ai.ts"; +export type { GoogleThinkingLevel } from "./api/google-shared.ts"; +export * from "./api/google-vertex.lazy.ts"; +export type { GoogleVertexOptions } from "./api/google-vertex.ts"; export * from "./api/lazy.ts"; +export * from "./api/mistral-conversations.lazy.ts"; +export type { MistralOptions } from "./api/mistral-conversations.ts"; +export * from "./api/openai-codex-responses.lazy.ts"; +export type { OpenAICodexResponsesOptions, OpenAICodexWebSocketDebugStats } from "./api/openai-codex-responses.ts"; +export * from "./api/openai-completions.lazy.ts"; +export type { OpenAICompletionsOptions } from "./api/openai-completions.ts"; +export * from "./api/openai-responses.lazy.ts"; +export type { OpenAIResponsesOptions } from "./api/openai-responses.ts"; export * from "./api-registry.ts"; export * from "./auth/context.ts"; export * from "./auth/credential-store.ts"; @@ -11,22 +30,8 @@ export * from "./image-models.ts"; export * from "./images.ts"; export * from "./images-api-registry.ts"; export * from "./models.ts"; -export type { BedrockOptions, BedrockThinkingDisplay } from "./providers/amazon-bedrock.ts"; -export type { AnthropicEffort, AnthropicOptions, AnthropicThinkingDisplay } from "./providers/anthropic.ts"; -export type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses.ts"; export * from "./providers/faux.ts"; -export type { GoogleOptions } from "./providers/google.ts"; -export type { GoogleThinkingLevel } from "./providers/google-shared.ts"; -export type { GoogleVertexOptions } from "./providers/google-vertex.ts"; export * from "./providers/images/register-builtins.ts"; -export type { MistralOptions } from "./providers/mistral.ts"; -export type { - OpenAICodexResponsesOptions, - OpenAICodexWebSocketDebugStats, -} from "./providers/openai-codex-responses.ts"; -export type { OpenAICompletionsOptions } from "./providers/openai-completions.ts"; -export type { OpenAIResponsesOptions } from "./providers/openai-responses.ts"; -export * from "./providers/register-builtins.ts"; export * from "./session-resources.ts"; export * from "./stream.ts"; export * from "./types.ts"; diff --git a/packages/ai/src/providers/register-builtins.ts b/packages/ai/src/providers/register-builtins.ts deleted file mode 100644 index 8fdcaaf0..00000000 --- a/packages/ai/src/providers/register-builtins.ts +++ /dev/null @@ -1,406 +0,0 @@ -import { clearApiProviders, registerApiProvider } from "../api-registry.ts"; -import type { - Api, - AssistantMessage, - AssistantMessageEvent, - Context, - Model, - SimpleStreamOptions, - StreamFunction, - StreamOptions, -} from "../types.ts"; -import { AssistantMessageEventStream } from "../utils/event-stream.ts"; -import type { BedrockOptions } from "./amazon-bedrock.ts"; -import type { AnthropicOptions } from "./anthropic.ts"; -import type { AzureOpenAIResponsesOptions } from "./azure-openai-responses.ts"; -import type { GoogleOptions } from "./google.ts"; -import type { GoogleVertexOptions } from "./google-vertex.ts"; -import type { MistralOptions } from "./mistral.ts"; -import type { OpenAICodexResponsesOptions } from "./openai-codex-responses.ts"; -import type { OpenAICompletionsOptions } from "./openai-completions.ts"; -import type { OpenAIResponsesOptions } from "./openai-responses.ts"; - -interface LazyProviderModule< - TApi extends Api, - TOptions extends StreamOptions, - TSimpleOptions extends SimpleStreamOptions, -> { - stream: (model: Model, context: Context, options?: TOptions) => AsyncIterable; - streamSimple: ( - model: Model, - context: Context, - options?: TSimpleOptions, - ) => AsyncIterable; -} - -interface AnthropicProviderModule { - streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOptions>; - streamSimpleAnthropic: StreamFunction<"anthropic-messages", SimpleStreamOptions>; -} - -interface AzureOpenAIResponsesProviderModule { - streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses", AzureOpenAIResponsesOptions>; - streamSimpleAzureOpenAIResponses: StreamFunction<"azure-openai-responses", SimpleStreamOptions>; -} - -interface GoogleProviderModule { - streamGoogle: StreamFunction<"google-generative-ai", GoogleOptions>; - streamSimpleGoogle: StreamFunction<"google-generative-ai", SimpleStreamOptions>; -} - -interface GoogleVertexProviderModule { - streamGoogleVertex: StreamFunction<"google-vertex", GoogleVertexOptions>; - streamSimpleGoogleVertex: StreamFunction<"google-vertex", SimpleStreamOptions>; -} - -interface MistralProviderModule { - streamMistral: StreamFunction<"mistral-conversations", MistralOptions>; - streamSimpleMistral: StreamFunction<"mistral-conversations", SimpleStreamOptions>; -} - -interface OpenAICodexResponsesProviderModule { - streamOpenAICodexResponses: StreamFunction<"openai-codex-responses", OpenAICodexResponsesOptions>; - streamSimpleOpenAICodexResponses: StreamFunction<"openai-codex-responses", SimpleStreamOptions>; -} - -interface OpenAICompletionsProviderModule { - streamOpenAICompletions: StreamFunction<"openai-completions", OpenAICompletionsOptions>; - streamSimpleOpenAICompletions: StreamFunction<"openai-completions", SimpleStreamOptions>; -} - -interface OpenAIResponsesProviderModule { - streamOpenAIResponses: StreamFunction<"openai-responses", OpenAIResponsesOptions>; - streamSimpleOpenAIResponses: StreamFunction<"openai-responses", SimpleStreamOptions>; -} - -interface BedrockProviderModule { - streamBedrock: ( - model: Model<"bedrock-converse-stream">, - context: Context, - options?: BedrockOptions, - ) => AsyncIterable; - streamSimpleBedrock: ( - model: Model<"bedrock-converse-stream">, - context: Context, - options?: SimpleStreamOptions, - ) => AsyncIterable; -} - -const importNodeOnlyProvider = (specifier: string): Promise => { - const runtimeSpecifier = import.meta.url.endsWith(".js") ? specifier.replace(/\.ts$/, ".js") : specifier; - return import(runtimeSpecifier); -}; - -let anthropicProviderModulePromise: - | Promise> - | undefined; -let azureOpenAIResponsesProviderModulePromise: - | Promise> - | undefined; -let googleProviderModulePromise: - | Promise> - | undefined; -let googleVertexProviderModulePromise: - | Promise> - | undefined; -let mistralProviderModulePromise: - | Promise> - | undefined; -let openAICodexResponsesProviderModulePromise: - | Promise> - | undefined; -let openAICompletionsProviderModulePromise: - | Promise> - | undefined; -let openAIResponsesProviderModulePromise: - | Promise> - | undefined; -let bedrockProviderModuleOverride: - | LazyProviderModule<"bedrock-converse-stream", BedrockOptions, SimpleStreamOptions> - | undefined; -let bedrockProviderModulePromise: - | Promise> - | undefined; - -export function setBedrockProviderModule(module: BedrockProviderModule): void { - bedrockProviderModuleOverride = { - stream: module.streamBedrock, - streamSimple: module.streamSimpleBedrock, - }; -} - -function forwardStream(target: AssistantMessageEventStream, source: AsyncIterable): void { - (async () => { - for await (const event of source) { - target.push(event); - } - target.end(); - })(); -} - -function createLazyLoadErrorMessage(model: Model, error: unknown): AssistantMessage { - return { - role: "assistant", - content: [], - api: model.api, - provider: model.provider, - model: model.id, - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - stopReason: "error", - errorMessage: error instanceof Error ? error.message : String(error), - timestamp: Date.now(), - }; -} - -function createLazyStream( - loadModule: () => Promise>, -): StreamFunction { - return (model, context, options) => { - const outer = new AssistantMessageEventStream(); - - loadModule() - .then((module) => { - const inner = module.stream(model, context, options); - forwardStream(outer, inner); - }) - .catch((error) => { - const message = createLazyLoadErrorMessage(model, error); - outer.push({ type: "error", reason: "error", error: message }); - outer.end(message); - }); - - return outer; - }; -} - -function createLazySimpleStream< - TApi extends Api, - TOptions extends StreamOptions, - TSimpleOptions extends SimpleStreamOptions, ->(loadModule: () => Promise>): StreamFunction { - return (model, context, options) => { - const outer = new AssistantMessageEventStream(); - - loadModule() - .then((module) => { - const inner = module.streamSimple(model, context, options); - forwardStream(outer, inner); - }) - .catch((error) => { - const message = createLazyLoadErrorMessage(model, error); - outer.push({ type: "error", reason: "error", error: message }); - outer.end(message); - }); - - return outer; - }; -} - -function loadAnthropicProviderModule(): Promise< - LazyProviderModule<"anthropic-messages", AnthropicOptions, SimpleStreamOptions> -> { - anthropicProviderModulePromise ||= import("./anthropic.ts").then((module) => { - const provider = module as AnthropicProviderModule; - return { - stream: provider.streamAnthropic, - streamSimple: provider.streamSimpleAnthropic, - }; - }); - return anthropicProviderModulePromise; -} - -function loadAzureOpenAIResponsesProviderModule(): Promise< - LazyProviderModule<"azure-openai-responses", AzureOpenAIResponsesOptions, SimpleStreamOptions> -> { - azureOpenAIResponsesProviderModulePromise ||= import("./azure-openai-responses.ts").then((module) => { - const provider = module as AzureOpenAIResponsesProviderModule; - return { - stream: provider.streamAzureOpenAIResponses, - streamSimple: provider.streamSimpleAzureOpenAIResponses, - }; - }); - return azureOpenAIResponsesProviderModulePromise; -} - -function loadGoogleProviderModule(): Promise< - LazyProviderModule<"google-generative-ai", GoogleOptions, SimpleStreamOptions> -> { - googleProviderModulePromise ||= import("./google.ts").then((module) => { - const provider = module as GoogleProviderModule; - return { - stream: provider.streamGoogle, - streamSimple: provider.streamSimpleGoogle, - }; - }); - return googleProviderModulePromise; -} - -function loadGoogleVertexProviderModule(): Promise< - LazyProviderModule<"google-vertex", GoogleVertexOptions, SimpleStreamOptions> -> { - googleVertexProviderModulePromise ||= import("./google-vertex.ts").then((module) => { - const provider = module as GoogleVertexProviderModule; - return { - stream: provider.streamGoogleVertex, - streamSimple: provider.streamSimpleGoogleVertex, - }; - }); - return googleVertexProviderModulePromise; -} - -function loadMistralProviderModule(): Promise< - LazyProviderModule<"mistral-conversations", MistralOptions, SimpleStreamOptions> -> { - mistralProviderModulePromise ||= import("./mistral.ts").then((module) => { - const provider = module as MistralProviderModule; - return { - stream: provider.streamMistral, - streamSimple: provider.streamSimpleMistral, - }; - }); - return mistralProviderModulePromise; -} - -function loadOpenAICodexResponsesProviderModule(): Promise< - LazyProviderModule<"openai-codex-responses", OpenAICodexResponsesOptions, SimpleStreamOptions> -> { - openAICodexResponsesProviderModulePromise ||= import("./openai-codex-responses.ts").then((module) => { - const provider = module as OpenAICodexResponsesProviderModule; - return { - stream: provider.streamOpenAICodexResponses, - streamSimple: provider.streamSimpleOpenAICodexResponses, - }; - }); - return openAICodexResponsesProviderModulePromise; -} - -function loadOpenAICompletionsProviderModule(): Promise< - LazyProviderModule<"openai-completions", OpenAICompletionsOptions, SimpleStreamOptions> -> { - openAICompletionsProviderModulePromise ||= import("./openai-completions.ts").then((module) => { - const provider = module as OpenAICompletionsProviderModule; - return { - stream: provider.streamOpenAICompletions, - streamSimple: provider.streamSimpleOpenAICompletions, - }; - }); - return openAICompletionsProviderModulePromise; -} - -function loadOpenAIResponsesProviderModule(): Promise< - LazyProviderModule<"openai-responses", OpenAIResponsesOptions, SimpleStreamOptions> -> { - openAIResponsesProviderModulePromise ||= import("./openai-responses.ts").then((module) => { - const provider = module as OpenAIResponsesProviderModule; - return { - stream: provider.streamOpenAIResponses, - streamSimple: provider.streamSimpleOpenAIResponses, - }; - }); - return openAIResponsesProviderModulePromise; -} - -function loadBedrockProviderModule(): Promise< - LazyProviderModule<"bedrock-converse-stream", BedrockOptions, SimpleStreamOptions> -> { - if (bedrockProviderModuleOverride) { - return Promise.resolve(bedrockProviderModuleOverride); - } - bedrockProviderModulePromise ||= importNodeOnlyProvider("./amazon-bedrock.ts").then((module) => { - const provider = module as BedrockProviderModule; - return { - stream: provider.streamBedrock, - streamSimple: provider.streamSimpleBedrock, - }; - }); - return bedrockProviderModulePromise; -} - -export const streamAnthropic = createLazyStream(loadAnthropicProviderModule); -export const streamSimpleAnthropic = createLazySimpleStream(loadAnthropicProviderModule); -export const streamAzureOpenAIResponses = createLazyStream(loadAzureOpenAIResponsesProviderModule); -export const streamSimpleAzureOpenAIResponses = createLazySimpleStream(loadAzureOpenAIResponsesProviderModule); -export const streamGoogle = createLazyStream(loadGoogleProviderModule); -export const streamSimpleGoogle = createLazySimpleStream(loadGoogleProviderModule); -export const streamGoogleVertex = createLazyStream(loadGoogleVertexProviderModule); -export const streamSimpleGoogleVertex = createLazySimpleStream(loadGoogleVertexProviderModule); -export const streamMistral = createLazyStream(loadMistralProviderModule); -export const streamSimpleMistral = createLazySimpleStream(loadMistralProviderModule); -export const streamOpenAICodexResponses = createLazyStream(loadOpenAICodexResponsesProviderModule); -export const streamSimpleOpenAICodexResponses = createLazySimpleStream(loadOpenAICodexResponsesProviderModule); -export const streamOpenAICompletions = createLazyStream(loadOpenAICompletionsProviderModule); -export const streamSimpleOpenAICompletions = createLazySimpleStream(loadOpenAICompletionsProviderModule); -export const streamOpenAIResponses = createLazyStream(loadOpenAIResponsesProviderModule); -export const streamSimpleOpenAIResponses = createLazySimpleStream(loadOpenAIResponsesProviderModule); -const streamBedrockLazy = createLazyStream(loadBedrockProviderModule); -const streamSimpleBedrockLazy = createLazySimpleStream(loadBedrockProviderModule); - -export function registerBuiltInApiProviders(): void { - registerApiProvider({ - api: "anthropic-messages", - stream: streamAnthropic, - streamSimple: streamSimpleAnthropic, - }); - - registerApiProvider({ - api: "openai-completions", - stream: streamOpenAICompletions, - streamSimple: streamSimpleOpenAICompletions, - }); - - registerApiProvider({ - api: "mistral-conversations", - stream: streamMistral, - streamSimple: streamSimpleMistral, - }); - - registerApiProvider({ - api: "openai-responses", - stream: streamOpenAIResponses, - streamSimple: streamSimpleOpenAIResponses, - }); - - registerApiProvider({ - api: "azure-openai-responses", - stream: streamAzureOpenAIResponses, - streamSimple: streamSimpleAzureOpenAIResponses, - }); - - registerApiProvider({ - api: "openai-codex-responses", - stream: streamOpenAICodexResponses, - streamSimple: streamSimpleOpenAICodexResponses, - }); - - registerApiProvider({ - api: "google-generative-ai", - stream: streamGoogle, - streamSimple: streamSimpleGoogle, - }); - - registerApiProvider({ - api: "google-vertex", - stream: streamGoogleVertex, - streamSimple: streamSimpleGoogleVertex, - }); - - registerApiProvider({ - api: "bedrock-converse-stream", - stream: streamBedrockLazy, - streamSimple: streamSimpleBedrockLazy, - }); -} - -export function resetApiProviders(): void { - clearApiProviders(); - registerBuiltInApiProviders(); -} - -registerBuiltInApiProviders(); diff --git a/packages/ai/src/stream.ts b/packages/ai/src/stream.ts index 3ae631a1..f874f03d 100644 --- a/packages/ai/src/stream.ts +++ b/packages/ai/src/stream.ts @@ -1,6 +1,13 @@ -import "./providers/register-builtins.ts"; - -import { getApiProvider } from "./api-registry.ts"; +import { anthropicMessagesApi } from "./api/anthropic-messages.lazy.ts"; +import { azureOpenAIResponsesApi } from "./api/azure-openai-responses.lazy.ts"; +import { bedrockConverseStreamApi } from "./api/bedrock-converse-stream.lazy.ts"; +import { googleGenerativeAIApi } from "./api/google-generative-ai.lazy.ts"; +import { googleVertexApi } from "./api/google-vertex.lazy.ts"; +import { mistralConversationsApi } from "./api/mistral-conversations.lazy.ts"; +import { openAICodexResponsesApi } from "./api/openai-codex-responses.lazy.ts"; +import { openAICompletionsApi } from "./api/openai-completions.lazy.ts"; +import { openAIResponsesApi } from "./api/openai-responses.lazy.ts"; +import { clearApiProviders, getApiProvider, registerApiProvider } from "./api-registry.ts"; import { getEnvApiKey } from "./env-api-keys.ts"; import type { Api, @@ -9,12 +16,38 @@ import type { Context, Model, ProviderStreamOptions, + ProviderStreams, SimpleStreamOptions, StreamOptions, } from "./types.ts"; export { getEnvApiKey } from "./env-api-keys.ts"; +const BUILTIN_APIS: [Api, ProviderStreams][] = [ + ["anthropic-messages", anthropicMessagesApi()], + ["openai-completions", openAICompletionsApi()], + ["openai-responses", openAIResponsesApi()], + ["openai-codex-responses", openAICodexResponsesApi()], + ["azure-openai-responses", azureOpenAIResponsesApi()], + ["google-generative-ai", googleGenerativeAIApi()], + ["google-vertex", googleVertexApi()], + ["mistral-conversations", mistralConversationsApi()], + ["bedrock-converse-stream", bedrockConverseStreamApi()], +]; + +export function registerBuiltInApiProviders(): void { + for (const [api, streams] of BUILTIN_APIS) { + registerApiProvider({ api, stream: streams.stream, streamSimple: streams.streamSimple }); + } +} + +export function resetApiProviders(): void { + clearApiProviders(); + registerBuiltInApiProviders(); +} + +registerBuiltInApiProviders(); + function hasExplicitApiKey(apiKey: string | undefined): apiKey is string { return typeof apiKey === "string" && apiKey.trim().length > 0; } diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index 4d7b4738..2bce49bc 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -1,12 +1,12 @@ -import type { BedrockOptions } from "./providers/amazon-bedrock.ts"; -import type { AnthropicOptions } from "./providers/anthropic.ts"; -import type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses.ts"; -import type { GoogleOptions } from "./providers/google.ts"; -import type { GoogleVertexOptions } from "./providers/google-vertex.ts"; -import type { MistralOptions } from "./providers/mistral.ts"; -import type { OpenAICodexResponsesOptions } from "./providers/openai-codex-responses.ts"; -import type { OpenAICompletionsOptions } from "./providers/openai-completions.ts"; -import type { OpenAIResponsesOptions } from "./providers/openai-responses.ts"; +import type { AnthropicOptions } from "./api/anthropic-messages.ts"; +import type { AzureOpenAIResponsesOptions } from "./api/azure-openai-responses.ts"; +import type { BedrockOptions } from "./api/bedrock-converse-stream.ts"; +import type { GoogleOptions } from "./api/google-generative-ai.ts"; +import type { GoogleVertexOptions } from "./api/google-vertex.ts"; +import type { MistralOptions } from "./api/mistral-conversations.ts"; +import type { OpenAICodexResponsesOptions } from "./api/openai-codex-responses.ts"; +import type { OpenAICompletionsOptions } from "./api/openai-completions.ts"; +import type { OpenAIResponsesOptions } from "./api/openai-responses.ts"; import type { AssistantMessageDiagnostic } from "./utils/diagnostics.ts"; import type { AssistantMessageEventStream } from "./utils/event-stream.ts"; @@ -191,6 +191,19 @@ export type ApiStreamOptions = TApi extends keyof ApiOptionsMa ? ApiOptionsMap[TApi] : StreamOptions & Record; +/** + * The uniform stream contract of an API implementation module: every module + * under `src/api/` exports exactly `stream` and `streamSimple`, so the module + * itself satisfies this interface. Lazy wrappers (`lazyApi()`) and provider + * factories pass these around as values. This is the untyped dispatch shape; + * per-API option typing lives on the implementation modules themselves and on + * `Provider.stream()` via `ApiStreamOptions`. + */ +export interface ProviderStreams { + stream(model: Model, context: Context, options?: StreamOptions): AssistantMessageEventStream; + streamSimple(model: Model, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream; +} + export interface ImagesOptions { signal?: AbortSignal; apiKey?: string; diff --git a/packages/ai/test/anthropic-eager-tool-input-compat.test.ts b/packages/ai/test/anthropic-eager-tool-input-compat.test.ts index 22ac0e59..c53c9d6b 100644 --- a/packages/ai/test/anthropic-eager-tool-input-compat.test.ts +++ b/packages/ai/test/anthropic-eager-tool-input-compat.test.ts @@ -2,7 +2,7 @@ import { createServer, type IncomingMessage, type ServerResponse } from "node:ht import type { AddressInfo } from "node:net"; import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { streamAnthropic } from "../src/providers/anthropic.ts"; +import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts"; import type { Context, Model, Tool } from "../src/types.ts"; interface CapturedRequest { diff --git a/packages/ai/test/anthropic-sse-parsing.test.ts b/packages/ai/test/anthropic-sse-parsing.test.ts index 03024966..88943582 100644 --- a/packages/ai/test/anthropic-sse-parsing.test.ts +++ b/packages/ai/test/anthropic-sse-parsing.test.ts @@ -1,8 +1,8 @@ import type Anthropic from "@anthropic-ai/sdk"; import { Type } from "typebox"; import { describe, expect, it } from "vitest"; +import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts"; import { getModel } from "../src/models.ts"; -import { streamAnthropic } from "../src/providers/anthropic.ts"; import type { Context, ToolCall } from "../src/types.ts"; function createSseResponse(events: Array<{ event: string; data: string }>): Response { diff --git a/packages/ai/test/azure-openai-base-url.test.ts b/packages/ai/test/azure-openai-base-url.test.ts index 15b8a528..b80bae58 100644 --- a/packages/ai/test/azure-openai-base-url.test.ts +++ b/packages/ai/test/azure-openai-base-url.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { stream as streamAzureOpenAIResponses } from "../src/api/azure-openai-responses.ts"; import { getModel } from "../src/models.ts"; -import { streamAzureOpenAIResponses } from "../src/providers/azure-openai-responses.ts"; import type { Context } from "../src/types.ts"; interface CapturedAzureClientOptions { diff --git a/packages/ai/test/bedrock-convert-messages.test.ts b/packages/ai/test/bedrock-convert-messages.test.ts index d74dedae..ba3ff137 100644 --- a/packages/ai/test/bedrock-convert-messages.test.ts +++ b/packages/ai/test/bedrock-convert-messages.test.ts @@ -44,8 +44,8 @@ vi.mock("@aws-sdk/client-bedrock-runtime", () => { }; }); +import { stream as streamBedrock } from "../src/api/bedrock-converse-stream.ts"; import { getModel } from "../src/models.ts"; -import { streamBedrock } from "../src/providers/amazon-bedrock.ts"; import type { Context, Message } from "../src/types.ts"; const baseModel = getModel("amazon-bedrock", "us.anthropic.claude-sonnet-4-5-20250929-v1:0"); diff --git a/packages/ai/test/bedrock-custom-headers.test.ts b/packages/ai/test/bedrock-custom-headers.test.ts index 1017d089..e0899068 100644 --- a/packages/ai/test/bedrock-custom-headers.test.ts +++ b/packages/ai/test/bedrock-custom-headers.test.ts @@ -51,9 +51,9 @@ vi.mock("@aws-sdk/client-bedrock-runtime", () => { }; }); +import type { BedrockOptions } from "../src/api/bedrock-converse-stream.ts"; +import { stream as streamBedrock, streamSimple as streamSimpleBedrock } from "../src/api/bedrock-converse-stream.ts"; import { getModel } from "../src/models.ts"; -import type { BedrockOptions } from "../src/providers/amazon-bedrock.ts"; -import { streamBedrock, streamSimpleBedrock } from "../src/providers/amazon-bedrock.ts"; import type { Context, Model } from "../src/types.ts"; const context: Context = { diff --git a/packages/ai/test/bedrock-endpoint-resolution.test.ts b/packages/ai/test/bedrock-endpoint-resolution.test.ts index 412c62e0..28221d5c 100644 --- a/packages/ai/test/bedrock-endpoint-resolution.test.ts +++ b/packages/ai/test/bedrock-endpoint-resolution.test.ts @@ -44,8 +44,8 @@ vi.mock("@aws-sdk/client-bedrock-runtime", () => { }; }); +import { stream as streamBedrock } from "../src/api/bedrock-converse-stream.ts"; import { getModel } from "../src/models.ts"; -import { streamBedrock } from "../src/providers/amazon-bedrock.ts"; import type { Context, Model } from "../src/types.ts"; const context: Context = { diff --git a/packages/ai/test/bedrock-thinking-payload.test.ts b/packages/ai/test/bedrock-thinking-payload.test.ts index 8f4e06e7..ff4509cb 100644 --- a/packages/ai/test/bedrock-thinking-payload.test.ts +++ b/packages/ai/test/bedrock-thinking-payload.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { type BedrockOptions, stream as streamBedrock } from "../src/api/bedrock-converse-stream.ts"; import { getModel } from "../src/models.ts"; -import { type BedrockOptions, streamBedrock } from "../src/providers/amazon-bedrock.ts"; import type { Context, Model } from "../src/types.ts"; import { hasBedrockCredentials } from "./bedrock-utils.ts"; diff --git a/packages/ai/test/cache-retention.test.ts b/packages/ai/test/cache-retention.test.ts index 8828ba86..b3745198 100644 --- a/packages/ai/test/cache-retention.test.ts +++ b/packages/ai/test/cache-retention.test.ts @@ -1,8 +1,8 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts"; +import { stream as streamOpenAICompletions } from "../src/api/openai-completions.ts"; +import { stream as streamOpenAIResponses } from "../src/api/openai-responses.ts"; import { getModel } from "../src/models.ts"; -import { streamAnthropic } from "../src/providers/anthropic.ts"; -import { streamOpenAICompletions } from "../src/providers/openai-completions.ts"; -import { streamOpenAIResponses } from "../src/providers/openai-responses.ts"; import { stream } from "../src/stream.ts"; import type { Context, Model } from "../src/types.ts"; diff --git a/packages/ai/test/codex-websocket-cached-probe.ts b/packages/ai/test/codex-websocket-cached-probe.ts index 7317035c..ca08c6c7 100644 --- a/packages/ai/test/codex-websocket-cached-probe.ts +++ b/packages/ai/test/codex-websocket-cached-probe.ts @@ -10,13 +10,13 @@ import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { Type } from "typebox"; import { AuthStorage } from "../../coding-agent/src/core/auth-storage.ts"; -import { getModel } from "../src/models.ts"; import { closeOpenAICodexWebSocketSessions, getOpenAICodexWebSocketDebugStats, resetOpenAICodexWebSocketDebugStats, - streamOpenAICodexResponses, -} from "../src/providers/openai-codex-responses.ts"; + stream as streamOpenAICodexResponses, +} from "../src/api/openai-codex-responses.ts"; +import { getModel } from "../src/models.ts"; import type { AssistantMessage, Context, Message, Model, Tool, ToolResultMessage, Transport } from "../src/types.ts"; type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh"; diff --git a/packages/ai/test/fireworks-models.test.ts b/packages/ai/test/fireworks-models.test.ts index 8b291b89..0f7ac4c1 100644 --- a/packages/ai/test/fireworks-models.test.ts +++ b/packages/ai/test/fireworks-models.test.ts @@ -2,9 +2,9 @@ import { createServer, type IncomingMessage, type ServerResponse } from "node:ht import type { AddressInfo } from "node:net"; import { Type } from "typebox"; import { afterEach, describe, expect, it } from "vitest"; +import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts"; import { findEnvKeys, getEnvApiKey } from "../src/env-api-keys.ts"; import { getModel, getModels } from "../src/models.ts"; -import { streamAnthropic } from "../src/providers/anthropic.ts"; import type { Context, Model, Tool } from "../src/types.ts"; const originalFireworksApiKey = process.env.FIREWORKS_API_KEY; diff --git a/packages/ai/test/github-copilot-anthropic.test.ts b/packages/ai/test/github-copilot-anthropic.test.ts index ee3de3e8..c1c1a30f 100644 --- a/packages/ai/test/github-copilot-anthropic.test.ts +++ b/packages/ai/test/github-copilot-anthropic.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; +import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts"; import { getModel } from "../src/models.ts"; -import { streamAnthropic } from "../src/providers/anthropic.ts"; import type { Context } from "../src/types.ts"; const mockState = vi.hoisted(() => ({ diff --git a/packages/ai/test/google-shared-convert-tools.test.ts b/packages/ai/test/google-shared-convert-tools.test.ts index c7b10b41..d91bcfa0 100644 --- a/packages/ai/test/google-shared-convert-tools.test.ts +++ b/packages/ai/test/google-shared-convert-tools.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { convertTools } from "../src/providers/google-shared.ts"; +import { convertTools } from "../src/api/google-shared.ts"; import type { Tool } from "../src/types.ts"; function makeTool(parameters: Record): Tool { diff --git a/packages/ai/test/google-shared-gemini3-unsigned-tool-call.test.ts b/packages/ai/test/google-shared-gemini3-unsigned-tool-call.test.ts index 920c6a4e..406c5a37 100644 --- a/packages/ai/test/google-shared-gemini3-unsigned-tool-call.test.ts +++ b/packages/ai/test/google-shared-gemini3-unsigned-tool-call.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { convertMessages } from "../src/providers/google-shared.ts"; +import { convertMessages } from "../src/api/google-shared.ts"; import type { Context, Model } from "../src/types.ts"; function makeGemini3Model( diff --git a/packages/ai/test/google-shared-image-tool-result-routing.test.ts b/packages/ai/test/google-shared-image-tool-result-routing.test.ts index 1430a084..8ba5660c 100644 --- a/packages/ai/test/google-shared-image-tool-result-routing.test.ts +++ b/packages/ai/test/google-shared-image-tool-result-routing.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { convertMessages } from "../src/providers/google-shared.ts"; +import { convertMessages } from "../src/api/google-shared.ts"; import type { Context, Model } from "../src/types.ts"; function makeModel( diff --git a/packages/ai/test/google-thinking-signature.test.ts b/packages/ai/test/google-thinking-signature.test.ts index 83b17ccc..853a6a60 100644 --- a/packages/ai/test/google-thinking-signature.test.ts +++ b/packages/ai/test/google-thinking-signature.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { isThinkingPart, retainThoughtSignature } from "../src/providers/google-shared.ts"; +import { isThinkingPart, retainThoughtSignature } from "../src/api/google-shared.ts"; describe("Google thinking detection (thoughtSignature)", () => { it("treats part.thought === true as thinking", () => { diff --git a/packages/ai/test/google-vertex-api-key-resolution.test.ts b/packages/ai/test/google-vertex-api-key-resolution.test.ts index 5f66649c..7fa182f9 100644 --- a/packages/ai/test/google-vertex-api-key-resolution.test.ts +++ b/packages/ai/test/google-vertex-api-key-resolution.test.ts @@ -45,8 +45,8 @@ vi.mock("@google/genai", () => { }; }); +import { stream as streamGoogleVertex } from "../src/api/google-vertex.ts"; import { getModel } from "../src/models.ts"; -import { streamGoogleVertex } from "../src/providers/google-vertex.ts"; import type { Context, Model } from "../src/types.ts"; const model = getModel("google-vertex", "gemini-3-flash-preview"); diff --git a/packages/ai/test/lazy-module-load.test.ts b/packages/ai/test/lazy-module-load.test.ts index e21f0d12..bfb4005e 100644 --- a/packages/ai/test/lazy-module-load.test.ts +++ b/packages/ai/test/lazy-module-load.test.ts @@ -66,7 +66,7 @@ describe("lazy provider module loading", () => { expect(result.loadedSpecifiers).toEqual([]); }); - it("loads only the Anthropic SDK when calling the root lazy wrapper", () => { + it("loads only the Anthropic SDK when streaming through the lazy API wrapper", () => { const result = runProbe(` const model = { id: "claude-sonnet-4-6", @@ -81,7 +81,7 @@ describe("lazy provider module loading", () => { maxTokens: 8192, }; const context = { messages: [{ role: "user", content: "hi" }] }; - await mod.streamSimpleAnthropic(model, context).result(); + await mod.anthropicMessagesApi().streamSimple(model, context).result(); `); expect(result.loadedSpecifiers).toEqual(["@anthropic-ai/sdk"]); diff --git a/packages/ai/test/openai-codex-stream.test.ts b/packages/ai/test/openai-codex-stream.test.ts index 9cf0f981..f683d678 100644 --- a/packages/ai/test/openai-codex-stream.test.ts +++ b/packages/ai/test/openai-codex-stream.test.ts @@ -5,9 +5,9 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { getOpenAICodexWebSocketDebugStats, resetOpenAICodexWebSocketDebugStats, - streamOpenAICodexResponses, - streamSimpleOpenAICodexResponses, -} from "../src/providers/openai-codex-responses.ts"; + stream as streamOpenAICodexResponses, + streamSimple as streamSimpleOpenAICodexResponses, +} from "../src/api/openai-codex-responses.ts"; import type { Context, Model } from "../src/types.ts"; const originalAgentDir = process.env.PI_CODING_AGENT_DIR; diff --git a/packages/ai/test/openai-completions-cache-control-format.test.ts b/packages/ai/test/openai-completions-cache-control-format.test.ts index 7d8ee9c4..04fd2361 100644 --- a/packages/ai/test/openai-completions-cache-control-format.test.ts +++ b/packages/ai/test/openai-completions-cache-control-format.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { stream as streamOpenAICompletions } from "../src/api/openai-completions.ts"; import { getModel } from "../src/models.ts"; -import { streamOpenAICompletions } from "../src/providers/openai-completions.ts"; import type { Model } from "../src/types.ts"; interface CacheControl { diff --git a/packages/ai/test/openai-completions-prompt-cache.test.ts b/packages/ai/test/openai-completions-prompt-cache.test.ts index 75098fc1..0b25e857 100644 --- a/packages/ai/test/openai-completions-prompt-cache.test.ts +++ b/packages/ai/test/openai-completions-prompt-cache.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { stream as streamOpenAICompletions } from "../src/api/openai-completions.ts"; import { getModel } from "../src/models.ts"; -import { streamOpenAICompletions } from "../src/providers/openai-completions.ts"; import type { Model } from "../src/types.ts"; interface FakeOpenAIClientOptions { diff --git a/packages/ai/test/openai-completions-retry.test.ts b/packages/ai/test/openai-completions-retry.test.ts index cf631dd1..f67dbbbc 100644 --- a/packages/ai/test/openai-completions-retry.test.ts +++ b/packages/ai/test/openai-completions-retry.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { streamOpenAICompletions } from "../src/providers/openai-completions.ts"; +import { stream as streamOpenAICompletions } from "../src/api/openai-completions.ts"; import type { Context, Model } from "../src/types.ts"; const mockState = vi.hoisted(() => ({ diff --git a/packages/ai/test/openai-completions-thinking-as-text.test.ts b/packages/ai/test/openai-completions-thinking-as-text.test.ts index 1c49aad3..f8db4788 100644 --- a/packages/ai/test/openai-completions-thinking-as-text.test.ts +++ b/packages/ai/test/openai-completions-thinking-as-text.test.ts @@ -2,7 +2,7 @@ import { once } from "node:events"; import http from "node:http"; import type { AddressInfo } from "node:net"; import { afterEach, describe, expect, it } from "vitest"; -import { convertMessages, streamOpenAICompletions } from "../src/providers/openai-completions.ts"; +import { convertMessages, stream as streamOpenAICompletions } from "../src/api/openai-completions.ts"; import type { AssistantMessage, AssistantMessageEvent, diff --git a/packages/ai/test/openai-completions-tool-result-images.test.ts b/packages/ai/test/openai-completions-tool-result-images.test.ts index 3510f396..4fa0fd68 100644 --- a/packages/ai/test/openai-completions-tool-result-images.test.ts +++ b/packages/ai/test/openai-completions-tool-result-images.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { convertMessages } from "../src/api/openai-completions.ts"; import { getModel } from "../src/models.ts"; -import { convertMessages } from "../src/providers/openai-completions.ts"; import type { AssistantMessage, Context, diff --git a/packages/ai/test/openai-responses-copilot-provider.test.ts b/packages/ai/test/openai-responses-copilot-provider.test.ts index 04236fed..92b3d740 100644 --- a/packages/ai/test/openai-responses-copilot-provider.test.ts +++ b/packages/ai/test/openai-responses-copilot-provider.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; +import { stream as streamOpenAIResponses } from "../src/api/openai-responses.ts"; import { getModel } from "../src/models.ts"; -import { streamOpenAIResponses } from "../src/providers/openai-responses.ts"; import type { Model } from "../src/types.ts"; type CapturedHeaders = Headers | string[][] | Record | undefined; diff --git a/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts b/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts index 10f06377..7eea6821 100644 --- a/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts +++ b/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { convertResponsesMessages } from "../src/api/openai-responses-shared.ts"; import { getModel } from "../src/models.ts"; -import { convertResponsesMessages } from "../src/providers/openai-responses-shared.ts"; import type { AssistantMessage, Context, ToolResultMessage, Usage } from "../src/types.ts"; import { shortHash } from "../src/utils/hash.ts"; diff --git a/packages/ai/test/openai-responses-message-id.test.ts b/packages/ai/test/openai-responses-message-id.test.ts index f675cc8e..06687fa1 100644 --- a/packages/ai/test/openai-responses-message-id.test.ts +++ b/packages/ai/test/openai-responses-message-id.test.ts @@ -1,7 +1,7 @@ import type { ResponseOutputMessage } from "openai/resources/responses/responses.js"; import { describe, expect, it } from "vitest"; +import { convertResponsesMessages } from "../src/api/openai-responses-shared.ts"; import { getModel } from "../src/models.ts"; -import { convertResponsesMessages } from "../src/providers/openai-responses-shared.ts"; import type { AssistantMessage, Context, Usage } from "../src/types.ts"; const usage: Usage = { diff --git a/packages/ai/test/openai-responses-partial-json-cleanup.test.ts b/packages/ai/test/openai-responses-partial-json-cleanup.test.ts index 76b16ad3..26a6704a 100644 --- a/packages/ai/test/openai-responses-partial-json-cleanup.test.ts +++ b/packages/ai/test/openai-responses-partial-json-cleanup.test.ts @@ -1,6 +1,6 @@ import type { ResponseStreamEvent } from "openai/resources/responses/responses.js"; import { describe, expect, it, vi } from "vitest"; -import { processResponsesStream } from "../src/providers/openai-responses-shared.ts"; +import { processResponsesStream } from "../src/api/openai-responses-shared.ts"; import type { AssistantMessage, AssistantMessageEvent, Model } from "../src/types.ts"; import { AssistantMessageEventStream } from "../src/utils/event-stream.ts"; diff --git a/packages/ai/test/scratch.ts b/packages/ai/test/scratch.ts index 949dccbe..e41af118 100644 --- a/packages/ai/test/scratch.ts +++ b/packages/ai/test/scratch.ts @@ -2,10 +2,12 @@ // 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 { streamAnthropic, streamSimpleAnthropic } from "../src/providers/register-builtins.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()`; @@ -33,8 +35,8 @@ const anthropic: Provider<"anthropic-messages"> = { getModels: async () => getModels("anthropic"), // shared lazy API implementation (loads the SDK on first request) - stream: streamAnthropic, - streamSimple: streamSimpleAnthropic, + stream: anthropicApi.stream, + streamSimple: anthropicApi.streamSimple, }; // --------------------------------------------------------------------------- diff --git a/packages/ai/test/transform-messages-copilot-openai-to-anthropic.test.ts b/packages/ai/test/transform-messages-copilot-openai-to-anthropic.test.ts index 4d858835..24f218df 100644 --- a/packages/ai/test/transform-messages-copilot-openai-to-anthropic.test.ts +++ b/packages/ai/test/transform-messages-copilot-openai-to-anthropic.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { transformMessages } from "../src/providers/transform-messages.ts"; +import { transformMessages } from "../src/api/transform-messages.ts"; import type { AssistantMessage, Message, Model, ToolCall } from "../src/types.ts"; // Normalize function matching what anthropic.ts uses diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/index.ts b/packages/coding-agent/examples/extensions/custom-provider-anthropic/index.ts index 51426362..cfa80dbe 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/index.ts +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/index.ts @@ -153,7 +153,7 @@ async function refreshAnthropicToken(credentials: OAuthCredentials): Promise), compat: { @@ -336,7 +336,11 @@ export function streamGitLabDuo( context, streamOptions, ) - : streamSimpleOpenAIResponses(modelWithBaseUrl as Model<"openai-responses">, context, streamOptions); + : openAIResponsesApi().streamSimple( + modelWithBaseUrl as Model<"openai-responses">, + context, + streamOptions, + ); for await (const event of innerStream) stream.push(event); stream.end(); diff --git a/packages/coding-agent/test/sdk-codex-cache-probe-tool-loop.ts b/packages/coding-agent/test/sdk-codex-cache-probe-tool-loop.ts index a6338d14..6d00a1a0 100644 --- a/packages/coding-agent/test/sdk-codex-cache-probe-tool-loop.ts +++ b/packages/coding-agent/test/sdk-codex-cache-probe-tool-loop.ts @@ -23,8 +23,8 @@ import { } from "@earendil-works/pi-ai"; import { getOpenAICodexWebSocketDebugStats, - streamSimpleOpenAICodexResponses, -} from "../../ai/src/providers/openai-codex-responses.ts"; + streamSimple as streamSimpleOpenAICodexResponses, +} from "../../ai/src/api/openai-codex-responses.ts"; import { AuthStorage } from "../src/core/auth-storage.ts"; import { createExtensionRuntime } from "../src/core/extensions/loader.ts"; import type { ToolDefinition } from "../src/core/extensions/types.ts"; From 20a9b82cf9df59143cb50cfabc93c26420b23a41 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 10 Jun 2026 20:09:13 +0200 Subject: [PATCH 006/122] fix(ai): regenerate model catalog Picks up the Claude Fable 5 thinking-off metadata (off: null in thinkingLevelMap) that 9ccfcd7c added to the generator without regenerating, fixing anthropic-thinking-disable and supports-xhigh tests. Includes upstream catalog drift; OpenRouter delisted moonshotai/kimi-k2.6:free, so the kimi compat test now pins only the listed variant. --- packages/ai/src/models.generated.ts | 440 +++++------------- .../openai-completions-tool-choice.test.ts | 12 +- 2 files changed, 134 insertions(+), 318 deletions(-) diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 57da60bc..02652336 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -357,7 +357,7 @@ export const MODELS = { provider: "amazon-bedrock", baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 11, @@ -497,7 +497,7 @@ export const MODELS = { provider: "amazon-bedrock", baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 10, @@ -1389,7 +1389,7 @@ export const MODELS = { provider: "amazon-bedrock", baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 10, @@ -1878,7 +1878,7 @@ export const MODELS = { baseUrl: "https://api.anthropic.com", compat: {"forceAdaptiveThinking":true}, reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 10, @@ -3073,7 +3073,7 @@ export const MODELS = { baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", compat: {"forceAdaptiveThinking":true}, reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 10, @@ -5022,77 +5022,9 @@ export const MODELS = { } satisfies Model<"google-vertex">, }, "groq": { - "deepseek-r1-distill-llama-70b": { - id: "deepseek-r1-distill-llama-70b", - name: "DeepSeek R1 Distill Llama 70B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.75, - output: 0.99, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "gemma2-9b-it": { - id: "gemma2-9b-it", - name: "Gemma 2 9B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.2, - output: 0.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "groq/compound": { - id: "groq/compound", - name: "Compound", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "groq/compound-mini": { - id: "groq/compound-mini", - name: "Compound Mini", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, "llama-3.1-8b-instant": { id: "llama-3.1-8b-instant", - name: "Llama 3.1 8B Instant", + name: "Llama 3.1 8B", api: "openai-completions", provider: "groq", baseUrl: "https://api.groq.com/openai/v1", @@ -5109,7 +5041,7 @@ export const MODELS = { } satisfies Model<"openai-completions">, "llama-3.3-70b-versatile": { id: "llama-3.3-70b-versatile", - name: "Llama 3.3 70B Versatile", + name: "Llama 3.3 70B", api: "openai-completions", provider: "groq", baseUrl: "https://api.groq.com/openai/v1", @@ -5124,60 +5056,9 @@ export const MODELS = { contextWindow: 131072, maxTokens: 32768, } satisfies Model<"openai-completions">, - "llama3-70b-8192": { - id: "llama3-70b-8192", - name: "Llama 3 70B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.59, - output: 0.79, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "llama3-8b-8192": { - id: "llama3-8b-8192", - name: "Llama 3 8B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.05, - output: 0.08, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "meta-llama/llama-4-maverick-17b-128e-instruct": { - id: "meta-llama/llama-4-maverick-17b-128e-instruct", - name: "Llama 4 Maverick 17B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.2, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, "meta-llama/llama-4-scout-17b-16e-instruct": { id: "meta-llama/llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout 17B", + name: "Llama 4 Scout 17B 16E", api: "openai-completions", provider: "groq", baseUrl: "https://api.groq.com/openai/v1", @@ -5192,57 +5073,6 @@ export const MODELS = { contextWindow: 131072, maxTokens: 8192, } satisfies Model<"openai-completions">, - "mistral-saba-24b": { - id: "mistral-saba-24b", - name: "Mistral Saba 24B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.79, - output: 0.79, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2-instruct": { - id: "moonshotai/kimi-k2-instruct", - name: "Kimi K2 Instruct", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2-instruct-0905": { - id: "moonshotai/kimi-k2-instruct-0905", - name: "Kimi K2 Instruct 0905", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 16384, - } satisfies Model<"openai-completions">, "openai/gpt-oss-120b": { id: "openai/gpt-oss-120b", name: "GPT OSS 120B", @@ -5294,26 +5124,9 @@ export const MODELS = { contextWindow: 131072, maxTokens: 65536, } satisfies Model<"openai-completions">, - "qwen-qwq-32b": { - id: "qwen-qwq-32b", - name: "Qwen QwQ 32B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.29, - output: 0.39, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, "qwen/qwen3-32b": { id: "qwen/qwen3-32b", - name: "Qwen3 32B", + name: "Qwen3-32B", api: "openai-completions", provider: "groq", baseUrl: "https://api.groq.com/openai/v1", @@ -6763,8 +6576,8 @@ export const MODELS = { baseUrl: "https://integrate.api.nvidia.com/v1", headers: {"NVCF-POLL-SECONDS":"3600"}, compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], + reasoning: true, + input: ["text", "image"], cost: { input: 0, output: 0, @@ -6793,44 +6606,6 @@ export const MODELS = { contextWindow: 262144, maxTokens: 262144, } satisfies Model<"openai-completions">, - "nvidia/llama-3.3-nemotron-super-49b-v1": { - id: "nvidia/llama-3.3-nemotron-super-49b-v1", - name: "Llama 3.3 Nemotron Super 49B v1", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "nvidia/llama-3.3-nemotron-super-49b-v1.5": { - id: "nvidia/llama-3.3-nemotron-super-49b-v1.5", - name: "Llama 3.3 Nemotron Super 49B v1.5", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, "nvidia/nemotron-3-nano-30b-a3b": { id: "nvidia/nemotron-3-nano-30b-a3b", name: "nemotron-3-nano-30b-a3b", @@ -6926,6 +6701,25 @@ export const MODELS = { contextWindow: 131072, maxTokens: 131072, } satisfies Model<"openai-completions">, + "openai/gpt-oss-120b": { + id: "openai/gpt-oss-120b", + name: "GPT-OSS-120B", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"openai-completions">, "openai/gpt-oss-20b": { id: "openai/gpt-oss-20b", name: "GPT OSS 20B", @@ -7882,7 +7676,7 @@ export const MODELS = { baseUrl: "https://opencode.ai/zen", compat: {"forceAdaptiveThinking":true}, reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 10, @@ -8066,7 +7860,7 @@ export const MODELS = { cost: { input: 0.14, output: 0.28, - cacheRead: 0.03, + cacheRead: 0.028, cacheWrite: 0, }, contextWindow: 1000000, @@ -8091,6 +7885,25 @@ export const MODELS = { contextWindow: 200000, maxTokens: 128000, } satisfies Model<"openai-completions">, + "deepseek-v4-pro": { + id: "deepseek-v4-pro", + name: "DeepSeek V4 Pro", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 1.74, + output: 3.84, + cacheRead: 0.145, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"openai-completions">, "gemini-3-flash": { id: "gemini-3-flash", name: "Gemini 3 Flash", @@ -9484,7 +9297,7 @@ export const MODELS = { cacheRead: 0.135, cacheWrite: 0, }, - contextWindow: 163840, + contextWindow: 131072, maxTokens: 16384, } satisfies Model<"openai-completions">, "deepseek/deepseek-chat-v3.1": { @@ -10632,24 +10445,6 @@ export const MODELS = { contextWindow: 262144, maxTokens: 262142, } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2.6:free": { - id: "moonshotai/kimi-k2.6:free", - name: "MoonshotAI: Kimi K2.6 (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - compat: {"supportsDeveloperRole":false,"requiresReasoningContentOnAssistantMessages":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, "nex-agi/nex-n2-pro:free": { id: "nex-agi/nex-n2-pro:free", name: "Nex AGI: Nex-N2-Pro (free)", @@ -12923,23 +12718,6 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 131072, } satisfies Model<"openai-completions">, - "z-ai/glm-4-32b": { - id: "z-ai/glm-4-32b", - name: "Z.ai: GLM 4 32B ", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.09999999999999999, - output: 0.09999999999999999, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, "z-ai/glm-4.5": { id: "z-ai/glm-4.5", name: "Z.ai: GLM 4.5", @@ -12974,23 +12752,6 @@ export const MODELS = { contextWindow: 131072, maxTokens: 131070, } satisfies Model<"openai-completions">, - "z-ai/glm-4.5-air:free": { - id: "z-ai/glm-4.5-air:free", - name: "Z.ai: GLM 4.5 Air (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 96000, - } satisfies Model<"openai-completions">, "z-ai/glm-4.5v": { id: "z-ai/glm-4.5v", name: "Z.ai: GLM 4.5V", @@ -13036,11 +12797,11 @@ export const MODELS = { cost: { input: 0.3, output: 0.8999999999999999, - cacheRead: 0.049999999999999996, + cacheRead: 0.055, cacheWrite: 0, }, contextWindow: 131072, - maxTokens: 24000, + maxTokens: 32768, } satisfies Model<"openai-completions">, "z-ai/glm-4.7": { id: "z-ai/glm-4.7", @@ -13107,7 +12868,7 @@ export const MODELS = { cacheRead: 0.24, cacheWrite: 0, }, - contextWindow: 202752, + contextWindow: 262144, maxTokens: 131072, } satisfies Model<"openai-completions">, "z-ai/glm-5.1": { @@ -13127,23 +12888,6 @@ export const MODELS = { contextWindow: 202752, maxTokens: 4096, } satisfies Model<"openai-completions">, - "z-ai/glm-5v-turbo": { - id: "z-ai/glm-5v-turbo", - name: "Z.ai: GLM 5V Turbo", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.2, - output: 4, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 131072, - } satisfies Model<"openai-completions">, "~anthropic/claude-fable-latest": { id: "~anthropic/claude-fable-latest", name: "Anthropic: Claude Fable Latest", @@ -14075,7 +13819,7 @@ export const MODELS = { baseUrl: "https://ai-gateway.vercel.sh", compat: {"forceAdaptiveThinking":true}, reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 10, @@ -16749,6 +16493,24 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 131072, } satisfies Model<"openai-completions">, + "mimo-v2.5-pro-ultraspeed": { + id: "mimo-v2.5-pro-ultraspeed", + name: "MiMo-V2.5-Pro-UltraSpeed", + api: "openai-completions", + provider: "xiaomi", + baseUrl: "https://api.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.305, + output: 2.61, + cacheRead: 0.0108, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, }, "xiaomi-token-plan-ams": { "mimo-v2-omni": { @@ -16823,6 +16585,24 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 131072, } satisfies Model<"openai-completions">, + "mimo-v2.5-pro-ultraspeed": { + id: "mimo-v2.5-pro-ultraspeed", + name: "MiMo-V2.5-Pro-UltraSpeed", + api: "openai-completions", + provider: "xiaomi-token-plan-ams", + baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.305, + output: 2.61, + cacheRead: 0.0108, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, }, "xiaomi-token-plan-cn": { "mimo-v2-omni": { @@ -16897,6 +16677,24 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 131072, } satisfies Model<"openai-completions">, + "mimo-v2.5-pro-ultraspeed": { + id: "mimo-v2.5-pro-ultraspeed", + name: "MiMo-V2.5-Pro-UltraSpeed", + api: "openai-completions", + provider: "xiaomi-token-plan-cn", + baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.305, + output: 2.61, + cacheRead: 0.0108, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, }, "xiaomi-token-plan-sgp": { "mimo-v2-omni": { @@ -16971,6 +16769,24 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 131072, } satisfies Model<"openai-completions">, + "mimo-v2.5-pro-ultraspeed": { + id: "mimo-v2.5-pro-ultraspeed", + name: "MiMo-V2.5-Pro-UltraSpeed", + api: "openai-completions", + provider: "xiaomi-token-plan-sgp", + baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.305, + output: 2.61, + cacheRead: 0.0108, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, }, "zai": { "glm-4.5-air": { diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index 319b18af..61054aad 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { convertMessages } from "../src/api/openai-completions.ts"; import { getModel } from "../src/models.ts"; -import { convertMessages } from "../src/providers/openai-completions.ts"; import { stream, streamSimple } from "../src/stream.ts"; import type { AssistantMessage, Model, Tool, ToolResultMessage } from "../src/types.ts"; @@ -890,11 +890,11 @@ describe("openai-completions tool_choice", () => { }); it("stores OpenRouter Kimi K2.6 reasoning replay compat in built-in metadata", () => { - for (const modelId of ["moonshotai/kimi-k2.6", "moonshotai/kimi-k2.6:free"] as const) { - const model = getModel("openrouter", modelId)!; - expect(model.compat?.supportsDeveloperRole).toBe(false); - expect(model.compat?.requiresReasoningContentOnAssistantMessages).toBe(true); - } + // `:free` variant delisted from the OpenRouter API; the generator override + // matches any `moonshotai/kimi-k2.6*` variant that is listed. + const model = getModel("openrouter", "moonshotai/kimi-k2.6")!; + expect(model.compat?.supportsDeveloperRole).toBe(false); + expect(model.compat?.requiresReasoningContentOnAssistantMessages).toBe(true); }); it("stores Xiaomi MiMo reasoning replay compat in built-in metadata", () => { From a46f4e19f0a18df2c29f1f417d4ca0b59b2bf9e0 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 10 Jun 2026 20:09:16 +0200 Subject: [PATCH 007/122] fix: unset NVIDIA_API_KEY and ANT_LING_API_KEY in test.sh stream.test.ts gates e2e suites on these env vars; test.sh missed them, so local runs with the keys present executed live NVIDIA NIM e2e tests. --- test.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test.sh b/test.sh index 9a553f6f..b4ea70c7 100755 --- a/test.sh +++ b/test.sh @@ -25,6 +25,8 @@ export PI_NO_LOCAL_LLM=1 # Unset API keys (see packages/ai/src/stream.ts getEnvApiKey) unset ANTHROPIC_API_KEY unset ANTHROPIC_OAUTH_TOKEN +unset ANT_LING_API_KEY +unset NVIDIA_API_KEY unset OPENAI_API_KEY unset AZURE_OPENAI_API_KEY unset DEEPSEEK_API_KEY From afc2bd370e0b7d495dc45ff7751d2fcaae9a7c34 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 10 Jun 2026 20:09:18 +0200 Subject: [PATCH 008/122] fix(coding-agent): use physical temp paths in session-id-readonly test On macOS tmpdir() is a symlink (/var -> /private/var) while the spawned CLI's process.cwd() is physical, so session cwd filtering never matched the fixtures and the fork-target rejection test failed locally. --- .../test/session-id-readonly.test.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/test/session-id-readonly.test.ts b/packages/coding-agent/test/session-id-readonly.test.ts index b6e97ce8..47537b60 100644 --- a/packages/coding-agent/test/session-id-readonly.test.ts +++ b/packages/coding-agent/test/session-id-readonly.test.ts @@ -1,5 +1,14 @@ import { spawn } from "node:child_process"; -import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readdirSync, + readFileSync, + realpathSync, + rmSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; @@ -15,7 +24,10 @@ afterEach(() => { }); function createTempDir(): string { - const dir = mkdtempSync(join(tmpdir(), "pi-session-id-readonly-")); + // realpath: on macOS tmpdir() is a symlink (/var -> /private/var), but the + // spawned CLI sees the physical path via process.cwd(). Session cwd + // filtering compares paths textually, so the fixture must use physical paths. + const dir = realpathSync(mkdtempSync(join(tmpdir(), "pi-session-id-readonly-"))); tempDirs.push(dir); return dir; } From fec0c3d12f4156d7be6a4254968222b7d1fc655d Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 10 Jun 2026 20:33:20 +0200 Subject: [PATCH 009/122] 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/.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. --- biome.json | 1 + packages/agent/docs/models.md | 48 +- packages/ai/scripts/generate-models.ts | 116 +- packages/ai/src/auth/helpers.ts | 46 + packages/ai/src/index.ts | 1 + packages/ai/src/models.generated.ts | 17039 +--------------- packages/ai/src/models.ts | 56 + packages/ai/src/providers/all.ts | 92 + .../ai/src/providers/amazon-bedrock.models.ts | 1677 ++ packages/ai/src/providers/amazon-bedrock.ts | 35 + packages/ai/src/providers/ant-ling.models.ts | 62 + packages/ai/src/providers/ant-ling.ts | 15 + packages/ai/src/providers/anthropic.models.ts | 441 + packages/ai/src/providers/anthropic.ts | 20 + .../azure-openai-responses.models.ts | 745 + .../src/providers/azure-openai-responses.ts | 14 + packages/ai/src/providers/cerebras.models.ts | 58 + packages/ai/src/providers/cerebras.ts | 15 + .../providers/cloudflare-ai-gateway.models.ts | 656 + .../ai/src/providers/cloudflare-ai-gateway.ts | 22 + .../providers/cloudflare-workers-ai.models.ts | 205 + .../ai/src/providers/cloudflare-workers-ai.ts | 14 + packages/ai/src/providers/deepseek.models.ts | 45 + packages/ai/src/providers/deepseek.ts | 15 + packages/ai/src/providers/faux.ts | 71 +- packages/ai/src/providers/fireworks.models.ts | 241 + packages/ai/src/providers/fireworks.ts | 15 + .../ai/src/providers/github-copilot.models.ts | 427 + packages/ai/src/providers/github-copilot.ts | 25 + .../ai/src/providers/google-vertex.models.ts | 232 + packages/ai/src/providers/google-vertex.ts | 38 + packages/ai/src/providers/google.models.ts | 288 + packages/ai/src/providers/google.ts | 15 + packages/ai/src/providers/groq.models.ts | 127 + packages/ai/src/providers/groq.ts | 15 + .../ai/src/providers/huggingface.models.ts | 403 + packages/ai/src/providers/huggingface.ts | 15 + .../ai/src/providers/kimi-coding.models.ts | 43 + packages/ai/src/providers/kimi-coding.ts | 15 + .../ai/src/providers/minimax-cn.models.ts | 58 + packages/ai/src/providers/minimax-cn.ts | 15 + packages/ai/src/providers/minimax.models.ts | 58 + packages/ai/src/providers/minimax.ts | 15 + packages/ai/src/providers/mistral.models.ts | 517 + packages/ai/src/providers/mistral.ts | 15 + .../ai/src/providers/moonshotai-cn.models.ts | 133 + packages/ai/src/providers/moonshotai-cn.ts | 15 + .../ai/src/providers/moonshotai.models.ts | 133 + packages/ai/src/providers/moonshotai.ts | 15 + packages/ai/src/providers/nvidia.models.ts | 387 + packages/ai/src/providers/nvidia.ts | 15 + .../ai/src/providers/openai-codex.models.ts | 79 + packages/ai/src/providers/openai-codex.ts | 18 + packages/ai/src/providers/openai.models.ts | 745 + packages/ai/src/providers/openai.ts | 15 + .../ai/src/providers/opencode-go.models.ts | 258 + packages/ai/src/providers/opencode-go.ts | 18 + packages/ai/src/providers/opencode.models.ts | 818 + packages/ai/src/providers/opencode.ts | 24 + .../ai/src/providers/openrouter.models.ts | 4332 ++++ packages/ai/src/providers/openrouter.ts | 15 + packages/ai/src/providers/together.models.ts | 365 + packages/ai/src/providers/together.ts | 15 + .../src/providers/vercel-ai-gateway.models.ts | 2884 +++ .../ai/src/providers/vercel-ai-gateway.ts | 15 + packages/ai/src/providers/xai.models.ts | 126 + packages/ai/src/providers/xai.ts | 15 + .../providers/xiaomi-token-plan-ams.models.ts | 97 + .../ai/src/providers/xiaomi-token-plan-ams.ts | 15 + .../providers/xiaomi-token-plan-cn.models.ts | 97 + .../ai/src/providers/xiaomi-token-plan-cn.ts | 15 + .../providers/xiaomi-token-plan-sgp.models.ts | 97 + .../ai/src/providers/xiaomi-token-plan-sgp.ts | 15 + packages/ai/src/providers/xiaomi.models.ts | 115 + packages/ai/src/providers/xiaomi.ts | 15 + .../ai/src/providers/zai-coding-cn.models.ts | 97 + packages/ai/src/providers/zai-coding-cn.ts | 15 + packages/ai/src/providers/zai.models.ts | 97 + packages/ai/src/providers/zai.ts | 15 + packages/ai/src/utils/oauth/load.ts | 21 + packages/ai/test/lazy-module-load.test.ts | 10 + packages/ai/test/providers.test.ts | 208 + packages/ai/test/scratch.ts | 48 +- 83 files changed, 18409 insertions(+), 17094 deletions(-) create mode 100644 packages/ai/src/auth/helpers.ts create mode 100644 packages/ai/src/providers/all.ts create mode 100644 packages/ai/src/providers/amazon-bedrock.models.ts create mode 100644 packages/ai/src/providers/amazon-bedrock.ts create mode 100644 packages/ai/src/providers/ant-ling.models.ts create mode 100644 packages/ai/src/providers/ant-ling.ts create mode 100644 packages/ai/src/providers/anthropic.models.ts create mode 100644 packages/ai/src/providers/anthropic.ts create mode 100644 packages/ai/src/providers/azure-openai-responses.models.ts create mode 100644 packages/ai/src/providers/azure-openai-responses.ts create mode 100644 packages/ai/src/providers/cerebras.models.ts create mode 100644 packages/ai/src/providers/cerebras.ts create mode 100644 packages/ai/src/providers/cloudflare-ai-gateway.models.ts create mode 100644 packages/ai/src/providers/cloudflare-ai-gateway.ts create mode 100644 packages/ai/src/providers/cloudflare-workers-ai.models.ts create mode 100644 packages/ai/src/providers/cloudflare-workers-ai.ts create mode 100644 packages/ai/src/providers/deepseek.models.ts create mode 100644 packages/ai/src/providers/deepseek.ts create mode 100644 packages/ai/src/providers/fireworks.models.ts create mode 100644 packages/ai/src/providers/fireworks.ts create mode 100644 packages/ai/src/providers/github-copilot.models.ts create mode 100644 packages/ai/src/providers/github-copilot.ts create mode 100644 packages/ai/src/providers/google-vertex.models.ts create mode 100644 packages/ai/src/providers/google-vertex.ts create mode 100644 packages/ai/src/providers/google.models.ts create mode 100644 packages/ai/src/providers/google.ts create mode 100644 packages/ai/src/providers/groq.models.ts create mode 100644 packages/ai/src/providers/groq.ts create mode 100644 packages/ai/src/providers/huggingface.models.ts create mode 100644 packages/ai/src/providers/huggingface.ts create mode 100644 packages/ai/src/providers/kimi-coding.models.ts create mode 100644 packages/ai/src/providers/kimi-coding.ts create mode 100644 packages/ai/src/providers/minimax-cn.models.ts create mode 100644 packages/ai/src/providers/minimax-cn.ts create mode 100644 packages/ai/src/providers/minimax.models.ts create mode 100644 packages/ai/src/providers/minimax.ts create mode 100644 packages/ai/src/providers/mistral.models.ts create mode 100644 packages/ai/src/providers/mistral.ts create mode 100644 packages/ai/src/providers/moonshotai-cn.models.ts create mode 100644 packages/ai/src/providers/moonshotai-cn.ts create mode 100644 packages/ai/src/providers/moonshotai.models.ts create mode 100644 packages/ai/src/providers/moonshotai.ts create mode 100644 packages/ai/src/providers/nvidia.models.ts create mode 100644 packages/ai/src/providers/nvidia.ts create mode 100644 packages/ai/src/providers/openai-codex.models.ts create mode 100644 packages/ai/src/providers/openai-codex.ts create mode 100644 packages/ai/src/providers/openai.models.ts create mode 100644 packages/ai/src/providers/openai.ts create mode 100644 packages/ai/src/providers/opencode-go.models.ts create mode 100644 packages/ai/src/providers/opencode-go.ts create mode 100644 packages/ai/src/providers/opencode.models.ts create mode 100644 packages/ai/src/providers/opencode.ts create mode 100644 packages/ai/src/providers/openrouter.models.ts create mode 100644 packages/ai/src/providers/openrouter.ts create mode 100644 packages/ai/src/providers/together.models.ts create mode 100644 packages/ai/src/providers/together.ts create mode 100644 packages/ai/src/providers/vercel-ai-gateway.models.ts create mode 100644 packages/ai/src/providers/vercel-ai-gateway.ts create mode 100644 packages/ai/src/providers/xai.models.ts create mode 100644 packages/ai/src/providers/xai.ts create mode 100644 packages/ai/src/providers/xiaomi-token-plan-ams.models.ts create mode 100644 packages/ai/src/providers/xiaomi-token-plan-ams.ts create mode 100644 packages/ai/src/providers/xiaomi-token-plan-cn.models.ts create mode 100644 packages/ai/src/providers/xiaomi-token-plan-cn.ts create mode 100644 packages/ai/src/providers/xiaomi-token-plan-sgp.models.ts create mode 100644 packages/ai/src/providers/xiaomi-token-plan-sgp.ts create mode 100644 packages/ai/src/providers/xiaomi.models.ts create mode 100644 packages/ai/src/providers/xiaomi.ts create mode 100644 packages/ai/src/providers/zai-coding-cn.models.ts create mode 100644 packages/ai/src/providers/zai-coding-cn.ts create mode 100644 packages/ai/src/providers/zai.models.ts create mode 100644 packages/ai/src/providers/zai.ts create mode 100644 packages/ai/src/utils/oauth/load.ts create mode 100644 packages/ai/test/providers.test.ts diff --git a/biome.json b/biome.json index 7c89d187..b5451402 100644 --- a/biome.json +++ b/biome.json @@ -31,6 +31,7 @@ "!**/node_modules/**/*", "!**/test-sessions.ts", "!**/models.generated.ts", + "!**/*.models.ts", "!packages/mom/data/**/*", "!!**/node_modules" ] diff --git a/packages/agent/docs/models.md b/packages/agent/docs/models.md index 67f87549..de3c3027 100644 --- a/packages/agent/docs/models.md +++ b/packages/agent/docs/models.md @@ -108,7 +108,7 @@ All built-ins, explicitly heavy metadata entrypoint: ```ts import { builtinModels } from "@earendil-works/pi-ai/providers/all"; -const models = builtinModels({ oauth: "node" }); +const models = builtinModels(); ``` `providers/all` may import all provider metadata/catalogs. It still must not eagerly import SDK implementations; provider streams use lazy wrappers. @@ -581,31 +581,22 @@ export type AuthEvent = `prompt()` returns the entered/selected string (`select` returns the option id). Flows race a `manual_code` prompt against a callback server by setting `AuthPrompt.signal` and aborting the prompt when the callback wins. -### OAuth implementation target +### OAuth attachment -OAuth must not force Node-only code (`node:http`, `node:crypto`) into browser bundles. Keep OAuth lazy; the provider factory decides which implementation to attach: +Providers that support OAuth always attach it. There is no factory toggle: the flow is lazy-loaded, so advertising OAuth costs nothing until `login()`/`refresh()` actually runs, and a host that never logs in never loads it. ```ts -export type OAuthTarget = "node" | "web" | false; - -export interface AnthropicProviderOptions { - oauth?: OAuthTarget; // default false -} - -export function anthropicProvider(options: AnthropicProviderOptions = {}): Provider { +export function anthropicProvider(): Provider { return createProvider({ id: "anthropic", name: "Anthropic", baseUrl: "https://api.anthropic.com/v1", auth: { apiKey: envApiKeyAuth("Anthropic API key", ["ANTHROPIC_API_KEY"]), - oauth: - options.oauth === "node" - ? lazyOAuth({ - name: "Anthropic (Claude Pro/Max)", - load: () => import("../utils/oauth/anthropic.ts").then((m) => m.anthropicOAuth), - }) - : undefined, + oauth: lazyOAuth({ + name: "Anthropic (Claude Pro/Max)", + load: () => import("../utils/oauth/anthropic.ts").then((m) => m.anthropicOAuth), + }), }, models: ANTHROPIC_MODELS, api: anthropicMessagesApi(), @@ -613,10 +604,6 @@ export function anthropicProvider(options: AnthropicProviderOptions = {}): Provi } ``` -- Individual factories default to `oauth: false`. -- `builtinModels({ oauth: "node" })` for pi CLI/coding-agent. -- `"web"` is reserved; web flows (sitegeist-style: Web Crypto PKCE, auth tab, extension tab APIs watching the localhost redirect, fetch token exchange, device-code polling for Copilot) are a follow-up. Until implemented, passing `"web"` throws at login time with a clear message. - `lazyOAuth()` wraps a dynamically imported `OAuthAuth` so provider definitions can advertise OAuth without importing the implementation (`toAuth` is async for exactly this reason): ```ts @@ -626,6 +613,8 @@ export function lazyOAuth(input: { }): OAuthAuth; ``` +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`. ## Provider wrappers and models.json @@ -699,7 +688,7 @@ Built-in provider factories use `createProvider()` internally. models.json custo Old semantics being preserved: global `stream()` dispatched purely on `model.api` via the api-registry, with env API key injection. The compat module reproduces this: -- Lazily creates a default `Models` singleton from `builtinModels({ oauth: "node" })` on first use. +- Lazily creates a default `Models` singleton from `builtinModels()` on first use. - `stream/complete/streamSimple/completeSimple(model, ctx, opts)`: look up `getProvider(model.provider)`; if found, route through the singleton (auth resolution included). If not found (custom models.json/extension models), fall back to api-dispatch through a hidden `createProvider()` map containing all builtin API implementations plus anything registered via compat `registerApiProvider()`. - `registerApiProvider()/unregisterApiProviders()` feed that fallback dispatch map. `api-registry.ts` dies as a real mechanism. - Sync `getModel/getModels/getProviders` become deprecated aliases of `getBuiltinModel/getBuiltinModels/getBuiltinProviders` (they were always pure generated-catalog reads — verified: nothing ever mutated the old `modelRegistry`). @@ -730,7 +719,7 @@ Rules: 2. Provider modules import their catalog, auth helpers, and lazy API wrappers only. 3. Lazy API wrappers dynamically import real API implementations. 4. Real API implementations import SDK dependencies. -5. OAuth implementations are selected by factory option (`oauth: "node" | "web" | false`) and lazy-loaded; provider metadata never eagerly imports Node-only OAuth code. +5. OAuth implementations are always attached via `lazyOAuth()` and lazy-loaded behind a bundler-opaque dynamic import; provider metadata never eagerly imports Node-only OAuth code. 6. `providers/all` may import all provider metadata, but no eager SDK imports. 7. Provider modules are side-effect-free; importing a provider does not register anything globally. 8. `package.json` sets `sideEffects: false`. @@ -809,18 +798,17 @@ Check items off as they land. Keep this list current; it is the working state fo ### Phase 3 — provider factories + catalogs -- [ ] Auth helpers in `src/auth/`: `envApiKeyAuth()`, `lazyOAuth()`, `OAuthTarget`. -- [ ] `createProvider()` (single + mixed `api` map, dispatch on `model.api`). -- [ ] Per-provider factories under `src/providers/` for all built-in catalog providers, `oauth` factory options where applicable. -- [ ] `providers/all.ts`: `builtinModels({ oauth? })`, `getBuiltinModel/getBuiltinModels/getBuiltinProviders`. -- [ ] Faux provider factory (`providers/faux.ts`) for tests. -- [ ] Split generated catalogs per provider via `scripts/generate-models.ts` (`providers/.models.ts`) — or record explicitly that this is deferred. +- [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] `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. +- [x] Faux provider factory (`fauxProvider()` in `providers/faux.ts`) for tests; legacy `registerFauxProvider()` kept until compat dies. +- [x] Split generated catalogs per provider via `scripts/generate-models.ts` (`providers/.models.ts`); `models.generated.ts` becomes a generated aggregator. ### Phase 4 — OAuth adaptation - [ ] Adapt `utils/oauth/anthropic.ts`, `openai-codex.ts`, `github-copilot.ts` to `OAuthAuth` (`login`/`refresh`/`toAuth`) + `prompt()/notify()`; `modifyModels` baseUrl rewriting becomes `toAuth().baseUrl`. - [ ] Remove `usesCallbackServer`; callback-server flows race a `manual_code` prompt instead. -- [ ] `oauth: "web"` reserved: throws at login with clear message. ### Phase 5 — packaging diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 9744961d..51285220 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { writeFileSync } from "fs"; +import { readdirSync, rmSync, writeFileSync } from "fs"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; import { @@ -2103,62 +2103,80 @@ async function generateModels() { } } - // Generate TypeScript file - let output = `// This file is auto-generated by scripts/generate-models.ts + // Generate TypeScript files: one catalog per provider plus an aggregator + const generatedHeader = `// This file is auto-generated by scripts/generate-models.ts // Do not edit manually - run 'npm run generate-models' to update -import type { Model } from "./types.ts"; - -export const MODELS = { `; + const catalogConstName = (providerId: string) => `${providerId.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_MODELS`; - // Generate provider sections (sorted for deterministic output) - const sortedProviderIds = Object.keys(providers).sort(); - for (const providerId of sortedProviderIds) { - const models = providers[providerId]; - output += `\t${JSON.stringify(providerId)}: {\n`; - - const sortedModelIds = Object.keys(models).sort(); - for (const modelId of sortedModelIds) { - const model = models[modelId]; - output += `\t\t"${model.id}": {\n`; - output += `\t\t\tid: "${model.id}",\n`; - output += `\t\t\tname: "${model.name}",\n`; - output += `\t\t\tapi: "${model.api}",\n`; - output += `\t\t\tprovider: "${model.provider}",\n`; - if (model.baseUrl !== undefined) { - output += `\t\t\tbaseUrl: "${model.baseUrl}",\n`; - } - if (model.headers) { - output += `\t\t\theaders: ${JSON.stringify(model.headers)},\n`; - } - if (model.compat) { - output += ` compat: ${JSON.stringify(model.compat)}, -`; - } - output += `\t\t\treasoning: ${model.reasoning},\n`; - if (model.thinkingLevelMap) { - output += `\t\t\tthinkingLevelMap: ${JSON.stringify(model.thinkingLevelMap)},\n`; - } - output += `\t\t\tinput: [${model.input.map(i => `"${i}"`).join(", ")}],\n`; - output += `\t\t\tcost: {\n`; - output += `\t\t\t\tinput: ${model.cost.input},\n`; - output += `\t\t\t\toutput: ${model.cost.output},\n`; - output += `\t\t\t\tcacheRead: ${model.cost.cacheRead},\n`; - output += `\t\t\t\tcacheWrite: ${model.cost.cacheWrite},\n`; - output += `\t\t\t},\n`; - output += `\t\t\tcontextWindow: ${model.contextWindow},\n`; - output += `\t\t\tmaxTokens: ${model.maxTokens},\n`; - output += `\t\t} satisfies Model<"${model.api}">,\n`; + function emitModel(model: Model, indent: string): string { + let output = `${indent}"${model.id}": {\n`; + output += `${indent}\tid: "${model.id}",\n`; + output += `${indent}\tname: "${model.name}",\n`; + output += `${indent}\tapi: "${model.api}",\n`; + output += `${indent}\tprovider: "${model.provider}",\n`; + if (model.baseUrl !== undefined) { + output += `${indent}\tbaseUrl: "${model.baseUrl}",\n`; } - - output += `\t},\n`; + if (model.headers) { + output += `${indent}\theaders: ${JSON.stringify(model.headers)},\n`; + } + if (model.compat) { + output += `${indent}\tcompat: ${JSON.stringify(model.compat)},\n`; + } + output += `${indent}\treasoning: ${model.reasoning},\n`; + if (model.thinkingLevelMap) { + output += `${indent}\tthinkingLevelMap: ${JSON.stringify(model.thinkingLevelMap)},\n`; + } + output += `${indent}\tinput: [${model.input.map(i => `"${i}"`).join(", ")}],\n`; + output += `${indent}\tcost: {\n`; + output += `${indent}\t\tinput: ${model.cost.input},\n`; + output += `${indent}\t\toutput: ${model.cost.output},\n`; + output += `${indent}\t\tcacheRead: ${model.cost.cacheRead},\n`; + output += `${indent}\t\tcacheWrite: ${model.cost.cacheWrite},\n`; + output += `${indent}\t},\n`; + output += `${indent}\tcontextWindow: ${model.contextWindow},\n`; + output += `${indent}\tmaxTokens: ${model.maxTokens},\n`; + output += `${indent}} satisfies Model<"${model.api}">,\n`; + return output; } - output += `} as const; -`; + const sortedProviderIds = Object.keys(providers).sort(); + const providersDir = join(packageRoot, "src/providers"); - // Write file + // Remove stale per-provider catalogs + for (const entry of readdirSync(providersDir)) { + if (entry.endsWith(".models.ts")) { + rmSync(join(providersDir, entry)); + } + } + + // Per-provider catalogs (sorted for deterministic output) + for (const providerId of sortedProviderIds) { + const models = providers[providerId]; + let output = generatedHeader; + output += `import type { Model } from "../types.ts";\n\n`; + output += `export const ${catalogConstName(providerId)} = {\n`; + const sortedModelIds = Object.keys(models).sort(); + for (const modelId of sortedModelIds) { + output += emitModel(models[modelId], "\t"); + } + output += `} as const;\n`; + writeFileSync(join(providersDir, `${providerId}.models.ts`), output); + } + console.log(`Generated ${sortedProviderIds.length} catalogs under src/providers/`); + + // Aggregator + let output = generatedHeader; + for (const providerId of sortedProviderIds) { + output += `import { ${catalogConstName(providerId)} } from "./providers/${providerId}.models.ts";\n`; + } + output += `\nexport const MODELS = {\n`; + for (const providerId of sortedProviderIds) { + output += `\t${JSON.stringify(providerId)}: ${catalogConstName(providerId)},\n`; + } + output += `} as const;\n`; writeFileSync(join(packageRoot, "src/models.generated.ts"), output); console.log("Generated src/models.generated.ts"); diff --git a/packages/ai/src/auth/helpers.ts b/packages/ai/src/auth/helpers.ts new file mode 100644 index 00000000..0362c364 --- /dev/null +++ b/packages/ai/src/auth/helpers.ts @@ -0,0 +1,46 @@ +import type { ApiKeyAuth, OAuthAuth } from "./types.ts"; + +/** + * Standard api-key auth: a stored credential key wins, otherwise the first + * set env var resolves. Includes a `login` that prompts for the key. + * Providers with non-standard resolution (metadata, ambient files, IAM) + * write their own `ApiKeyAuth`. + */ +export function envApiKeyAuth(name: string, envVars: readonly string[]): ApiKeyAuth { + return { + name, + login: async (callbacks) => { + const key = await callbacks.prompt({ type: "secret", message: `Enter ${name}` }); + return { type: "api-key", key }; + }, + resolve: async ({ ctx, credential }) => { + if (credential?.key) return { auth: { apiKey: credential.key }, source: "stored credential" }; + for (const envVar of envVars) { + const value = await ctx.env(envVar); + if (value) return { auth: { apiKey: value }, source: envVar }; + } + return undefined; + }, + }; +} + +/** + * Wraps a dynamically imported `OAuthAuth` so provider definitions can + * advertise OAuth without importing the implementation. The flow loads on + * first `login`/`refresh`/`toAuth` call; callers keep Node-only flow code out + * of bundles by loading through a bundler-opaque dynamic import (variable + * specifier, see the bedrock lazy wrapper). + */ +export function lazyOAuth(input: { name: string; load: () => Promise }): OAuthAuth { + let promise: Promise | undefined; + const loaded = () => { + promise ??= input.load(); + return promise; + }; + return { + name: input.name, + login: async (callbacks) => (await loaded()).login(callbacks), + refresh: async (credential) => (await loaded()).refresh(credential), + toAuth: async (credential) => (await loaded()).toAuth(credential), + }; +} diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index f98a7616..cd531d5b 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -24,6 +24,7 @@ export type { OpenAIResponsesOptions } from "./api/openai-responses.ts"; export * from "./api-registry.ts"; export * from "./auth/context.ts"; export * from "./auth/credential-store.ts"; +export * from "./auth/helpers.ts"; export * from "./auth/types.ts"; export * from "./env-api-keys.ts"; export * from "./image-models.ts"; diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 02652336..0129ddee 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -1,16975 +1,76 @@ // This file is auto-generated by scripts/generate-models.ts // Do not edit manually - run 'npm run generate-models' to update -import type { Model } from "./types.ts"; +import { AMAZON_BEDROCK_MODELS } from "./providers/amazon-bedrock.models.ts"; +import { ANT_LING_MODELS } from "./providers/ant-ling.models.ts"; +import { ANTHROPIC_MODELS } from "./providers/anthropic.models.ts"; +import { AZURE_OPENAI_RESPONSES_MODELS } from "./providers/azure-openai-responses.models.ts"; +import { CEREBRAS_MODELS } from "./providers/cerebras.models.ts"; +import { CLOUDFLARE_AI_GATEWAY_MODELS } from "./providers/cloudflare-ai-gateway.models.ts"; +import { CLOUDFLARE_WORKERS_AI_MODELS } from "./providers/cloudflare-workers-ai.models.ts"; +import { DEEPSEEK_MODELS } from "./providers/deepseek.models.ts"; +import { FIREWORKS_MODELS } from "./providers/fireworks.models.ts"; +import { GITHUB_COPILOT_MODELS } from "./providers/github-copilot.models.ts"; +import { GOOGLE_MODELS } from "./providers/google.models.ts"; +import { GOOGLE_VERTEX_MODELS } from "./providers/google-vertex.models.ts"; +import { GROQ_MODELS } from "./providers/groq.models.ts"; +import { HUGGINGFACE_MODELS } from "./providers/huggingface.models.ts"; +import { KIMI_CODING_MODELS } from "./providers/kimi-coding.models.ts"; +import { MINIMAX_MODELS } from "./providers/minimax.models.ts"; +import { MINIMAX_CN_MODELS } from "./providers/minimax-cn.models.ts"; +import { MISTRAL_MODELS } from "./providers/mistral.models.ts"; +import { MOONSHOTAI_MODELS } from "./providers/moonshotai.models.ts"; +import { MOONSHOTAI_CN_MODELS } from "./providers/moonshotai-cn.models.ts"; +import { NVIDIA_MODELS } from "./providers/nvidia.models.ts"; +import { OPENAI_MODELS } from "./providers/openai.models.ts"; +import { OPENAI_CODEX_MODELS } from "./providers/openai-codex.models.ts"; +import { OPENCODE_MODELS } from "./providers/opencode.models.ts"; +import { OPENCODE_GO_MODELS } from "./providers/opencode-go.models.ts"; +import { OPENROUTER_MODELS } from "./providers/openrouter.models.ts"; +import { TOGETHER_MODELS } from "./providers/together.models.ts"; +import { VERCEL_AI_GATEWAY_MODELS } from "./providers/vercel-ai-gateway.models.ts"; +import { XAI_MODELS } from "./providers/xai.models.ts"; +import { XIAOMI_MODELS } from "./providers/xiaomi.models.ts"; +import { XIAOMI_TOKEN_PLAN_AMS_MODELS } from "./providers/xiaomi-token-plan-ams.models.ts"; +import { XIAOMI_TOKEN_PLAN_CN_MODELS } from "./providers/xiaomi-token-plan-cn.models.ts"; +import { XIAOMI_TOKEN_PLAN_SGP_MODELS } from "./providers/xiaomi-token-plan-sgp.models.ts"; +import { ZAI_MODELS } from "./providers/zai.models.ts"; +import { ZAI_CODING_CN_MODELS } from "./providers/zai-coding-cn.models.ts"; export const MODELS = { - "amazon-bedrock": { - "amazon.nova-2-lite-v1:0": { - id: "amazon.nova-2-lite-v1:0", - name: "Nova 2 Lite", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.33, - output: 2.75, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "amazon.nova-lite-v1:0": { - id: "amazon.nova-lite-v1:0", - name: "Nova Lite", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.06, - output: 0.24, - cacheRead: 0.015, - cacheWrite: 0, - }, - contextWindow: 300000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "amazon.nova-micro-v1:0": { - id: "amazon.nova-micro-v1:0", - name: "Nova Micro", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.035, - output: 0.14, - cacheRead: 0.00875, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "amazon.nova-pro-v1:0": { - id: "amazon.nova-pro-v1:0", - name: "Nova Pro", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 3.2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 300000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-haiku-4-5-20251001-v1:0": { - id: "anthropic.claude-haiku-4-5-20251001-v1:0", - name: "Claude Haiku 4.5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-opus-4-1-20250805-v1:0": { - id: "anthropic.claude-opus-4-1-20250805-v1:0", - name: "Claude Opus 4.1", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-opus-4-5-20251101-v1:0": { - id: "anthropic.claude-opus-4-5-20251101-v1:0", - name: "Claude Opus 4.5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-opus-4-6-v1": { - id: "anthropic.claude-opus-4-6-v1", - name: "Claude Opus 4.6", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-opus-4-7": { - id: "anthropic.claude-opus-4-7", - name: "Claude Opus 4.7", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-opus-4-8": { - id: "anthropic.claude-opus-4-8", - name: "Claude Opus 4.8", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-sonnet-4-6": { - id: "anthropic.claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "au.anthropic.claude-haiku-4-5-20251001-v1:0": { - id: "au.anthropic.claude-haiku-4-5-20251001-v1:0", - name: "Claude Haiku 4.5 (AU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "au.anthropic.claude-opus-4-6-v1": { - id: "au.anthropic.claude-opus-4-6-v1", - name: "AU Anthropic Claude Opus 4.6", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 16.5, - output: 82.5, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "au.anthropic.claude-opus-4-8": { - id: "au.anthropic.claude-opus-4-8", - name: "Claude Opus 4.8 (AU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "au.anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "au.anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5 (AU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "au.anthropic.claude-sonnet-4-6": { - id: "au.anthropic.claude-sonnet-4-6", - name: "AU Anthropic Claude Sonnet 4.6", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3.3, - output: 16.5, - cacheRead: 0.33, - cacheWrite: 4.125, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "deepseek.r1-v1:0": { - id: "deepseek.r1-v1:0", - name: "DeepSeek-R1", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 1.35, - output: 5.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 32768, - } satisfies Model<"bedrock-converse-stream">, - "deepseek.v3-v1:0": { - id: "deepseek.v3-v1:0", - name: "DeepSeek-V3.1", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.58, - output: 1.68, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 81920, - } satisfies Model<"bedrock-converse-stream">, - "deepseek.v3.2": { - id: "deepseek.v3.2", - name: "DeepSeek-V3.2", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.62, - output: 1.85, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 81920, - } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-fable-5": { - id: "eu.anthropic.claude-fable-5", - name: "Claude Fable 5 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 11, - output: 55, - cacheRead: 1.1, - cacheWrite: 13.75, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-haiku-4-5-20251001-v1:0": { - id: "eu.anthropic.claude-haiku-4-5-20251001-v1:0", - name: "Claude Haiku 4.5 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-opus-4-5-20251101-v1:0": { - id: "eu.anthropic.claude-opus-4-5-20251101-v1:0", - name: "Claude Opus 4.5 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-opus-4-6-v1": { - id: "eu.anthropic.claude-opus-4-6-v1", - name: "Claude Opus 4.6 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 5.5, - output: 27.5, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-opus-4-7": { - id: "eu.anthropic.claude-opus-4-7", - name: "Claude Opus 4.7 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5.5, - output: 27.5, - cacheRead: 0.55, - cacheWrite: 6.875, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-opus-4-8": { - id: "eu.anthropic.claude-opus-4-8", - name: "Claude Opus 4.8 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5.5, - output: 27.5, - cacheRead: 0.55, - cacheWrite: 6.875, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3.3, - output: 16.5, - cacheRead: 0.33, - cacheWrite: 4.125, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-sonnet-4-6": { - id: "eu.anthropic.claude-sonnet-4-6", - name: "Claude Sonnet 4.6 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3.3, - output: 16.5, - cacheRead: 0.33, - cacheWrite: 4.125, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-fable-5": { - id: "global.anthropic.claude-fable-5", - name: "Claude Fable 5 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-haiku-4-5-20251001-v1:0": { - id: "global.anthropic.claude-haiku-4-5-20251001-v1:0", - name: "Claude Haiku 4.5 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-opus-4-5-20251101-v1:0": { - id: "global.anthropic.claude-opus-4-5-20251101-v1:0", - name: "Claude Opus 4.5 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-opus-4-6-v1": { - id: "global.anthropic.claude-opus-4-6-v1", - name: "Claude Opus 4.6 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-opus-4-7": { - id: "global.anthropic.claude-opus-4-7", - name: "Claude Opus 4.7 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-opus-4-8": { - id: "global.anthropic.claude-opus-4-8", - name: "Claude Opus 4.8 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "global.anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-sonnet-4-6": { - id: "global.anthropic.claude-sonnet-4-6", - name: "Claude Sonnet 4.6 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "google.gemma-3-27b-it": { - id: "google.gemma-3-27b-it", - name: "Google Gemma 3 27B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.12, - output: 0.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "google.gemma-3-4b-it": { - id: "google.gemma-3-4b-it", - name: "Gemma 3 4B IT", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.04, - output: 0.08, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "jp.anthropic.claude-opus-4-7": { - id: "jp.anthropic.claude-opus-4-7", - name: "Claude Opus 4.7 (JP)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "jp.anthropic.claude-opus-4-8": { - id: "jp.anthropic.claude-opus-4-8", - name: "Claude Opus 4.8 (JP)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "jp.anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "jp.anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5 (JP)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "jp.anthropic.claude-sonnet-4-6": { - id: "jp.anthropic.claude-sonnet-4-6", - name: "Claude Sonnet 4.6 (JP)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-1-70b-instruct-v1:0": { - id: "meta.llama3-1-70b-instruct-v1:0", - name: "Llama 3.1 70B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.72, - output: 0.72, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-1-8b-instruct-v1:0": { - id: "meta.llama3-1-8b-instruct-v1:0", - name: "Llama 3.1 8B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.22, - output: 0.22, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-3-70b-instruct-v1:0": { - id: "meta.llama3-3-70b-instruct-v1:0", - name: "Llama 3.3 70B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.72, - output: 0.72, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama4-maverick-17b-instruct-v1:0": { - id: "meta.llama4-maverick-17b-instruct-v1:0", - name: "Llama 4 Maverick 17B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.24, - output: 0.97, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama4-scout-17b-instruct-v1:0": { - id: "meta.llama4-scout-17b-instruct-v1:0", - name: "Llama 4 Scout 17B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.17, - output: 0.66, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 3500000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "minimax.minimax-m2": { - id: "minimax.minimax-m2", - name: "MiniMax M2", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204608, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "minimax.minimax-m2.1": { - id: "minimax.minimax-m2.1", - name: "MiniMax M2.1", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"bedrock-converse-stream">, - "minimax.minimax-m2.5": { - id: "minimax.minimax-m2.5", - name: "MiniMax M2.5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 196608, - maxTokens: 98304, - } satisfies Model<"bedrock-converse-stream">, - "mistral.devstral-2-123b": { - id: "mistral.devstral-2-123b", - name: "Devstral 2 123B", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "mistral.magistral-small-2509": { - id: "mistral.magistral-small-2509", - name: "Magistral Small 1.2", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 40000, - } satisfies Model<"bedrock-converse-stream">, - "mistral.ministral-3-14b-instruct": { - id: "mistral.ministral-3-14b-instruct", - name: "Ministral 14B 3.0", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.2, - output: 0.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "mistral.ministral-3-3b-instruct": { - id: "mistral.ministral-3-3b-instruct", - name: "Ministral 3 3B", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "mistral.ministral-3-8b-instruct": { - id: "mistral.ministral-3-8b-instruct", - name: "Ministral 3 8B", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "mistral.mistral-large-3-675b-instruct": { - id: "mistral.mistral-large-3-675b-instruct", - name: "Mistral Large 3", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "mistral.pixtral-large-2502-v1:0": { - id: "mistral.pixtral-large-2502-v1:0", - name: "Pixtral Large (25.02)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "mistral.voxtral-mini-3b-2507": { - id: "mistral.voxtral-mini-3b-2507", - name: "Voxtral Mini 3B 2507", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.04, - output: 0.04, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "mistral.voxtral-small-24b-2507": { - id: "mistral.voxtral-small-24b-2507", - name: "Voxtral Small 24B 2507", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.35, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "moonshot.kimi-k2-thinking": { - id: "moonshot.kimi-k2-thinking", - name: "Kimi K2 Thinking", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262143, - maxTokens: 16000, - } satisfies Model<"bedrock-converse-stream">, - "moonshotai.kimi-k2.5": { - id: "moonshotai.kimi-k2.5", - name: "Kimi K2.5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262143, - maxTokens: 16000, - } satisfies Model<"bedrock-converse-stream">, - "nvidia.nemotron-nano-12b-v2": { - id: "nvidia.nemotron-nano-12b-v2", - name: "NVIDIA Nemotron Nano 12B v2 VL BF16", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.2, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "nvidia.nemotron-nano-3-30b": { - id: "nvidia.nemotron-nano-3-30b", - name: "NVIDIA Nemotron Nano 3 30B", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.06, - output: 0.24, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "nvidia.nemotron-nano-9b-v2": { - id: "nvidia.nemotron-nano-9b-v2", - name: "NVIDIA Nemotron Nano 9B v2", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.06, - output: 0.23, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "nvidia.nemotron-super-3-120b": { - id: "nvidia.nemotron-super-3-120b", - name: "NVIDIA Nemotron 3 Super 120B A12B", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.65, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"bedrock-converse-stream">, - "openai.gpt-5.4": { - id: "openai.gpt-5.4", - name: "GPT-5.4", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 2.75, - output: 16.5, - cacheRead: 0.275, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "openai.gpt-5.5": { - id: "openai.gpt-5.5", - name: "GPT-5.5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5.5, - output: 33, - cacheRead: 0.55, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "openai.gpt-oss-120b": { - id: "openai.gpt-oss-120b", - name: "gpt-oss-120b", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "openai.gpt-oss-120b-1:0": { - id: "openai.gpt-oss-120b-1:0", - name: "gpt-oss-120b", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "openai.gpt-oss-20b": { - id: "openai.gpt-oss-20b", - name: "gpt-oss-20b", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.07, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "openai.gpt-oss-20b-1:0": { - id: "openai.gpt-oss-20b-1:0", - name: "gpt-oss-20b", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.07, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "openai.gpt-oss-safeguard-120b": { - id: "openai.gpt-oss-safeguard-120b", - name: "GPT OSS Safeguard 120B", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "openai.gpt-oss-safeguard-20b": { - id: "openai.gpt-oss-safeguard-20b", - name: "GPT OSS Safeguard 20B", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.07, - output: 0.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "qwen.qwen3-235b-a22b-2507-v1:0": { - id: "qwen.qwen3-235b-a22b-2507-v1:0", - name: "Qwen3 235B A22B 2507", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.22, - output: 0.88, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"bedrock-converse-stream">, - "qwen.qwen3-32b-v1:0": { - id: "qwen.qwen3-32b-v1:0", - name: "Qwen3 32B (dense)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 16384, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "qwen.qwen3-coder-30b-a3b-v1:0": { - id: "qwen.qwen3-coder-30b-a3b-v1:0", - name: "Qwen3 Coder 30B A3B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"bedrock-converse-stream">, - "qwen.qwen3-coder-480b-a35b-v1:0": { - id: "qwen.qwen3-coder-480b-a35b-v1:0", - name: "Qwen3 Coder 480B A35B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.22, - output: 1.8, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"bedrock-converse-stream">, - "qwen.qwen3-coder-next": { - id: "qwen.qwen3-coder-next", - name: "Qwen3 Coder Next", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.22, - output: 1.8, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"bedrock-converse-stream">, - "qwen.qwen3-next-80b-a3b": { - id: "qwen.qwen3-next-80b-a3b", - name: "Qwen/Qwen3-Next-80B-A3B-Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.14, - output: 1.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262000, - maxTokens: 262000, - } satisfies Model<"bedrock-converse-stream">, - "qwen.qwen3-vl-235b-a22b": { - id: "qwen.qwen3-vl-235b-a22b", - name: "Qwen/Qwen3-VL-235B-A22B-Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.3, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262000, - maxTokens: 262000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-fable-5": { - id: "us.anthropic.claude-fable-5", - name: "Claude Fable 5 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-haiku-4-5-20251001-v1:0": { - id: "us.anthropic.claude-haiku-4-5-20251001-v1:0", - name: "Claude Haiku 4.5 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-opus-4-1-20250805-v1:0": { - id: "us.anthropic.claude-opus-4-1-20250805-v1:0", - name: "Claude Opus 4.1 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-opus-4-5-20251101-v1:0": { - id: "us.anthropic.claude-opus-4-5-20251101-v1:0", - name: "Claude Opus 4.5 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-opus-4-6-v1": { - id: "us.anthropic.claude-opus-4-6-v1", - name: "Claude Opus 4.6 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-opus-4-7": { - id: "us.anthropic.claude-opus-4-7", - name: "Claude Opus 4.7 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-opus-4-8": { - id: "us.anthropic.claude-opus-4-8", - name: "Claude Opus 4.8 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-sonnet-4-6": { - id: "us.anthropic.claude-sonnet-4-6", - name: "Claude Sonnet 4.6 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "us.deepseek.r1-v1:0": { - id: "us.deepseek.r1-v1:0", - name: "DeepSeek-R1 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 1.35, - output: 5.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 32768, - } satisfies Model<"bedrock-converse-stream">, - "us.meta.llama4-maverick-17b-instruct-v1:0": { - id: "us.meta.llama4-maverick-17b-instruct-v1:0", - name: "Llama 4 Maverick 17B Instruct (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.24, - output: 0.97, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "us.meta.llama4-scout-17b-instruct-v1:0": { - id: "us.meta.llama4-scout-17b-instruct-v1:0", - name: "Llama 4 Scout 17B Instruct (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.17, - output: 0.66, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 3500000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "writer.palmyra-x4-v1:0": { - id: "writer.palmyra-x4-v1:0", - name: "Palmyra X4", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 2.5, - output: 10, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 122880, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "writer.palmyra-x5-v1:0": { - id: "writer.palmyra-x5-v1:0", - name: "Palmyra X5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1040000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "zai.glm-4.7": { - id: "zai.glm-4.7", - name: "GLM-4.7", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"bedrock-converse-stream">, - "zai.glm-4.7-flash": { - id: "zai.glm-4.7-flash", - name: "GLM-4.7-Flash", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.07, - output: 0.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 131072, - } satisfies Model<"bedrock-converse-stream">, - "zai.glm-5": { - id: "zai.glm-5", - name: "GLM-5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 101376, - } satisfies Model<"bedrock-converse-stream">, - }, - "ant-ling": { - "Ling-2.6-1T": { - id: "Ling-2.6-1T", - name: "Ling 2.6 1T", - api: "openai-completions", - provider: "ant-ling", - baseUrl: "https://api.ant-ling.com/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0.06, - output: 0.25, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "Ling-2.6-flash": { - id: "Ling-2.6-flash", - name: "Ling 2.6 Flash", - api: "openai-completions", - provider: "ant-ling", - baseUrl: "https://api.ant-ling.com/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0.01, - output: 0.02, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "Ring-2.6-1T": { - id: "Ring-2.6-1T", - name: "Ring 2.6 1T", - api: "openai-completions", - provider: "ant-ling", - baseUrl: "https://api.ant-ling.com/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false,"thinkingFormat":"ant-ling"}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh"}, - input: ["text"], - cost: { - input: 0.06, - output: 0.25, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - }, - "anthropic": { - "claude-3-5-haiku-20241022": { - id: "claude-3-5-haiku-20241022", - name: "Claude Haiku 3.5", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "claude-3-5-haiku-latest": { - id: "claude-3-5-haiku-latest", - name: "Claude Haiku 3.5 (latest)", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "claude-3-5-sonnet-20240620": { - id: "claude-3-5-sonnet-20240620", - name: "Claude Sonnet 3.5", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "claude-3-5-sonnet-20241022": { - id: "claude-3-5-sonnet-20241022", - name: "Claude Sonnet 3.5 v2", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "claude-3-7-sonnet-20250219": { - id: "claude-3-7-sonnet-20250219", - name: "Claude Sonnet 3.7", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-3-haiku-20240307": { - id: "claude-3-haiku-20240307", - name: "Claude Haiku 3", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.25, - cacheRead: 0.03, - cacheWrite: 0.3, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"anthropic-messages">, - "claude-3-opus-20240229": { - id: "claude-3-opus-20240229", - name: "Claude Opus 3", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"anthropic-messages">, - "claude-3-sonnet-20240229": { - id: "claude-3-sonnet-20240229", - name: "Claude Sonnet 3", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 0.3, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"anthropic-messages">, - "claude-fable-5": { - id: "claude-fable-5", - name: "Claude Fable 5", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "Claude Haiku 4.5 (latest)", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-haiku-4-5-20251001": { - id: "claude-haiku-4-5-20251001", - name: "Claude Haiku 4.5", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-0": { - id: "claude-opus-4-0", - name: "Claude Opus 4 (latest)", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-1": { - id: "claude-opus-4-1", - name: "Claude Opus 4.1 (latest)", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-1-20250805": { - id: "claude-opus-4-1-20250805", - name: "Claude Opus 4.1", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-20250514": { - id: "claude-opus-4-20250514", - name: "Claude Opus 4", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-5": { - id: "claude-opus-4-5", - name: "Claude Opus 4.5 (latest)", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-5-20251101": { - id: "claude-opus-4-5-20251101", - name: "Claude Opus 4.5", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-7": { - id: "claude-opus-4-7", - name: "Claude Opus 4.7", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-8": { - id: "claude-opus-4-8", - name: "Claude Opus 4.8", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-0": { - id: "claude-sonnet-4-0", - name: "Claude Sonnet 4 (latest)", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-20250514": { - id: "claude-sonnet-4-20250514", - name: "Claude Sonnet 4", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-5": { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5 (latest)", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-5-20250929": { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - }, - "azure-openai-responses": { - "gpt-4": { - id: "gpt-4", - name: "GPT-4", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text"], - cost: { - input: 30, - output: 60, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 8192, - } satisfies Model<"azure-openai-responses">, - "gpt-4-turbo": { - id: "gpt-4-turbo", - name: "GPT-4 Turbo", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 10, - output: 30, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"azure-openai-responses">, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT-4.1", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"azure-openai-responses">, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "GPT-4.1 mini", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.4, - output: 1.6, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"azure-openai-responses">, - "gpt-4.1-nano": { - id: "gpt-4.1-nano", - name: "GPT-4.1 nano", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"azure-openai-responses">, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-4o-2024-05-13": { - id: "gpt-4o-2024-05-13", - name: "GPT-4o (2024-05-13)", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 5, - output: 15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"azure-openai-responses">, - "gpt-4o-2024-08-06": { - id: "gpt-4o-2024-08-06", - name: "GPT-4o (2024-08-06)", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-4o-2024-11-20": { - id: "gpt-4o-2024-11-20", - name: "GPT-4o (2024-11-20)", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-4o-mini": { - id: "gpt-4o-mini", - name: "GPT-4o mini", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5-chat-latest": { - id: "gpt-5-chat-latest", - name: "GPT-5 Chat Latest", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "GPT-5-Codex", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5 Mini", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "GPT-5 Nano", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.05, - output: 0.4, - cacheRead: 0.005, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5-pro": { - id: "gpt-5-pro", - name: "GPT-5 Pro", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 15, - output: 120, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.1-chat-latest": { - id: "gpt-5.1-chat-latest", - name: "GPT-5.1 Chat", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex mini", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.2-chat-latest": { - id: "gpt-5.2-chat-latest", - name: "GPT-5.2 Chat", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.2-pro": { - id: "gpt-5.2-pro", - name: "GPT-5.2 Pro", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 21, - output: 168, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.3-chat-latest": { - id: "gpt-5.3-chat-latest", - name: "GPT-5.3 Chat (latest)", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.3-codex-spark": { - id: "gpt-5.3-codex-spark", - name: "GPT-5.3 Codex Spark", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 32000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 mini", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.75, - output: 4.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "GPT-5.4 nano", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.2, - output: 1.25, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.4-pro": { - id: "gpt-5.4-pro", - name: "GPT-5.4 Pro", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.5": { - id: "gpt-5.5", - name: "GPT-5.5", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.5-pro": { - id: "gpt-5.5-pro", - name: "GPT-5.5 Pro", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh","minimal":null,"low":null}, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "o1": { - id: "o1", - name: "o1", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 60, - cacheRead: 7.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - "o1-pro": { - id: "o1-pro", - name: "o1-pro", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 150, - output: 600, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - "o3": { - id: "o3", - name: "o3", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - "o3-deep-research": { - id: "o3-deep-research", - name: "o3-deep-research", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 10, - output: 40, - cacheRead: 2.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - "o3-mini": { - id: "o3-mini", - name: "o3-mini", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.55, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - "o3-pro": { - id: "o3-pro", - name: "o3-pro", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 20, - output: 80, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - "o4-mini": { - id: "o4-mini", - name: "o4-mini", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.275, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - "o4-mini-deep-research": { - id: "o4-mini-deep-research", - name: "o4-mini-deep-research", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - }, - "cerebras": { - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "GPT OSS 120B", - api: "openai-completions", - provider: "cerebras", - baseUrl: "https://api.cerebras.ai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.25, - output: 0.69, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "llama3.1-8b": { - id: "llama3.1-8b", - name: "Llama 3.1 8B", - api: "openai-completions", - provider: "cerebras", - baseUrl: "https://api.cerebras.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.1, - output: 0.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32000, - maxTokens: 8000, - } satisfies Model<"openai-completions">, - "zai-glm-4.7": { - id: "zai-glm-4.7", - name: "Z.AI GLM-4.7", - api: "openai-completions", - provider: "cerebras", - baseUrl: "https://api.cerebras.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2.25, - output: 2.75, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 40000, - } satisfies Model<"openai-completions">, - }, - "cloudflare-ai-gateway": { - "claude-3-5-haiku": { - id: "claude-3-5-haiku", - name: "Claude Haiku 3.5 (latest)", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "claude-3-haiku": { - id: "claude-3-haiku", - name: "Claude Haiku 3", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.25, - cacheRead: 0.03, - cacheWrite: 0.3, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"anthropic-messages">, - "claude-3-opus": { - id: "claude-3-opus", - name: "Claude Opus 3", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: false, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"anthropic-messages">, - "claude-3-sonnet": { - id: "claude-3-sonnet", - name: "Claude Sonnet 3", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 0.3, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"anthropic-messages">, - "claude-3.5-haiku": { - id: "claude-3.5-haiku", - name: "Claude Haiku 3.5 (latest)", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "claude-3.5-sonnet": { - id: "claude-3.5-sonnet", - name: "Claude Sonnet 3.5 v2", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "claude-fable-5": { - id: "claude-fable-5", - name: "Claude Fable 5", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "Claude Haiku 4.5 (latest)", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4": { - id: "claude-opus-4", - name: "Claude Opus 4 (latest)", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-1": { - id: "claude-opus-4-1", - name: "Claude Opus 4.1 (latest)", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-5": { - id: "claude-opus-4-5", - name: "Claude Opus 4.5 (latest)", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6 (latest)", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-7": { - id: "claude-opus-4-7", - name: "Claude Opus 4.7", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-8": { - id: "claude-opus-4-8", - name: "Claude Opus 4.8", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4": { - id: "claude-sonnet-4", - name: "Claude Sonnet 4 (latest)", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-5": { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5 (latest)", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "gpt-4": { - id: "gpt-4", - name: "GPT-4", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: false, - input: ["text"], - cost: { - input: 30, - output: 60, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 8192, - } satisfies Model<"openai-responses">, - "gpt-4-turbo": { - id: "gpt-4-turbo", - name: "GPT-4 Turbo", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 10, - output: 30, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-responses">, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-4o-mini": { - id: "gpt-4o-mini", - name: "GPT-4o mini", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.13, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.5": { - id: "gpt-5.5", - name: "GPT-5.5", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "o1": { - id: "o1", - name: "o1", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 60, - cacheRead: 7.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o3": { - id: "o3", - name: "o3", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o3-mini": { - id: "o3-mini", - name: "o3-mini", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - input: ["text"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.55, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o3-pro": { - id: "o3-pro", - name: "o3-pro", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 20, - output: 80, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o4-mini": { - id: "o4-mini", - name: "o4-mini", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.28, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "workers-ai/@cf/moonshotai/kimi-k2.5": { - id: "workers-ai/@cf/moonshotai/kimi-k2.5", - name: "Kimi K2.5", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"openai-completions">, - "workers-ai/@cf/moonshotai/kimi-k2.6": { - id: "workers-ai/@cf/moonshotai/kimi-k2.6", - name: "Kimi K2.6", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.16, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"openai-completions">, - "workers-ai/@cf/nvidia/nemotron-3-120b-a12b": { - id: "workers-ai/@cf/nvidia/nemotron-3-120b-a12b", - name: "Nemotron 3 Super 120B", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"openai-completions">, - "workers-ai/@cf/zai-org/glm-4.7-flash": { - id: "workers-ai/@cf/zai-org/glm-4.7-flash", - name: "GLM-4.7-Flash", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0.06, - output: 0.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, - "cloudflare-workers-ai": { - "@cf/google/gemma-4-26b-a4b-it": { - id: "@cf/google/gemma-4-26b-a4b-it", - name: "Gemma 4 26B A4B IT", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "@cf/ibm-granite/granite-4.0-h-micro": { - id: "@cf/ibm-granite/granite-4.0-h-micro", - name: "Granite 4.0 H Micro", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: false, - input: ["text"], - cost: { - input: 0.017, - output: 0.112, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131000, - maxTokens: 131000, - } satisfies Model<"openai-completions">, - "@cf/meta/llama-3.3-70b-instruct-fp8-fast": { - id: "@cf/meta/llama-3.3-70b-instruct-fp8-fast", - name: "Llama 3.3 70B Instruct fp8 Fast", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: false, - input: ["text"], - cost: { - input: 0.293, - output: 2.253, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 24000, - maxTokens: 24000, - } satisfies Model<"openai-completions">, - "@cf/meta/llama-4-scout-17b-16e-instruct": { - id: "@cf/meta/llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout 17B 16E Instruct", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.27, - output: 0.85, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "@cf/mistralai/mistral-small-3.1-24b-instruct": { - id: "@cf/mistralai/mistral-small-3.1-24b-instruct", - name: "Mistral Small 3.1 24B Instruct", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: false, - input: ["text"], - cost: { - input: 0.351, - output: 0.555, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "@cf/moonshotai/kimi-k2.6": { - id: "@cf/moonshotai/kimi-k2.6", - name: "Kimi K2.6", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.16, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 256000, - } satisfies Model<"openai-completions">, - "@cf/nvidia/nemotron-3-120b-a12b": { - id: "@cf/nvidia/nemotron-3-120b-a12b", - name: "Nemotron 3 Super 120B", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"openai-completions">, - "@cf/openai/gpt-oss-120b": { - id: "@cf/openai/gpt-oss-120b", - name: "GPT OSS 120B", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0.35, - output: 0.75, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "@cf/openai/gpt-oss-20b": { - id: "@cf/openai/gpt-oss-20b", - name: "GPT OSS 20B", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0.2, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "@cf/qwen/qwen3-30b-a3b-fp8": { - id: "@cf/qwen/qwen3-30b-a3b-fp8", - name: "Qwen3 30B A3b fp8", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0.0509, - output: 0.335, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "@cf/zai-org/glm-4.7-flash": { - id: "@cf/zai-org/glm-4.7-flash", - name: "GLM-4.7-Flash", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0.0605, - output: 0.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, - "deepseek": { - "deepseek-v4-flash": { - id: "deepseek-v4-flash", - name: "DeepSeek V4 Flash", - api: "openai-completions", - provider: "deepseek", - baseUrl: "https://api.deepseek.com", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, - input: ["text"], - cost: { - input: 0.14, - output: 0.28, - cacheRead: 0.0028, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 384000, - } satisfies Model<"openai-completions">, - "deepseek-v4-pro": { - id: "deepseek-v4-pro", - name: "DeepSeek V4 Pro", - api: "openai-completions", - provider: "deepseek", - baseUrl: "https://api.deepseek.com", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, - input: ["text"], - cost: { - input: 0.435, - output: 0.87, - cacheRead: 0.003625, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 384000, - } satisfies Model<"openai-completions">, - }, - "fireworks": { - "accounts/fireworks/models/deepseek-v4-flash": { - id: "accounts/fireworks/models/deepseek-v4-flash", - name: "DeepSeek V4 Flash", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.14, - output: 0.28, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 384000, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/deepseek-v4-pro": { - id: "accounts/fireworks/models/deepseek-v4-pro", - name: "DeepSeek V4 Pro", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 1.74, - output: 3.48, - cacheRead: 0.145, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 384000, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/glm-5p1": { - id: "accounts/fireworks/models/glm-5p1", - name: "GLM 5.1", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 1.4, - output: 4.4, - cacheRead: 0.26, - cacheWrite: 0, - }, - contextWindow: 202800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/gpt-oss-120b": { - id: "accounts/fireworks/models/gpt-oss-120b", - name: "GPT OSS 120B", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.015, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/gpt-oss-20b": { - id: "accounts/fireworks/models/gpt-oss-20b", - name: "GPT OSS 20B", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.07, - output: 0.3, - cacheRead: 0.035, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/kimi-k2p5": { - id: "accounts/fireworks/models/kimi-k2p5", - name: "Kimi K2.5", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/kimi-k2p6": { - id: "accounts/fireworks/models/kimi-k2p6", - name: "Kimi K2.6", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.16, - cacheWrite: 0, - }, - contextWindow: 262000, - maxTokens: 262000, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/minimax-m2p5": { - id: "accounts/fireworks/models/minimax-m2p5", - name: "MiniMax-M2.5", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 196608, - maxTokens: 196608, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/minimax-m2p7": { - id: "accounts/fireworks/models/minimax-m2p7", - name: "MiniMax-M2.7", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 196608, - maxTokens: 196608, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/qwen3p6-plus": { - id: "accounts/fireworks/models/qwen3p6-plus", - name: "Qwen 3.6 Plus", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/routers/glm-5p1-fast": { - id: "accounts/fireworks/routers/glm-5p1-fast", - name: "GLM 5.1 Fast", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 2.8, - output: 8.8, - cacheRead: 0.52, - cacheWrite: 0, - }, - contextWindow: 202800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/routers/kimi-k2p6-fast": { - id: "accounts/fireworks/routers/kimi-k2p6-fast", - name: "Kimi K2.6 Fast", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.3, - cacheWrite: 0, - }, - contextWindow: 262000, - maxTokens: 262000, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/routers/kimi-k2p6-turbo": { - id: "accounts/fireworks/routers/kimi-k2p6-turbo", - name: "Kimi K2.6 Turbo", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.3, - cacheWrite: 0, - }, - contextWindow: 262000, - maxTokens: 262000, - } satisfies Model<"anthropic-messages">, - }, - "github-copilot": { - "claude-haiku-4.5": { - id: "claude-haiku-4.5", - name: "Claude Haiku 4.5 (latest)", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsEagerToolInputStreaming":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4.5": { - id: "claude-opus-4.5", - name: "Claude Opus 4.5 (latest)", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4.6": { - id: "claude-opus-4.6", - name: "Claude Opus 4.6", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4.7": { - id: "claude-opus-4.7", - name: "Claude Opus 4.7", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4.8": { - id: "claude-opus-4.8", - name: "Claude Opus 4.8", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4": { - id: "claude-sonnet-4", - name: "Claude Sonnet 4 (latest)", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsEagerToolInputStreaming":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 216000, - maxTokens: 16000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4.5": { - id: "claude-sonnet-4.5", - name: "Claude Sonnet 4.5 (latest)", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsEagerToolInputStreaming":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4.6": { - id: "claude-sonnet-4.6", - name: "Claude Sonnet 4.6", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "gemini-3.5-flash": { - id: "gemini-3.5-flash", - name: "Gemini 3.5 Flash", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.5, - output: 9, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT-4.1", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5 Mini", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"low"}, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 264000, - maxTokens: 64000, - } satisfies Model<"openai-responses">, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 mini", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.75, - output: 4.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "GPT-5.4 nano", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.2, - output: 1.25, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.5": { - id: "gpt-5.5", - name: "GPT-5.5", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "raptor-mini": { - id: "raptor-mini", - name: "Raptor mini", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - }, - "google": { - "gemini-2.0-flash": { - id: "gemini-2.0-flash", - name: "Gemini 2.0 Flash", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 8192, - } satisfies Model<"google-generative-ai">, - "gemini-2.0-flash-lite": { - id: "gemini-2.0-flash-lite", - name: "Gemini 2.0 Flash-Lite", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 8192, - } satisfies Model<"google-generative-ai">, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 2.5, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-2.5-flash-lite": { - id: "gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash-Lite", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-3.1-flash-lite": { - id: "gemini-3.1-flash-lite", - name: "Gemini 3.1 Flash Lite", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.5, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-3.1-flash-lite-preview": { - id: "gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.5, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-3.1-pro-preview-customtools": { - id: "gemini-3.1-pro-preview-customtools", - name: "Gemini 3.1 Pro Preview Custom Tools", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-3.5-flash": { - id: "gemini-3.5-flash", - name: "Gemini 3.5 Flash", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.5, - output: 9, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-flash-latest": { - id: "gemini-flash-latest", - name: "Gemini Flash Latest", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 2.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-flash-lite-latest": { - id: "gemini-flash-lite-latest", - name: "Gemini Flash-Lite Latest", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemma-4-26b-a4b-it": { - id: "gemma-4-26b-a4b-it", - name: "Gemma 4 26B A4B IT", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"MINIMAL","low":null,"medium":null,"high":"HIGH"}, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"google-generative-ai">, - "gemma-4-31b-it": { - id: "gemma-4-31b-it", - name: "Gemma 4 31B IT", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"MINIMAL","low":null,"medium":null,"high":"HIGH"}, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"google-generative-ai">, - }, - "google-vertex": { - "gemini-1.5-flash": { - id: "gemini-1.5-flash", - name: "Gemini 1.5 Flash (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0.01875, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 8192, - } satisfies Model<"google-vertex">, - "gemini-1.5-flash-8b": { - id: "gemini-1.5-flash-8b", - name: "Gemini 1.5 Flash-8B (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.0375, - output: 0.15, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 8192, - } satisfies Model<"google-vertex">, - "gemini-1.5-pro": { - id: "gemini-1.5-pro", - name: "Gemini 1.5 Pro (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 1.25, - output: 5, - cacheRead: 0.3125, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 8192, - } satisfies Model<"google-vertex">, - "gemini-2.0-flash": { - id: "gemini-2.0-flash", - name: "Gemini 2.0 Flash (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.0375, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 8192, - } satisfies Model<"google-vertex">, - "gemini-2.0-flash-lite": { - id: "gemini-2.0-flash-lite", - name: "Gemini 2.0 Flash Lite (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0.01875, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 2.5, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - "gemini-2.5-flash-lite": { - id: "gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - "gemini-2.5-flash-lite-preview-09-2025": { - id: "gemini-2.5-flash-lite-preview-09-2025", - name: "Gemini 2.5 Flash Lite Preview 09-25 (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"google-vertex">, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - "gemini-3.1-pro-preview-customtools": { - id: "gemini-3.1-pro-preview-customtools", - name: "Gemini 3.1 Pro Preview Custom Tools (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - }, - "groq": { - "llama-3.1-8b-instant": { - id: "llama-3.1-8b-instant", - name: "Llama 3.1 8B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.05, - output: 0.08, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "llama-3.3-70b-versatile": { - id: "llama-3.3-70b-versatile", - name: "Llama 3.3 70B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.59, - output: 0.79, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "meta-llama/llama-4-scout-17b-16e-instruct": { - id: "meta-llama/llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout 17B 16E", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.11, - output: 0.34, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0.0375, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-safeguard-20b": { - id: "openai/gpt-oss-safeguard-20b", - name: "Safety GPT OSS 20B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0.037, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3-32b": { - id: "qwen/qwen3-32b", - name: "Qwen3-32B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"default"}, - input: ["text"], - cost: { - input: 0.29, - output: 0.59, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 40960, - } satisfies Model<"openai-completions">, - }, - "huggingface": { - "MiniMaxAI/MiniMax-M2.1": { - id: "MiniMaxAI/MiniMax-M2.1", - name: "MiniMax-M2.1", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax-M2.5", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "MiniMaxAI/MiniMax-M2.7": { - id: "MiniMaxAI/MiniMax-M2.7", - name: "MiniMax-M2.7", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3-235B-A22B-Thinking-2507": { - id: "Qwen/Qwen3-235B-A22B-Thinking-2507", - name: "Qwen3-235B-A22B-Thinking-2507", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3-Coder-480B-A35B-Instruct": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct", - name: "Qwen3-Coder-480B-A35B-Instruct", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 66536, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3-Coder-Next": { - id: "Qwen/Qwen3-Coder-Next", - name: "Qwen3-Coder-Next", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0.2, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3-Next-80B-A3B-Instruct": { - id: "Qwen/Qwen3-Next-80B-A3B-Instruct", - name: "Qwen3-Next-80B-A3B-Instruct", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0.25, - output: 1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 66536, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3-Next-80B-A3B-Thinking": { - id: "Qwen/Qwen3-Next-80B-A3B-Thinking", - name: "Qwen3-Next-80B-A3B-Thinking", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0.3, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3.5-397B-A17B": { - id: "Qwen/Qwen3.5-397B-A17B", - name: "Qwen3.5-397B-A17B", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "XiaomiMiMo/MiMo-V2-Flash": { - id: "XiaomiMiMo/MiMo-V2-Flash", - name: "MiMo-V2-Flash", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "deepseek-ai/DeepSeek-R1-0528": { - id: "deepseek-ai/DeepSeek-R1-0528", - name: "DeepSeek-R1-0528", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 3, - output: 5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 163840, - } satisfies Model<"openai-completions">, - "deepseek-ai/DeepSeek-V3.2": { - id: "deepseek-ai/DeepSeek-V3.2", - name: "DeepSeek-V3.2", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.28, - output: 0.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "deepseek-ai/DeepSeek-V4-Pro": { - id: "deepseek-ai/DeepSeek-V4-Pro", - name: "DeepSeek V4 Pro", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.435, - output: 0.87, - cacheRead: 0.003625, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 393216, - } satisfies Model<"openai-completions">, - "moonshotai/Kimi-K2-Instruct": { - id: "moonshotai/Kimi-K2-Instruct", - name: "Kimi-K2-Instruct", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "moonshotai/Kimi-K2-Instruct-0905": { - id: "moonshotai/Kimi-K2-Instruct-0905", - name: "Kimi-K2-Instruct-0905", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "moonshotai/Kimi-K2-Thinking": { - id: "moonshotai/Kimi-K2-Thinking", - name: "Kimi-K2-Thinking", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "Kimi-K2.5", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "moonshotai/Kimi-K2.6": { - id: "moonshotai/Kimi-K2.6", - name: "Kimi-K2.6", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.16, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "zai-org/GLM-4.7": { - id: "zai-org/GLM-4.7", - name: "GLM-4.7", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.2, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "zai-org/GLM-4.7-Flash": { - id: "zai-org/GLM-4.7-Flash", - name: "GLM-4.7-Flash", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "zai-org/GLM-5": { - id: "zai-org/GLM-5", - name: "GLM-5", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3.2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "zai-org/GLM-5.1": { - id: "zai-org/GLM-5.1", - name: "GLM-5.1", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3.2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, - "kimi-coding": { - "kimi-for-coding": { - id: "kimi-for-coding", - name: "Kimi For Coding", - api: "anthropic-messages", - provider: "kimi-coding", - baseUrl: "https://api.kimi.com/coding", - headers: {"User-Agent":"KimiCLI/1.5"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - api: "anthropic-messages", - provider: "kimi-coding", - baseUrl: "https://api.kimi.com/coding", - headers: {"User-Agent":"KimiCLI/1.5"}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - }, - "minimax": { - "MiniMax-M2.7": { - id: "MiniMax-M2.7", - name: "MiniMax-M2.7", - api: "anthropic-messages", - provider: "minimax", - baseUrl: "https://api.minimax.io/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "MiniMax-M2.7-highspeed": { - id: "MiniMax-M2.7-highspeed", - name: "MiniMax-M2.7-highspeed", - api: "anthropic-messages", - provider: "minimax", - baseUrl: "https://api.minimax.io/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.4, - cacheRead: 0.06, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "MiniMax-M3": { - id: "MiniMax-M3", - name: "MiniMax-M3", - api: "anthropic-messages", - provider: "minimax", - baseUrl: "https://api.minimax.io/anthropic", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 2.4, - cacheRead: 0.12, - cacheWrite: 0, - }, - contextWindow: 512000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - }, - "minimax-cn": { - "MiniMax-M2.7": { - id: "MiniMax-M2.7", - name: "MiniMax-M2.7", - api: "anthropic-messages", - provider: "minimax-cn", - baseUrl: "https://api.minimaxi.com/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "MiniMax-M2.7-highspeed": { - id: "MiniMax-M2.7-highspeed", - name: "MiniMax-M2.7-highspeed", - api: "anthropic-messages", - provider: "minimax-cn", - baseUrl: "https://api.minimaxi.com/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.4, - cacheRead: 0.06, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "MiniMax-M3": { - id: "MiniMax-M3", - name: "MiniMax-M3", - api: "anthropic-messages", - provider: "minimax-cn", - baseUrl: "https://api.minimaxi.com/anthropic", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 2.4, - cacheRead: 0.12, - cacheWrite: 0, - }, - contextWindow: 512000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - }, - "mistral": { - "codestral-latest": { - id: "codestral-latest", - name: "Codestral (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.3, - output: 0.9, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 4096, - } satisfies Model<"mistral-conversations">, - "devstral-2512": { - id: "devstral-2512", - name: "Devstral 2", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"mistral-conversations">, - "devstral-latest": { - id: "devstral-latest", - name: "Devstral 2", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"mistral-conversations">, - "devstral-medium-2507": { - id: "devstral-medium-2507", - name: "Devstral Medium", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "devstral-medium-latest": { - id: "devstral-medium-latest", - name: "Devstral 2 (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"mistral-conversations">, - "devstral-small-2505": { - id: "devstral-small-2505", - name: "Devstral Small 2505", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "devstral-small-2507": { - id: "devstral-small-2507", - name: "Devstral Small", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "labs-devstral-small-2512": { - id: "labs-devstral-small-2512", - name: "Devstral Small 2", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"mistral-conversations">, - "magistral-medium-latest": { - id: "magistral-medium-latest", - name: "Magistral Medium (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: true, - input: ["text"], - cost: { - input: 2, - output: 5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"mistral-conversations">, - "magistral-small": { - id: "magistral-small", - name: "Magistral Small", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: true, - input: ["text"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "ministral-3b-latest": { - id: "ministral-3b-latest", - name: "Ministral 3B (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.04, - output: 0.04, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "ministral-8b-latest": { - id: "ministral-8b-latest", - name: "Ministral 8B (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.1, - output: 0.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "mistral-large-2411": { - id: "mistral-large-2411", - name: "Mistral Large 2.1", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"mistral-conversations">, - "mistral-large-2512": { - id: "mistral-large-2512", - name: "Mistral Large 3", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"mistral-conversations">, - "mistral-large-latest": { - id: "mistral-large-latest", - name: "Mistral Large (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"mistral-conversations">, - "mistral-medium-2505": { - id: "mistral-medium-2505", - name: "Mistral Medium 3", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"mistral-conversations">, - "mistral-medium-2508": { - id: "mistral-medium-2508", - name: "Mistral Medium 3.1", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"mistral-conversations">, - "mistral-medium-2604": { - id: "mistral-medium-2604", - name: "Mistral Medium 3.5", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.5, - output: 7.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"mistral-conversations">, - "mistral-medium-3.5": { - id: "mistral-medium-3.5", - name: "Mistral Medium 3.5", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.5, - output: 7.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"mistral-conversations">, - "mistral-medium-latest": { - id: "mistral-medium-latest", - name: "Mistral Medium (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"mistral-conversations">, - "mistral-nemo": { - id: "mistral-nemo", - name: "Mistral Nemo", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "mistral-small-2506": { - id: "mistral-small-2506", - name: "Mistral Small 3.2", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"mistral-conversations">, - "mistral-small-2603": { - id: "mistral-small-2603", - name: "Mistral Small 4", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"mistral-conversations">, - "mistral-small-latest": { - id: "mistral-small-latest", - name: "Mistral Small (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"mistral-conversations">, - "open-mistral-7b": { - id: "open-mistral-7b", - name: "Mistral 7B", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.25, - output: 0.25, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8000, - maxTokens: 8000, - } satisfies Model<"mistral-conversations">, - "open-mistral-nemo": { - id: "open-mistral-nemo", - name: "Open Mistral Nemo", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "open-mixtral-8x22b": { - id: "open-mixtral-8x22b", - name: "Mixtral 8x22B", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 64000, - maxTokens: 64000, - } satisfies Model<"mistral-conversations">, - "open-mixtral-8x7b": { - id: "open-mixtral-8x7b", - name: "Mixtral 8x7B", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.7, - output: 0.7, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32000, - maxTokens: 32000, - } satisfies Model<"mistral-conversations">, - "pixtral-12b": { - id: "pixtral-12b", - name: "Pixtral 12B", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "pixtral-large-latest": { - id: "pixtral-large-latest", - name: "Pixtral Large (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - }, - "moonshotai": { - "kimi-k2-0711-preview": { - id: "kimi-k2-0711-preview", - name: "Kimi K2 0711", - api: "openai-completions", - provider: "moonshotai", - baseUrl: "https://api.moonshot.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: false, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "kimi-k2-0905-preview": { - id: "kimi-k2-0905-preview", - name: "Kimi K2 0905", - api: "openai-completions", - provider: "moonshotai", - baseUrl: "https://api.moonshot.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: false, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - api: "openai-completions", - provider: "moonshotai", - baseUrl: "https://api.moonshot.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2-thinking-turbo": { - id: "kimi-k2-thinking-turbo", - name: "Kimi K2 Thinking Turbo", - api: "openai-completions", - provider: "moonshotai", - baseUrl: "https://api.moonshot.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1.15, - output: 8, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2-turbo-preview": { - id: "kimi-k2-turbo-preview", - name: "Kimi K2 Turbo", - api: "openai-completions", - provider: "moonshotai", - baseUrl: "https://api.moonshot.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: false, - input: ["text"], - cost: { - input: 2.4, - output: 10, - cacheRead: 0.6, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - api: "openai-completions", - provider: "moonshotai", - baseUrl: "https://api.moonshot.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2.6": { - id: "kimi-k2.6", - name: "Kimi K2.6", - api: "openai-completions", - provider: "moonshotai", - baseUrl: "https://api.moonshot.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.16, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - }, - "moonshotai-cn": { - "kimi-k2-0711-preview": { - id: "kimi-k2-0711-preview", - name: "Kimi K2 0711", - api: "openai-completions", - provider: "moonshotai-cn", - baseUrl: "https://api.moonshot.cn/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: false, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "kimi-k2-0905-preview": { - id: "kimi-k2-0905-preview", - name: "Kimi K2 0905", - api: "openai-completions", - provider: "moonshotai-cn", - baseUrl: "https://api.moonshot.cn/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: false, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - api: "openai-completions", - provider: "moonshotai-cn", - baseUrl: "https://api.moonshot.cn/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2-thinking-turbo": { - id: "kimi-k2-thinking-turbo", - name: "Kimi K2 Thinking Turbo", - api: "openai-completions", - provider: "moonshotai-cn", - baseUrl: "https://api.moonshot.cn/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1.15, - output: 8, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2-turbo-preview": { - id: "kimi-k2-turbo-preview", - name: "Kimi K2 Turbo", - api: "openai-completions", - provider: "moonshotai-cn", - baseUrl: "https://api.moonshot.cn/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: false, - input: ["text"], - cost: { - input: 2.4, - output: 10, - cacheRead: 0.6, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - api: "openai-completions", - provider: "moonshotai-cn", - baseUrl: "https://api.moonshot.cn/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2.6": { - id: "kimi-k2.6", - name: "Kimi K2.6", - api: "openai-completions", - provider: "moonshotai-cn", - baseUrl: "https://api.moonshot.cn/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.16, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - }, - "nvidia": { - "meta/llama-3.1-70b-instruct": { - id: "meta/llama-3.1-70b-instruct", - name: "Llama 3.1 70b Instruct", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "meta/llama-3.1-8b-instruct": { - id: "meta/llama-3.1-8b-instruct", - name: "Llama 3.1 8B Instruct", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 16000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "meta/llama-3.2-11b-vision-instruct": { - id: "meta/llama-3.2-11b-vision-instruct", - name: "Llama 3.2 11b Vision Instruct", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "meta/llama-3.2-90b-vision-instruct": { - id: "meta/llama-3.2-90b-vision-instruct", - name: "Llama-3.2-90B-Vision-Instruct", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "meta/llama-3.3-70b-instruct": { - id: "meta/llama-3.3-70b-instruct", - name: "Llama 3.3 70b Instruct", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-large-3-675b-instruct-2512": { - id: "mistralai/mistral-large-3-675b-instruct-2512", - name: "Mistral Large 3 675B Instruct 2512", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "mistralai/mistral-small-4-119b-2603": { - id: "mistralai/mistral-small-4-119b-2603", - name: "mistral-small-4-119b-2603", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2.6": { - id: "moonshotai/kimi-k2.6", - name: "Kimi K2.6", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-nano-30b-a3b": { - id: "nvidia/nemotron-3-nano-30b-a3b", - name: "nemotron-3-nano-30b-a3b", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning": { - id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning", - name: "Nemotron 3 Nano Omni", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-super-120b-a12b": { - id: "nvidia/nemotron-3-super-120b-a12b", - name: "Nemotron 3 Super", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.2, - output: 0.8, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-ultra-550b-a55b": { - id: "nvidia/nemotron-3-ultra-550b-a55b", - name: "Nemotron 3 Ultra 550B A55B", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.5, - output: 2.5, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "nvidia/nvidia-nemotron-nano-9b-v2": { - id: "nvidia/nvidia-nemotron-nano-9b-v2", - name: "nvidia-nemotron-nano-9b-v2", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT-OSS-120B", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-coder-480b-a35b-instruct": { - id: "qwen/qwen3-coder-480b-a35b-instruct", - name: "Qwen3 Coder 480B A35B Instruct", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 66536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-122b-a10b": { - id: "qwen/qwen3.5-122b-a10b", - name: "Qwen3.5 122B-A10B", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "stepfun-ai/step-3.5-flash": { - id: "stepfun-ai/step-3.5-flash", - name: "Step 3.5 Flash", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "stepfun-ai/step-3.7-flash": { - id: "stepfun-ai/step-3.7-flash", - name: "Step 3.7 Flash", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "z-ai/glm-5.1": { - id: "z-ai/glm-5.1", - name: "GLM-5.1", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, - "openai": { - "gpt-4": { - id: "gpt-4", - name: "GPT-4", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text"], - cost: { - input: 30, - output: 60, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 8192, - } satisfies Model<"openai-responses">, - "gpt-4-turbo": { - id: "gpt-4-turbo", - name: "GPT-4 Turbo", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 10, - output: 30, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-responses">, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT-4.1", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"openai-responses">, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "GPT-4.1 mini", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.4, - output: 1.6, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"openai-responses">, - "gpt-4.1-nano": { - id: "gpt-4.1-nano", - name: "GPT-4.1 nano", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"openai-responses">, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-4o-2024-05-13": { - id: "gpt-4o-2024-05-13", - name: "GPT-4o (2024-05-13)", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 5, - output: 15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-responses">, - "gpt-4o-2024-08-06": { - id: "gpt-4o-2024-08-06", - name: "GPT-4o (2024-08-06)", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-4o-2024-11-20": { - id: "gpt-4o-2024-11-20", - name: "GPT-4o (2024-11-20)", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-4o-mini": { - id: "gpt-4o-mini", - name: "GPT-4o mini", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5-chat-latest": { - id: "gpt-5-chat-latest", - name: "GPT-5 Chat Latest", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "GPT-5-Codex", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5 Mini", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "GPT-5 Nano", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.05, - output: 0.4, - cacheRead: 0.005, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5-pro": { - id: "gpt-5-pro", - name: "GPT-5 Pro", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 15, - output: 120, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":"none"}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-chat-latest": { - id: "gpt-5.1-chat-latest", - name: "GPT-5.1 Chat", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex mini", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":"none","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2-chat-latest": { - id: "gpt-5.2-chat-latest", - name: "GPT-5.2 Chat", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2-pro": { - id: "gpt-5.2-pro", - name: "GPT-5.2 Pro", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 21, - output: 168, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.3-chat-latest": { - id: "gpt-5.3-chat-latest", - name: "GPT-5.3 Chat (latest)", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":"none","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.3-codex-spark": { - id: "gpt-5.3-codex-spark", - name: "GPT-5.3 Codex Spark", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 32000, - } satisfies Model<"openai-responses">, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":"none","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 mini", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":"none","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.75, - output: 4.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "GPT-5.4 nano", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":"none","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.2, - output: 1.25, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4-pro": { - id: "gpt-5.4-pro", - name: "GPT-5.4 Pro", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.5": { - id: "gpt-5.5", - name: "GPT-5.5", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":"none","xhigh":"xhigh","minimal":null}, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.5-pro": { - id: "gpt-5.5-pro", - name: "GPT-5.5 Pro", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh","minimal":null,"low":null}, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "o1": { - id: "o1", - name: "o1", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 60, - cacheRead: 7.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o1-pro": { - id: "o1-pro", - name: "o1-pro", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 150, - output: 600, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o3": { - id: "o3", - name: "o3", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o3-deep-research": { - id: "o3-deep-research", - name: "o3-deep-research", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 10, - output: 40, - cacheRead: 2.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o3-mini": { - id: "o3-mini", - name: "o3-mini", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.55, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o3-pro": { - id: "o3-pro", - name: "o3-pro", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 20, - output: 80, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o4-mini": { - id: "o4-mini", - name: "o4-mini", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.275, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o4-mini-deep-research": { - id: "o4-mini-deep-research", - name: "o4-mini-deep-research", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - }, - "openai-codex": { - "gpt-5.3-codex-spark": { - id: "gpt-5.3-codex-spark", - name: "GPT-5.3 Codex Spark", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, - input: ["text"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"openai-codex-responses">, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-codex-responses">, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 mini", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, - input: ["text", "image"], - cost: { - input: 0.75, - output: 4.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-codex-responses">, - "gpt-5.5": { - id: "gpt-5.5", - name: "GPT-5.5", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-codex-responses">, - }, - "opencode": { - "big-pickle": { - id: "big-pickle", - name: "Big Pickle", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"openai-completions">, - "claude-fable-5": { - id: "claude-fable-5", - name: "Claude Fable 5", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "Claude Haiku 4.5", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-1": { - id: "claude-opus-4-1", - name: "Claude Opus 4.1", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-5": { - id: "claude-opus-4-5", - name: "Claude Opus 4.5", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-7": { - id: "claude-opus-4-7", - name: "Claude Opus 4.7", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-8": { - id: "claude-opus-4-8", - name: "Claude Opus 4.8", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4": { - id: "claude-sonnet-4", - name: "Claude Sonnet 4", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-5": { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "deepseek-v4-flash": { - id: "deepseek-v4-flash", - name: "DeepSeek V4 Flash", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, - input: ["text"], - cost: { - input: 0.14, - output: 0.28, - cacheRead: 0.028, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 384000, - } satisfies Model<"openai-completions">, - "deepseek-v4-flash-free": { - id: "deepseek-v4-flash-free", - name: "DeepSeek V4 Flash Free", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "deepseek-v4-pro": { - id: "deepseek-v4-pro", - name: "DeepSeek V4 Pro", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, - input: ["text"], - cost: { - input: 1.74, - output: 3.84, - cacheRead: 0.145, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 384000, - } satisfies Model<"openai-completions">, - "gemini-3-flash": { - id: "gemini-3-flash", - name: "Gemini 3 Flash", - api: "google-generative-ai", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-3.1-pro": { - id: "gemini-3.1-pro", - name: "Gemini 3.1 Pro Preview", - api: "google-generative-ai", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-3.5-flash": { - id: "gemini-3.5-flash", - name: "Gemini 3.5 Flash", - api: "google-generative-ai", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.5, - output: 9, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "glm-5": { - id: "glm-5", - name: "GLM-5", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3.2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text"], - cost: { - input: 1.4, - output: 4.4, - cacheRead: 0.26, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.07, - output: 8.5, - cacheRead: 0.107, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "GPT-5 Codex", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.07, - output: 8.5, - cacheRead: 0.107, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "GPT-5 Nano", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.05, - output: 0.4, - cacheRead: 0.005, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.07, - output: 8.5, - cacheRead: 0.107, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.07, - output: 8.5, - cacheRead: 0.107, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex Mini", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 Mini", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.75, - output: 4.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "GPT-5.4 Nano", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.2, - output: 1.25, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4-pro": { - id: "gpt-5.4-pro", - name: "GPT-5.4 Pro", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 30, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.5": { - id: "gpt-5.5", - name: "GPT-5.5", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.5-pro": { - id: "gpt-5.5-pro", - name: "GPT-5.5 Pro", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh","minimal":null,"low":null}, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 30, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "grok-build-0.1": { - id: "grok-build-0.1", - name: "Grok Build 0.1", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"supportsReasoningEffort":false,"maxTokensField":"max_tokens"}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null}, - input: ["text", "image"], - cost: { - input: 1, - output: 2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"openai-completions">, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "kimi-k2.6": { - id: "kimi-k2.6", - name: "Kimi K2.6", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.16, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "mimo-v2.5-free": { - id: "mimo-v2.5-free", - name: "MiMo V2.5 Free", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"openai-completions">, - "minimax-m2.5": { - id: "minimax-m2.5", - name: "MiniMax M2.5", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "minimax-m2.7": { - id: "minimax-m2.7", - name: "MiniMax M2.7", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "nemotron-3-ultra-free": { - id: "nemotron-3-ultra-free", - name: "Nemotron 3 Ultra Free", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "north-mini-code-free": { - id: "north-mini-code-free", - name: "North Mini Code Free", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "qwen3.5-plus": { - id: "qwen3.5-plus", - name: "Qwen3.5 Plus", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.2, - output: 1.2, - cacheRead: 0.02, - cacheWrite: 0.25, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "qwen3.6-plus": { - id: "qwen3.6-plus", - name: "Qwen3.6 Plus", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.05, - cacheWrite: 0.625, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - }, - "opencode-go": { - "deepseek-v4-flash": { - id: "deepseek-v4-flash", - name: "DeepSeek V4 Flash", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, - input: ["text"], - cost: { - input: 0.14, - output: 0.28, - cacheRead: 0.0028, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 384000, - } satisfies Model<"openai-completions">, - "deepseek-v4-pro": { - id: "deepseek-v4-pro", - name: "DeepSeek V4 Pro", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, - input: ["text"], - cost: { - input: 1.74, - output: 3.48, - cacheRead: 0.0145, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 384000, - } satisfies Model<"openai-completions">, - "glm-5": { - id: "glm-5", - name: "GLM-5", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3.2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text"], - cost: { - input: 1.4, - output: 4.4, - cacheRead: 0.26, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "kimi-k2.6": { - id: "kimi-k2.6", - name: "Kimi K2.6", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.16, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "mimo-v2.5": { - id: "mimo-v2.5", - name: "MiMo V2.5", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.14, - output: 0.28, - cacheRead: 0.0028, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "mimo-v2.5-pro": { - id: "mimo-v2.5-pro", - name: "MiMo V2.5 Pro", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text"], - cost: { - input: 1.74, - output: 3.48, - cacheRead: 0.0145, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "minimax-m2.5": { - id: "minimax-m2.5", - name: "MiniMax M2.5", - api: "anthropic-messages", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "minimax-m2.7": { - id: "minimax-m2.7", - name: "MiniMax M2.7", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "minimax-m3": { - id: "minimax-m3", - name: "MiniMax M3", - api: "anthropic-messages", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 512000, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "qwen3.6-plus": { - id: "qwen3.6-plus", - name: "Qwen3.6 Plus", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"thinkingFormat":"qwen","maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.05, - cacheWrite: 0.625, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen3.7-max": { - id: "qwen3.7-max", - name: "Qwen3.7 Max", - api: "anthropic-messages", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go", - reasoning: true, - input: ["text"], - cost: { - input: 2.5, - output: 7.5, - cacheRead: 0.5, - cacheWrite: 3.125, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "qwen3.7-plus": { - id: "qwen3.7-plus", - name: "Qwen3.7 Plus", - api: "anthropic-messages", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 1.6, - cacheRead: 0.04, - cacheWrite: 0.5, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - }, - "openrouter": { - "ai21/jamba-large-1.7": { - id: "ai21/jamba-large-1.7", - name: "AI21: Jamba Large 1.7", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 8, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "amazon/nova-2-lite-v1": { - id: "amazon/nova-2-lite-v1", - name: "Amazon: Nova 2 Lite", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 2.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65535, - } satisfies Model<"openai-completions">, - "amazon/nova-lite-v1": { - id: "amazon/nova-lite-v1", - name: "Amazon: Nova Lite 1.0", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.06, - output: 0.24, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 300000, - maxTokens: 5120, - } satisfies Model<"openai-completions">, - "amazon/nova-micro-v1": { - id: "amazon/nova-micro-v1", - name: "Amazon: Nova Micro 1.0", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.035, - output: 0.14, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 5120, - } satisfies Model<"openai-completions">, - "amazon/nova-premier-v1": { - id: "amazon/nova-premier-v1", - name: "Amazon: Nova Premier 1.0", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 12.5, - cacheRead: 0.625, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 32000, - } satisfies Model<"openai-completions">, - "amazon/nova-pro-v1": { - id: "amazon/nova-pro-v1", - name: "Amazon: Nova Pro 1.0", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.7999999999999999, - output: 3.1999999999999997, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 300000, - maxTokens: 5120, - } satisfies Model<"openai-completions">, - "anthropic/claude-3-haiku": { - id: "anthropic/claude-3-haiku", - name: "Anthropic: Claude 3 Haiku", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.25, - cacheRead: 0.03, - cacheWrite: 0.3, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "anthropic/claude-3.5-haiku": { - id: "anthropic/claude-3.5-haiku", - name: "Anthropic: Claude 3.5 Haiku", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.7999999999999999, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "anthropic/claude-fable-5": { - id: "anthropic/claude-fable-5", - name: "Anthropic: Claude Fable 5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "anthropic/claude-haiku-4.5": { - id: "anthropic/claude-haiku-4.5", - name: "Anthropic: Claude Haiku 4.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.09999999999999999, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4": { - id: "anthropic/claude-opus-4", - name: "Anthropic: Claude Opus 4", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.1": { - id: "anthropic/claude-opus-4.1", - name: "Anthropic: Claude Opus 4.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.5": { - id: "anthropic/claude-opus-4.5", - name: "Anthropic: Claude Opus 4.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.6": { - id: "anthropic/claude-opus-4.6", - name: "Anthropic: Claude Opus 4.6", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.6-fast": { - id: "anthropic/claude-opus-4.6-fast", - name: "Anthropic: Claude Opus 4.6 (Fast)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 30, - output: 150, - cacheRead: 3, - cacheWrite: 37.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.7": { - id: "anthropic/claude-opus-4.7", - name: "Anthropic: Claude Opus 4.7", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.7-fast": { - id: "anthropic/claude-opus-4.7-fast", - name: "Anthropic: Claude Opus 4.7 (Fast)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 30, - output: 150, - cacheRead: 3, - cacheWrite: 37.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.8": { - id: "anthropic/claude-opus-4.8", - name: "Anthropic: Claude Opus 4.8", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.8-fast": { - id: "anthropic/claude-opus-4.8-fast", - name: "Anthropic: Claude Opus 4.8 (Fast)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "anthropic/claude-sonnet-4": { - id: "anthropic/claude-sonnet-4", - name: "Anthropic: Claude Sonnet 4", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "anthropic/claude-sonnet-4.5": { - id: "anthropic/claude-sonnet-4.5", - name: "Anthropic: Claude Sonnet 4.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "anthropic/claude-sonnet-4.6": { - id: "anthropic/claude-sonnet-4.6", - name: "Anthropic: Claude Sonnet 4.6", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "arcee-ai/trinity-large-thinking": { - id: "arcee-ai/trinity-large-thinking", - name: "Arcee AI: Trinity Large Thinking", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.22, - output: 0.85, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "arcee-ai/trinity-mini": { - id: "arcee-ai/trinity-mini", - name: "Arcee AI: Trinity Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.045, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "arcee-ai/virtuoso-large": { - id: "arcee-ai/virtuoso-large", - name: "Arcee AI: Virtuoso Large", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.75, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "auto": { - id: "auto", - name: "Auto", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, - "bytedance-seed/seed-1.6": { - id: "bytedance-seed/seed-1.6", - name: "ByteDance Seed: Seed 1.6", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "bytedance-seed/seed-1.6-flash": { - id: "bytedance-seed/seed-1.6-flash", - name: "ByteDance Seed: Seed 1.6 Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "bytedance-seed/seed-2.0-lite": { - id: "bytedance-seed/seed-2.0-lite", - name: "ByteDance Seed: Seed-2.0-Lite", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "bytedance-seed/seed-2.0-mini": { - id: "bytedance-seed/seed-2.0-mini", - name: "ByteDance Seed: Seed-2.0-Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "cohere/command-r-08-2024": { - id: "cohere/command-r-08-2024", - name: "Cohere: Command R (08-2024)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4000, - } satisfies Model<"openai-completions">, - "cohere/command-r-plus-08-2024": { - id: "cohere/command-r-plus-08-2024", - name: "Cohere: Command R+ (08-2024)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2.5, - output: 10, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4000, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-chat": { - id: "deepseek/deepseek-chat", - name: "DeepSeek: DeepSeek V3", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.20020000000000002, - output: 0.8000999999999999, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16000, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-chat-v3-0324": { - id: "deepseek/deepseek-chat-v3-0324", - name: "DeepSeek: DeepSeek V3 0324", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.19999999999999998, - output: 0.77, - cacheRead: 0.135, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-chat-v3.1": { - id: "deepseek/deepseek-chat-v3.1", - name: "DeepSeek: DeepSeek V3.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.21, - output: 0.7899999999999999, - cacheRead: 0.13, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-r1": { - id: "deepseek/deepseek-r1", - name: "DeepSeek: R1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.7, - output: 2.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 16000, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-r1-0528": { - id: "deepseek/deepseek-r1-0528", - name: "DeepSeek: R1 0528", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.5, - output: 2.1500000000000004, - cacheRead: 0.35, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-v3.1-terminus": { - id: "deepseek/deepseek-v3.1-terminus", - name: "DeepSeek: DeepSeek V3.1 Terminus", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.27, - output: 0.95, - cacheRead: 0.13, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-v3.2": { - id: "deepseek/deepseek-v3.2", - name: "DeepSeek: DeepSeek V3.2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.2288, - output: 0.3432, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-v3.2-exp": { - id: "deepseek/deepseek-v3.2-exp", - name: "DeepSeek: DeepSeek V3.2 Exp", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.27, - output: 0.41, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-v4-flash": { - id: "deepseek/deepseek-v4-flash", - name: "DeepSeek: DeepSeek V4 Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh"}, - input: ["text"], - cost: { - input: 0.0983, - output: 0.1966, - cacheRead: 0.019700000000000002, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-v4-pro": { - id: "deepseek/deepseek-v4-pro", - name: "DeepSeek: DeepSeek V4 Pro", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh"}, - input: ["text"], - cost: { - input: 0.435, - output: 0.87, - cacheRead: 0.003625, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 384000, - } satisfies Model<"openai-completions">, - "essentialai/rnj-1-instruct": { - id: "essentialai/rnj-1-instruct", - name: "EssentialAI: Rnj 1 Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "google/gemini-2.5-flash": { - id: "google/gemini-2.5-flash", - name: "Google: Gemini 2.5 Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 2.5, - cacheRead: 0.03, - cacheWrite: 0.08333333333333334, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"openai-completions">, - "google/gemini-2.5-flash-lite": { - id: "google/gemini-2.5-flash-lite", - name: "Google: Gemini 2.5 Flash Lite", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, - cacheRead: 0.01, - cacheWrite: 0.08333333333333334, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"openai-completions">, - "google/gemini-2.5-flash-lite-preview-09-2025": { - id: "google/gemini-2.5-flash-lite-preview-09-2025", - name: "Google: Gemini 2.5 Flash Lite Preview 09-2025", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, - cacheRead: 0.01, - cacheWrite: 0.08333333333333334, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"openai-completions">, - "google/gemini-2.5-pro": { - id: "google/gemini-2.5-pro", - name: "Google: Gemini 2.5 Pro", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0.375, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "google/gemini-2.5-pro-preview": { - id: "google/gemini-2.5-pro-preview", - name: "Google: Gemini 2.5 Pro Preview 06-05", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0.375, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "google/gemini-2.5-pro-preview-05-06": { - id: "google/gemini-2.5-pro-preview-05-06", - name: "Google: Gemini 2.5 Pro Preview 05-06", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0.375, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"openai-completions">, - "google/gemini-3-flash-preview": { - id: "google/gemini-3-flash-preview", - name: "Google: Gemini 3 Flash Preview", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.049999999999999996, - cacheWrite: 0.08333333333333334, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "google/gemini-3.1-flash-lite": { - id: "google/gemini-3.1-flash-lite", - name: "Google: Gemini 3.1 Flash Lite", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.5, - cacheRead: 0.024999999999999998, - cacheWrite: 0.08333333333333334, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "google/gemini-3.1-flash-lite-preview": { - id: "google/gemini-3.1-flash-lite-preview", - name: "Google: Gemini 3.1 Flash Lite Preview", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.5, - cacheRead: 0.024999999999999998, - cacheWrite: 0.08333333333333334, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "google/gemini-3.1-pro-preview": { - id: "google/gemini-3.1-pro-preview", - name: "Google: Gemini 3.1 Pro Preview", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.19999999999999998, - cacheWrite: 0.375, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "google/gemini-3.1-pro-preview-customtools": { - id: "google/gemini-3.1-pro-preview-customtools", - name: "Google: Gemini 3.1 Pro Preview Custom Tools", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.19999999999999998, - cacheWrite: 0.375, - }, - contextWindow: 1048756, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "google/gemini-3.5-flash": { - id: "google/gemini-3.5-flash", - name: "Google: Gemini 3.5 Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.5, - output: 9, - cacheRead: 0.15, - cacheWrite: 0.08333333333333334, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "google/gemma-3-12b-it": { - id: "google/gemma-3-12b-it", - name: "Google: Gemma 3 12B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.049999999999999996, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "google/gemma-3-27b-it": { - id: "google/gemma-3-27b-it", - name: "Google: Gemma 3 27B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.08, - output: 0.16, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "google/gemma-4-26b-a4b-it": { - id: "google/gemma-4-26b-a4b-it", - name: "Google: Gemma 4 26B A4B ", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.06, - output: 0.33, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "google/gemma-4-26b-a4b-it:free": { - id: "google/gemma-4-26b-a4b-it:free", - name: "Google: Gemma 4 26B A4B (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "google/gemma-4-31b-it": { - id: "google/gemma-4-31b-it", - name: "Google: Gemma 4 31B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.12, - output: 0.36, - cacheRead: 0.09, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "google/gemma-4-31b-it:free": { - id: "google/gemma-4-31b-it:free", - name: "Google: Gemma 4 31B (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "ibm-granite/granite-4.1-8b": { - id: "ibm-granite/granite-4.1-8b", - name: "IBM: Granite 4.1 8B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.049999999999999996, - output: 0.09999999999999999, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "inception/mercury-2": { - id: "inception/mercury-2", - name: "Inception: Mercury 2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text"], - cost: { - input: 0.25, - output: 0.75, - cacheRead: 0.024999999999999998, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 50000, - } satisfies Model<"openai-completions">, - "inclusionai/ling-2.6-1t": { - id: "inclusionai/ling-2.6-1t", - name: "inclusionAI: Ling-2.6-1T", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.075, - output: 0.625, - cacheRead: 0.015, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "inclusionai/ling-2.6-flash": { - id: "inclusionai/ling-2.6-flash", - name: "inclusionAI: Ling-2.6-flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.01, - output: 0.03, - cacheRead: 0.002, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "inclusionai/ring-2.6-1t": { - id: "inclusionai/ring-2.6-1t", - name: "inclusionAI: Ring-2.6-1T", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.075, - output: 0.625, - cacheRead: 0.015, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "kwaipilot/kat-coder-pro-v2": { - id: "kwaipilot/kat-coder-pro-v2", - name: "Kwaipilot: KAT-Coder-Pro V2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 80000, - } satisfies Model<"openai-completions">, - "meta-llama/llama-3.1-70b-instruct": { - id: "meta-llama/llama-3.1-70b-instruct", - name: "Meta: Llama 3.1 70B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.39999999999999997, - output: 0.39999999999999997, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "meta-llama/llama-3.1-8b-instruct": { - id: "meta-llama/llama-3.1-8b-instruct", - name: "Meta: Llama 3.1 8B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.02, - output: 0.03, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "meta-llama/llama-3.3-70b-instruct": { - id: "meta-llama/llama-3.3-70b-instruct", - name: "Meta: Llama 3.3 70B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.09999999999999999, - output: 0.32, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "meta-llama/llama-3.3-70b-instruct:free": { - id: "meta-llama/llama-3.3-70b-instruct:free", - name: "Meta: Llama 3.3 70B Instruct (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "meta-llama/llama-4-maverick": { - id: "meta-llama/llama-4-maverick", - name: "Meta: Llama 4 Maverick", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "meta-llama/llama-4-scout": { - id: "meta-llama/llama-4-scout", - name: "Meta: Llama 4 Scout", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 10000000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "minimax/minimax-m1": { - id: "minimax/minimax-m1", - name: "MiniMax: MiniMax M1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.39999999999999997, - output: 2.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 40000, - } satisfies Model<"openai-completions">, - "minimax/minimax-m2": { - id: "minimax/minimax-m2", - name: "MiniMax: MiniMax M2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.255, - output: 1, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 196608, - } satisfies Model<"openai-completions">, - "minimax/minimax-m2.1": { - id: "minimax/minimax-m2.1", - name: "MiniMax: MiniMax M2.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.29, - output: 0.95, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 196608, - } satisfies Model<"openai-completions">, - "minimax/minimax-m2.5": { - id: "minimax/minimax-m2.5", - name: "MiniMax: MiniMax M2.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.8999999999999999, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 196608, - } satisfies Model<"openai-completions">, - "minimax/minimax-m2.7": { - id: "minimax/minimax-m2.7", - name: "MiniMax: MiniMax M2.7", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.27, - output: 1.08, - cacheRead: 0.054, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "minimax/minimax-m3": { - id: "minimax/minimax-m3", - name: "MiniMax: MiniMax M3", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 512000, - } satisfies Model<"openai-completions">, - "mistralai/codestral-2508": { - id: "mistralai/codestral-2508", - name: "Mistral: Codestral 2508", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.3, - output: 0.8999999999999999, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/devstral-2512": { - id: "mistralai/devstral-2512", - name: "Mistral: Devstral 2 2512", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.39999999999999997, - output: 2, - cacheRead: 0.04, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/ministral-14b-2512": { - id: "mistralai/ministral-14b-2512", - name: "Mistral: Ministral 3 14B 2512", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 0.19999999999999998, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/ministral-3b-2512": { - id: "mistralai/ministral-3b-2512", - name: "Mistral: Ministral 3 3B 2512", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.09999999999999999, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/ministral-8b-2512": { - id: "mistralai/ministral-8b-2512", - name: "Mistral: Ministral 3 8B 2512", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0.015, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-large": { - id: "mistralai/mistral-large", - name: "Mistral Large", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 6, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-large-2407": { - id: "mistralai/mistral-large-2407", - name: "Mistral Large 2407", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 6, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-large-2512": { - id: "mistralai/mistral-large-2512", - name: "Mistral: Mistral Large 3 2512", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-medium-3": { - id: "mistralai/mistral-medium-3", - name: "Mistral: Mistral Medium 3", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.39999999999999997, - output: 2, - cacheRead: 0.04, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-medium-3-5": { - id: "mistralai/mistral-medium-3-5", - name: "Mistral: Mistral Medium 3.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.5, - output: 7.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-medium-3.1": { - id: "mistralai/mistral-medium-3.1", - name: "Mistral: Mistral Medium 3.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.39999999999999997, - output: 2, - cacheRead: 0.04, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-nemo": { - id: "mistralai/mistral-nemo", - name: "Mistral: Mistral Nemo", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.02, - output: 0.03, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-saba": { - id: "mistralai/mistral-saba", - name: "Mistral: Saba", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.19999999999999998, - output: 0.6, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-small-2603": { - id: "mistralai/mistral-small-2603", - name: "Mistral: Mistral Small 4", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.015, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-small-3.2-24b-instruct": { - id: "mistralai/mistral-small-3.2-24b-instruct", - name: "Mistral: Mistral Small 3.2 24B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.075, - output: 0.19999999999999998, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "mistralai/mixtral-8x22b-instruct": { - id: "mistralai/mixtral-8x22b-instruct", - name: "Mistral: Mixtral 8x22B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 6, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 65536, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/voxtral-small-24b-2507": { - id: "mistralai/voxtral-small-24b-2507", - name: "Mistral: Voxtral Small 24B 2507", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.09999999999999999, - output: 0.3, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 32000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2": { - id: "moonshotai/kimi-k2", - name: "MoonshotAI: Kimi K2 0711", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.5700000000000001, - output: 2.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2-0905": { - id: "moonshotai/kimi-k2-0905", - name: "MoonshotAI: Kimi K2 0905", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2-thinking": { - id: "moonshotai/kimi-k2-thinking", - name: "MoonshotAI: Kimi K2 Thinking", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "MoonshotAI: Kimi K2.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.41, - output: 2.06, - cacheRead: 0.07, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2.6": { - id: "moonshotai/kimi-k2.6", - name: "MoonshotAI: Kimi K2.6", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - compat: {"supportsDeveloperRole":false,"requiresReasoningContentOnAssistantMessages":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6799999999999999, - output: 3.41, - cacheRead: 0.33999999999999997, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262142, - } satisfies Model<"openai-completions">, - "nex-agi/nex-n2-pro:free": { - id: "nex-agi/nex-n2-pro:free", - name: "Nex AGI: Nex-N2-Pro (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "nvidia/llama-3.3-nemotron-super-49b-v1.5": { - id: "nvidia/llama-3.3-nemotron-super-49b-v1.5", - name: "NVIDIA: Llama 3.3 Nemotron Super 49B V1.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.39999999999999997, - output: 0.39999999999999997, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-nano-30b-a3b": { - id: "nvidia/nemotron-3-nano-30b-a3b", - name: "NVIDIA: Nemotron 3 Nano 30B A3B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.049999999999999996, - output: 0.19999999999999998, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 228000, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-nano-30b-a3b:free": { - id: "nvidia/nemotron-3-nano-30b-a3b:free", - name: "NVIDIA: Nemotron 3 Nano 30B A3B (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free": { - id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free", - name: "NVIDIA: Nemotron 3 Nano Omni (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-super-120b-a12b": { - id: "nvidia/nemotron-3-super-120b-a12b", - name: "NVIDIA: Nemotron 3 Super", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.09, - output: 0.44999999999999996, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-super-120b-a12b:free": { - id: "nvidia/nemotron-3-super-120b-a12b:free", - name: "NVIDIA: Nemotron 3 Super (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-ultra-550b-a55b": { - id: "nvidia/nemotron-3-ultra-550b-a55b", - name: "NVIDIA: Nemotron 3 Ultra", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.5, - output: 2.5, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-ultra-550b-a55b:free": { - id: "nvidia/nemotron-3-ultra-550b-a55b:free", - name: "NVIDIA: Nemotron 3 Ultra (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-nano-12b-v2-vl:free": { - id: "nvidia/nemotron-nano-12b-v2-vl:free", - name: "NVIDIA: Nemotron Nano 12B 2 VL (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-nano-9b-v2": { - id: "nvidia/nemotron-nano-9b-v2", - name: "NVIDIA: Nemotron Nano 9B V2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.04, - output: 0.16, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-nano-9b-v2:free": { - id: "nvidia/nemotron-nano-9b-v2:free", - name: "NVIDIA: Nemotron Nano 9B V2 (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-3.5-turbo": { - id: "openai/gpt-3.5-turbo", - name: "OpenAI: GPT-3.5 Turbo", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 16385, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-3.5-turbo-0613": { - id: "openai/gpt-3.5-turbo-0613", - name: "OpenAI: GPT-3.5 Turbo (older v0613)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 4095, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-3.5-turbo-16k": { - id: "openai/gpt-3.5-turbo-16k", - name: "OpenAI: GPT-3.5 Turbo 16k", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 3, - output: 4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 16385, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-4": { - id: "openai/gpt-4", - name: "OpenAI: GPT-4", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 30, - output: 60, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8191, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-4-turbo": { - id: "openai/gpt-4-turbo", - name: "OpenAI: GPT-4 Turbo", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 10, - output: 30, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-4-turbo-preview": { - id: "openai/gpt-4-turbo-preview", - name: "OpenAI: GPT-4 Turbo Preview", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 10, - output: 30, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-4.1": { - id: "openai/gpt-4.1", - name: "OpenAI: GPT-4.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-4.1-mini": { - id: "openai/gpt-4.1-mini", - name: "OpenAI: GPT-4.1 Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.39999999999999997, - output: 1.5999999999999999, - cacheRead: 0.09999999999999999, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "openai/gpt-4.1-nano": { - id: "openai/gpt-4.1-nano", - name: "OpenAI: GPT-4.1 Nano", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, - cacheRead: 0.024999999999999998, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "openai/gpt-4o": { - id: "openai/gpt-4o", - name: "OpenAI: GPT-4o", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-4o-2024-05-13": { - id: "openai/gpt-4o-2024-05-13", - name: "OpenAI: GPT-4o (2024-05-13)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 5, - output: 15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-4o-2024-08-06": { - id: "openai/gpt-4o-2024-08-06", - name: "OpenAI: GPT-4o (2024-08-06)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-4o-2024-11-20": { - id: "openai/gpt-4o-2024-11-20", - name: "OpenAI: GPT-4o (2024-11-20)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-4o-mini": { - id: "openai/gpt-4o-mini", - name: "OpenAI: GPT-4o-mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-4o-mini-2024-07-18": { - id: "openai/gpt-4o-mini-2024-07-18", - name: "OpenAI: GPT-4o-mini (2024-07-18)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "OpenAI: GPT-5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5-codex": { - id: "openai/gpt-5-codex", - name: "OpenAI: GPT-5 Codex", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5-mini": { - id: "openai/gpt-5-mini", - name: "OpenAI: GPT-5 Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.024999999999999998, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5-nano": { - id: "openai/gpt-5-nano", - name: "OpenAI: GPT-5 Nano", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.049999999999999996, - output: 0.39999999999999997, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-5-pro": { - id: "openai/gpt-5-pro", - name: "OpenAI: GPT-5 Pro", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 120, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.1": { - id: "openai/gpt-5.1", - name: "OpenAI: GPT-5.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.13, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.1-chat": { - id: "openai/gpt-5.1-chat", - name: "OpenAI: GPT-5.1 Chat", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.13, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 32000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.1-codex": { - id: "openai/gpt-5.1-codex", - name: "OpenAI: GPT-5.1-Codex", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.13, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.1-codex-max": { - id: "openai/gpt-5.1-codex-max", - name: "OpenAI: GPT-5.1-Codex-Max", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.1-codex-mini": { - id: "openai/gpt-5.1-codex-mini", - name: "OpenAI: GPT-5.1-Codex-Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.024999999999999998, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "OpenAI: GPT-5.2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.2-chat": { - id: "openai/gpt-5.2-chat", - name: "OpenAI: GPT-5.2 Chat", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-5.2-codex": { - id: "openai/gpt-5.2-codex", - name: "OpenAI: GPT-5.2-Codex", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.2-pro": { - id: "openai/gpt-5.2-pro", - name: "OpenAI: GPT-5.2 Pro", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 21, - output: 168, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.3-chat": { - id: "openai/gpt-5.3-chat", - name: "OpenAI: GPT-5.3 Chat", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-5.3-codex": { - id: "openai/gpt-5.3-codex", - name: "OpenAI: GPT-5.3-Codex", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.4": { - id: "openai/gpt-5.4", - name: "OpenAI: GPT-5.4", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.4-mini": { - id: "openai/gpt-5.4-mini", - name: "OpenAI: GPT-5.4 Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.75, - output: 4.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.4-nano": { - id: "openai/gpt-5.4-nano", - name: "OpenAI: GPT-5.4 Nano", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 1.25, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.4-pro": { - id: "openai/gpt-5.4-pro", - name: "OpenAI: GPT-5.4 Pro", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.5": { - id: "openai/gpt-5.5", - name: "OpenAI: GPT-5.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.5-pro": { - id: "openai/gpt-5.5-pro", - name: "OpenAI: GPT-5.5 Pro", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh","off":null,"minimal":null,"low":null}, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-audio": { - id: "openai/gpt-audio", - name: "OpenAI: GPT Audio", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2.5, - output: 10, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-audio-mini": { - id: "openai/gpt-audio-mini", - name: "OpenAI: GPT Audio Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.6, - output: 2.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-chat-latest": { - id: "openai/gpt-chat-latest", - name: "OpenAI: GPT Chat Latest", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "OpenAI: gpt-oss-120b", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.039, - output: 0.18, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-120b:free": { - id: "openai/gpt-oss-120b:free", - name: "OpenAI: gpt-oss-120b (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "OpenAI: gpt-oss-20b", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.029, - output: 0.14, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-20b:free": { - id: "openai/gpt-oss-20b:free", - name: "OpenAI: gpt-oss-20b (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-safeguard-20b": { - id: "openai/gpt-oss-safeguard-20b", - name: "OpenAI: gpt-oss-safeguard-20b", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0.037, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "openai/o1": { - id: "openai/o1", - name: "OpenAI: o1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 60, - cacheRead: 7.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o3": { - id: "openai/o3", - name: "OpenAI: o3", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o3-deep-research": { - id: "openai/o3-deep-research", - name: "OpenAI: o3 Deep Research", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 10, - output: 40, - cacheRead: 2.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o3-mini": { - id: "openai/o3-mini", - name: "OpenAI: o3 Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.55, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o3-mini-high": { - id: "openai/o3-mini-high", - name: "OpenAI: o3 Mini High", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.55, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o3-pro": { - id: "openai/o3-pro", - name: "OpenAI: o3 Pro", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 20, - output: 80, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o4-mini": { - id: "openai/o4-mini", - name: "OpenAI: o4 Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.275, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o4-mini-deep-research": { - id: "openai/o4-mini-deep-research", - name: "OpenAI: o4 Mini Deep Research", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o4-mini-high": { - id: "openai/o4-mini-high", - name: "OpenAI: o4 Mini High", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.275, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openrouter/auto": { - id: "openrouter/auto", - name: "Auto Router", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: -1000000, - output: -1000000, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openrouter/free": { - id: "openrouter/free", - name: "Free Models Router", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openrouter/owl-alpha": { - id: "openrouter/owl-alpha", - name: "Owl Alpha", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048756, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "poolside/laguna-m.1:free": { - id: "poolside/laguna-m.1:free", - name: "Poolside: Laguna M.1 (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "poolside/laguna-xs.2:free": { - id: "poolside/laguna-xs.2:free", - name: "Poolside: Laguna XS.2 (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "prime-intellect/intellect-3": { - id: "prime-intellect/intellect-3", - name: "Prime Intellect: INTELLECT-3", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.19999999999999998, - output: 1.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "qwen/qwen-2.5-72b-instruct": { - id: "qwen/qwen-2.5-72b-instruct", - name: "Qwen2.5 72B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.36, - output: 0.39999999999999997, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "qwen/qwen-plus": { - id: "qwen/qwen-plus", - name: "Qwen: Qwen-Plus", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.26, - output: 0.78, - cacheRead: 0.052000000000000005, - cacheWrite: 0.325, - }, - contextWindow: 1000000, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen-plus-2025-07-28": { - id: "qwen/qwen-plus-2025-07-28", - name: "Qwen: Qwen Plus 0728", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.26, - output: 0.78, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen-plus-2025-07-28:thinking": { - id: "qwen/qwen-plus-2025-07-28:thinking", - name: "Qwen: Qwen Plus 0728 (thinking)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.26, - output: 0.78, - cacheRead: 0, - cacheWrite: 0.325, - }, - contextWindow: 1000000, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-14b": { - id: "qwen/qwen3-14b", - name: "Qwen: Qwen3 14B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.09999999999999999, - output: 0.24, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131702, - maxTokens: 40960, - } satisfies Model<"openai-completions">, - "qwen/qwen3-235b-a22b": { - id: "qwen/qwen3-235b-a22b", - name: "Qwen: Qwen3 235B A22B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.45499999999999996, - output: 1.8199999999999998, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "qwen/qwen3-235b-a22b-2507": { - id: "qwen/qwen3-235b-a22b-2507", - name: "Qwen: Qwen3 235B A22B Instruct 2507", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.09, - output: 0.09999999999999999, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "qwen/qwen3-235b-a22b-thinking-2507": { - id: "qwen/qwen3-235b-a22b-thinking-2507", - name: "Qwen: Qwen3 235B A22B Thinking 2507", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.09999999999999999, - output: 0.09999999999999999, - cacheRead: 0.09999999999999999, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "qwen/qwen3-30b-a3b": { - id: "qwen/qwen3-30b-a3b", - name: "Qwen: Qwen3 30B A3B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.12, - output: 0.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "qwen/qwen3-30b-a3b-instruct-2507": { - id: "qwen/qwen3-30b-a3b-instruct-2507", - name: "Qwen: Qwen3 30B A3B Instruct 2507", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.04815, - output: 0.19305, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32000, - } satisfies Model<"openai-completions">, - "qwen/qwen3-30b-a3b-thinking-2507": { - id: "qwen/qwen3-30b-a3b-thinking-2507", - name: "Qwen: Qwen3 30B A3B Thinking 2507", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.08, - output: 0.39999999999999997, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "qwen/qwen3-32b": { - id: "qwen/qwen3-32b", - name: "Qwen: Qwen3 32B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.08, - output: 0.28, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "qwen/qwen3-8b": { - id: "qwen/qwen3-8b", - name: "Qwen: Qwen3 8B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.049999999999999996, - output: 0.39999999999999997, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "qwen/qwen3-coder": { - id: "qwen/qwen3-coder", - name: "Qwen: Qwen3 Coder 480B A35B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.22, - output: 1.7999999999999998, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3-coder-30b-a3b-instruct": { - id: "qwen/qwen3-coder-30b-a3b-instruct", - name: "Qwen: Qwen3 Coder 30B A3B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.07, - output: 0.27, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 160000, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-coder-flash": { - id: "qwen/qwen3-coder-flash", - name: "Qwen: Qwen3 Coder Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.195, - output: 0.975, - cacheRead: 0.039, - cacheWrite: 0.24375, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3-coder-next": { - id: "qwen/qwen3-coder-next", - name: "Qwen: Qwen3 Coder Next", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.11, - output: 0.7999999999999999, - cacheRead: 0.07, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "qwen/qwen3-coder-plus": { - id: "qwen/qwen3-coder-plus", - name: "Qwen: Qwen3 Coder Plus", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.65, - output: 3.25, - cacheRead: 0.13, - cacheWrite: 0.8125, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3-coder:free": { - id: "qwen/qwen3-coder:free", - name: "Qwen: Qwen3 Coder 480B A35B (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 262000, - } satisfies Model<"openai-completions">, - "qwen/qwen3-max": { - id: "qwen/qwen3-max", - name: "Qwen: Qwen3 Max", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.78, - output: 3.9, - cacheRead: 0.156, - cacheWrite: 0.975, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-max-thinking": { - id: "qwen/qwen3-max-thinking", - name: "Qwen: Qwen3 Max Thinking", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.78, - output: 3.9, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-next-80b-a3b-instruct": { - id: "qwen/qwen3-next-80b-a3b-instruct", - name: "Qwen: Qwen3 Next 80B A3B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.09, - output: 1.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "qwen/qwen3-next-80b-a3b-instruct:free": { - id: "qwen/qwen3-next-80b-a3b-instruct:free", - name: "Qwen: Qwen3 Next 80B A3B Instruct (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "qwen/qwen3-next-80b-a3b-thinking": { - id: "qwen/qwen3-next-80b-a3b-thinking", - name: "Qwen: Qwen3 Next 80B A3B Thinking", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.0975, - output: 0.78, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-vl-235b-a22b-instruct": { - id: "qwen/qwen3-vl-235b-a22b-instruct", - name: "Qwen: Qwen3 VL 235B A22B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 0.88, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "qwen/qwen3-vl-235b-a22b-thinking": { - id: "qwen/qwen3-vl-235b-a22b-thinking", - name: "Qwen: Qwen3 VL 235B A22B Thinking", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.26, - output: 2.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-vl-30b-a3b-instruct": { - id: "qwen/qwen3-vl-30b-a3b-instruct", - name: "Qwen: Qwen3 VL 30B A3B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.13, - output: 0.52, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-vl-30b-a3b-thinking": { - id: "qwen/qwen3-vl-30b-a3b-thinking", - name: "Qwen: Qwen3 VL 30B A3B Thinking", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.13, - output: 1.56, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-vl-32b-instruct": { - id: "qwen/qwen3-vl-32b-instruct", - name: "Qwen: Qwen3 VL 32B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.10400000000000001, - output: 0.41600000000000004, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-vl-8b-instruct": { - id: "qwen/qwen3-vl-8b-instruct", - name: "Qwen: Qwen3 VL 8B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.08, - output: 0.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-vl-8b-thinking": { - id: "qwen/qwen3-vl-8b-thinking", - name: "Qwen: Qwen3 VL 8B Thinking", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.117, - output: 1.365, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-122b-a10b": { - id: "qwen/qwen3.5-122b-a10b", - name: "Qwen: Qwen3.5-122B-A10B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.26, - output: 2.08, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-27b": { - id: "qwen/qwen3.5-27b", - name: "Qwen: Qwen3.5-27B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.195, - output: 1.56, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-35b-a3b": { - id: "qwen/qwen3.5-35b-a3b", - name: "Qwen: Qwen3.5-35B-A3B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.14, - output: 1, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-397b-a17b": { - id: "qwen/qwen3.5-397b-a17b", - name: "Qwen: Qwen3.5 397B A17B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.39, - output: 2.34, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-9b": { - id: "qwen/qwen3.5-9b", - name: "Qwen: Qwen3.5-9B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-flash-02-23": { - id: "qwen/qwen3.5-flash-02-23", - name: "Qwen: Qwen3.5-Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.065, - output: 0.26, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-plus-02-15": { - id: "qwen/qwen3.5-plus-02-15", - name: "Qwen: Qwen3.5 Plus 2026-02-15", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.26, - output: 1.56, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-plus-20260420": { - id: "qwen/qwen3.5-plus-20260420", - name: "Qwen: Qwen3.5 Plus 2026-04-20", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 1.7999999999999998, - cacheRead: 0, - cacheWrite: 0.375, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.6-27b": { - id: "qwen/qwen3.6-27b", - name: "Qwen: Qwen3.6 27B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.28900000000000003, - output: 2.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "qwen/qwen3.6-35b-a3b": { - id: "qwen/qwen3.6-35b-a3b", - name: "Qwen: Qwen3.6 35B A3B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.14, - output: 1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262140, - } satisfies Model<"openai-completions">, - "qwen/qwen3.6-flash": { - id: "qwen/qwen3.6-flash", - name: "Qwen: Qwen3.6 Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1875, - output: 1.125, - cacheRead: 0, - cacheWrite: 0.234375, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.6-max-preview": { - id: "qwen/qwen3.6-max-preview", - name: "Qwen: Qwen3.6 Max Preview", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1.04, - output: 6.24, - cacheRead: 0, - cacheWrite: 1.3, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.6-plus": { - id: "qwen/qwen3.6-plus", - name: "Qwen: Qwen3.6 Plus", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.325, - output: 1.95, - cacheRead: 0, - cacheWrite: 0.40625, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.7-max": { - id: "qwen/qwen3.7-max", - name: "Qwen: Qwen3.7 Max", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1.25, - output: 3.75, - cacheRead: 0.25, - cacheWrite: 1.5625, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.7-plus": { - id: "qwen/qwen3.7-plus", - name: "Qwen: Qwen3.7 Plus", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.39999999999999997, - output: 1.5999999999999999, - cacheRead: 0.08, - cacheWrite: 0.5, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "rekaai/reka-edge": { - id: "rekaai/reka-edge", - name: "Reka Edge", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.09999999999999999, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 16384, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "relace/relace-search": { - id: "relace/relace-search", - name: "Relace: Relace Search", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "sao10k/l3.1-euryale-70b": { - id: "sao10k/l3.1-euryale-70b", - name: "Sao10K: Llama 3.1 Euryale 70B v2.2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.85, - output: 0.85, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "stepfun/step-3.5-flash": { - id: "stepfun/step-3.5-flash", - name: "StepFun: Step 3.5 Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.09, - output: 0.3, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "stepfun/step-3.7-flash": { - id: "stepfun/step-3.7-flash", - name: "StepFun: Step 3.7 Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 1.15, - cacheRead: 0.04, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"openai-completions">, - "tencent/hy3-preview": { - id: "tencent/hy3-preview", - name: "Tencent: Hy3 preview", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.063, - output: 0.21, - cacheRead: 0.020999999999999998, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "thedrummer/rocinante-12b": { - id: "thedrummer/rocinante-12b", - name: "TheDrummer: Rocinante 12B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.16999999999999998, - output: 0.43, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "thedrummer/unslopnemo-12b": { - id: "thedrummer/unslopnemo-12b", - name: "TheDrummer: UnslopNemo 12B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.39999999999999997, - output: 0.39999999999999997, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "upstage/solar-pro-3": { - id: "upstage/solar-pro-3", - name: "Upstage: Solar Pro 3", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.015, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "x-ai/grok-4.20": { - id: "x-ai/grok-4.20", - name: "xAI: Grok 4.20", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "x-ai/grok-4.3": { - id: "x-ai/grok-4.3", - name: "xAI: Grok 4.3", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "x-ai/grok-build-0.1": { - id: "x-ai/grok-build-0.1", - name: "xAI: Grok Build 0.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 2, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "xiaomi/mimo-v2-flash": { - id: "xiaomi/mimo-v2-flash", - name: "Xiaomi: MiMo-V2-Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.09999999999999999, - output: 0.3, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "xiaomi/mimo-v2.5": { - id: "xiaomi/mimo-v2.5", - name: "Xiaomi: MiMo-V2.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.14, - output: 0.28, - cacheRead: 0.0028, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "xiaomi/mimo-v2.5-pro": { - id: "xiaomi/mimo-v2.5-pro", - name: "Xiaomi: MiMo-V2.5-Pro", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.435, - output: 0.87, - cacheRead: 0.0036, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "z-ai/glm-4.5": { - id: "z-ai/glm-4.5", - name: "Z.ai: GLM 4.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.2, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 98304, - } satisfies Model<"openai-completions">, - "z-ai/glm-4.5-air": { - id: "z-ai/glm-4.5-air", - name: "Z.ai: GLM 4.5 Air", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.125, - output: 0.85, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131070, - } satisfies Model<"openai-completions">, - "z-ai/glm-4.5v": { - id: "z-ai/glm-4.5v", - name: "Z.ai: GLM 4.5V", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 1.7999999999999998, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 65536, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "z-ai/glm-4.6": { - id: "z-ai/glm-4.6", - name: "Z.ai: GLM 4.6", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.43, - output: 1.74, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "z-ai/glm-4.6v": { - id: "z-ai/glm-4.6v", - name: "Z.ai: GLM 4.6V", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 0.8999999999999999, - cacheRead: 0.055, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "z-ai/glm-4.7": { - id: "z-ai/glm-4.7", - name: "Z.ai: GLM 4.7", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.39999999999999997, - output: 1.75, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "z-ai/glm-4.7-flash": { - id: "z-ai/glm-4.7-flash", - name: "Z.ai: GLM 4.7 Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.06, - output: 0.39999999999999997, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "z-ai/glm-5": { - id: "z-ai/glm-5", - name: "Z.ai: GLM 5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 1.9, - cacheRead: 0.119, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "z-ai/glm-5-turbo": { - id: "z-ai/glm-5-turbo", - name: "Z.ai: GLM 5 Turbo", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1.2, - output: 4, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "z-ai/glm-5.1": { - id: "z-ai/glm-5.1", - name: "Z.ai: GLM 5.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.98, - output: 3.08, - cacheRead: 0.182, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "~anthropic/claude-fable-latest": { - id: "~anthropic/claude-fable-latest", - name: "Anthropic: Claude Fable Latest", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "~anthropic/claude-haiku-latest": { - id: "~anthropic/claude-haiku-latest", - name: "Anthropic Claude Haiku Latest", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.09999999999999999, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "~anthropic/claude-opus-latest": { - id: "~anthropic/claude-opus-latest", - name: "Anthropic: Claude Opus Latest", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "~anthropic/claude-sonnet-latest": { - id: "~anthropic/claude-sonnet-latest", - name: "Anthropic Claude Sonnet Latest", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "~google/gemini-flash-latest": { - id: "~google/gemini-flash-latest", - name: "Google Gemini Flash Latest", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.5, - output: 9, - cacheRead: 0.15, - cacheWrite: 0.08333333333333334, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "~google/gemini-pro-latest": { - id: "~google/gemini-pro-latest", - name: "Google Gemini Pro Latest", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.19999999999999998, - cacheWrite: 0.375, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "~moonshotai/kimi-latest": { - id: "~moonshotai/kimi-latest", - name: "MoonshotAI Kimi Latest", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6799999999999999, - output: 3.41, - cacheRead: 0.33999999999999997, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262142, - } satisfies Model<"openai-completions">, - "~openai/gpt-latest": { - id: "~openai/gpt-latest", - name: "OpenAI GPT Latest", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "~openai/gpt-mini-latest": { - id: "~openai/gpt-mini-latest", - name: "OpenAI GPT Mini Latest", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.75, - output: 4.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - }, - "together": { - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax-M2.5", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "MiniMaxAI/MiniMax-M2.7": { - id: "MiniMaxAI/MiniMax-M2.7", - name: "MiniMax-M2.7", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null}, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3-235B-A22B-Instruct-2507-tput": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507-tput", - name: "Qwen3 235B A22B Instruct 2507 FP8", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text"], - cost: { - input: 0.2, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8", - name: "Qwen3 Coder 480B A35B Instruct", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3-Coder-Next-FP8": { - id: "Qwen/Qwen3-Coder-Next-FP8", - name: "Qwen3 Coder Next FP8", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text"], - cost: { - input: 0.5, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3.5-397B-A17B": { - id: "Qwen/Qwen3.5-397B-A17B", - name: "Qwen3.5 397B A17B", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 130000, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3.6-Plus": { - id: "Qwen/Qwen3.6-Plus", - name: "Qwen3.6 Plus", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 500000, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3.7-Max": { - id: "Qwen/Qwen3.7-Max", - name: "Qwen3.7 Max", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text"], - cost: { - input: 2.5, - output: 7.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 500000, - } satisfies Model<"openai-completions">, - "deepseek-ai/DeepSeek-V3": { - id: "deepseek-ai/DeepSeek-V3", - name: "DeepSeek-V3", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text"], - cost: { - input: 1.25, - output: 1.25, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "deepseek-ai/DeepSeek-V3-1": { - id: "deepseek-ai/DeepSeek-V3-1", - name: "DeepSeek V3.1", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text"], - cost: { - input: 0.6, - output: 1.7, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "deepseek-ai/DeepSeek-V4-Pro": { - id: "deepseek-ai/DeepSeek-V4-Pro", - name: "DeepSeek V4 Pro", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":null}, - input: ["text"], - cost: { - input: 2.1, - output: 4.4, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 512000, - maxTokens: 384000, - } satisfies Model<"openai-completions">, - "essentialai/Rnj-1-Instruct": { - id: "essentialai/Rnj-1-Instruct", - name: "Rnj-1 Instruct", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "google/gemma-4-31B-it": { - id: "google/gemma-4-31B-it", - name: "Gemma 4 31B Instruct", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text", "image"], - cost: { - input: 0.2, - output: 0.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "meta-llama/Llama-3.3-70B-Instruct-Turbo": { - id: "meta-llama/Llama-3.3-70B-Instruct-Turbo", - name: "Llama 3.3 70B", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0.88, - output: 0.88, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "Kimi K2.5", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text", "image"], - cost: { - input: 0.5, - output: 2.8, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "moonshotai/Kimi-K2.6": { - id: "moonshotai/Kimi-K2.6", - name: "Kimi K2.6", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text", "image"], - cost: { - input: 1.2, - output: 4.5, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131000, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-ultra-550b-a55b": { - id: "nvidia/nemotron-3-ultra-550b-a55b", - name: "Nemotron 3 Ultra 550B A55B", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text"], - cost: { - input: 0.6, - output: 3.6, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 512300, - maxTokens: 512300, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"openai"}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":null}, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "zai-org/GLM-5.1": { - id: "zai-org/GLM-5.1", - name: "GLM-5.1", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text"], - cost: { - input: 1.4, - output: 4.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, - "vercel-ai-gateway": { - "alibaba/qwen-3-14b": { - id: "alibaba/qwen-3-14b", - name: "Qwen3-14B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.12, - output: 0.24, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 40960, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen-3-235b": { - id: "alibaba/qwen-3-235b", - name: "Qwen3 235B A22B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.22, - output: 0.88, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen-3-30b": { - id: "alibaba/qwen-3-30b", - name: "Qwen3-30B-A3B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.12, - output: 0.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 40960, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen-3-32b": { - id: "alibaba/qwen-3-32b", - name: "Qwen 3 32B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.16, - output: 0.64, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen-3.6-max-preview": { - id: "alibaba/qwen-3.6-max-preview", - name: "Qwen 3.6 Max Preview", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.3, - output: 7.8, - cacheRead: 0.26, - cacheWrite: 1.625, - }, - contextWindow: 240000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-235b-a22b-thinking": { - id: "alibaba/qwen3-235b-a22b-thinking", - name: "Qwen3 VL 235B A22B Thinking", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.39999999999999997, - output: 4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-coder": { - id: "alibaba/qwen3-coder", - name: "Qwen3 Coder 480B A35B Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.5, - output: 7.5, - cacheRead: 0.3, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-coder-30b-a3b": { - id: "alibaba/qwen3-coder-30b-a3b", - name: "Qwen 3 Coder 30B A3B Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-coder-next": { - id: "alibaba/qwen3-coder-next", - name: "Qwen3 Coder Next", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.5, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-coder-plus": { - id: "alibaba/qwen3-coder-plus", - name: "Qwen3 Coder Plus", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 5, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-max": { - id: "alibaba/qwen3-max", - name: "Qwen3 Max", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 1.2, - output: 6, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-max-preview": { - id: "alibaba/qwen3-max-preview", - name: "Qwen3 Max Preview", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 1.2, - output: 6, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-max-thinking": { - id: "alibaba/qwen3-max-thinking", - name: "Qwen 3 Max Thinking", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.2, - output: 6, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-next-80b-a3b-instruct": { - id: "alibaba/qwen3-next-80b-a3b-instruct", - name: "Qwen3 Next 80B A3B Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-next-80b-a3b-thinking": { - id: "alibaba/qwen3-next-80b-a3b-thinking", - name: "Qwen3 Next 80B A3B Thinking", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-vl-thinking": { - id: "alibaba/qwen3-vl-thinking", - name: "Qwen3 VL 235B A22B Thinking", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.39999999999999997, - output: 4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3.5-flash": { - id: "alibaba/qwen3.5-flash", - name: "Qwen 3.5 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, - cacheRead: 0.001, - cacheWrite: 0.125, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3.5-plus": { - id: "alibaba/qwen3.5-plus", - name: "Qwen 3.5 Plus", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.39999999999999997, - output: 2.4, - cacheRead: 0.04, - cacheWrite: 0.5, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3.6-27b": { - id: "alibaba/qwen3.6-27b", - name: "Qwen 3.6 27B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3.5999999999999996, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3.6-plus": { - id: "alibaba/qwen3.6-plus", - name: "Qwen 3.6 Plus", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.09999999999999999, - cacheWrite: 0.625, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3.7-max": { - id: "alibaba/qwen3.7-max", - name: "Qwen 3.7 Max", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.25, - output: 3.75, - cacheRead: 0.25, - cacheWrite: 1.5625, - }, - contextWindow: 991000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3.7-plus": { - id: "alibaba/qwen3.7-plus", - name: "Qwen 3.7 Plus", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.39999999999999997, - output: 1.5999999999999999, - cacheRead: 0.08, - cacheWrite: 0.5, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-3-haiku": { - id: "anthropic/claude-3-haiku", - name: "Claude 3 Haiku", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.25, - cacheRead: 0.03, - cacheWrite: 0.3, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-3.5-haiku": { - id: "anthropic/claude-3.5-haiku", - name: "Claude 3.5 Haiku", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.7999999999999999, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-fable-5": { - id: "anthropic/claude-fable-5", - name: "Claude Fable 5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-haiku-4.5": { - id: "anthropic/claude-haiku-4.5", - name: "Claude Haiku 4.5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.09999999999999999, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-opus-4": { - id: "anthropic/claude-opus-4", - name: "Claude Opus 4", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-opus-4.1": { - id: "anthropic/claude-opus-4.1", - name: "Claude Opus 4.1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-opus-4.5": { - id: "anthropic/claude-opus-4.5", - name: "Claude Opus 4.5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-opus-4.6": { - id: "anthropic/claude-opus-4.6", - name: "Claude Opus 4.6", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-opus-4.7": { - id: "anthropic/claude-opus-4.7", - name: "Claude Opus 4.7", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-opus-4.8": { - id: "anthropic/claude-opus-4.8", - name: "Claude Opus 4.8", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-sonnet-4": { - id: "anthropic/claude-sonnet-4", - name: "Claude Sonnet 4", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-sonnet-4.5": { - id: "anthropic/claude-sonnet-4.5", - name: "Claude Sonnet 4.5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-sonnet-4.6": { - id: "anthropic/claude-sonnet-4.6", - name: "Claude Sonnet 4.6", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "arcee-ai/trinity-large-preview": { - id: "arcee-ai/trinity-large-preview", - name: "Trinity Large Preview", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.25, - output: 1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131000, - maxTokens: 131000, - } satisfies Model<"anthropic-messages">, - "arcee-ai/trinity-large-thinking": { - id: "arcee-ai/trinity-large-thinking", - name: "Trinity Large Thinking", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.25, - output: 0.8999999999999999, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262100, - maxTokens: 80000, - } satisfies Model<"anthropic-messages">, - "bytedance/seed-1.6": { - id: "bytedance/seed-1.6", - name: "Seed 1.6", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "cohere/command-a": { - id: "cohere/command-a", - name: "Command A", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 2.5, - output: 10, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 8000, - } satisfies Model<"anthropic-messages">, - "deepseek/deepseek-r1": { - id: "deepseek/deepseek-r1", - name: "DeepSeek-R1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.35, - output: 5.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "deepseek/deepseek-v3": { - id: "deepseek/deepseek-v3", - name: "DeepSeek V3 0324", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.27, - output: 1.12, - cacheRead: 0.135, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 163840, - } satisfies Model<"anthropic-messages">, - "deepseek/deepseek-v3.1": { - id: "deepseek/deepseek-v3.1", - name: "DeepSeek V3.1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.56, - output: 1.68, - cacheRead: 0.28, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "deepseek/deepseek-v3.1-terminus": { - id: "deepseek/deepseek-v3.1-terminus", - name: "DeepSeek V3.1 Terminus", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.27, - output: 1, - cacheRead: 0.135, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "deepseek/deepseek-v3.2": { - id: "deepseek/deepseek-v3.2", - name: "DeepSeek V3.2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.28, - output: 0.42, - cacheRead: 0.028, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8000, - } satisfies Model<"anthropic-messages">, - "deepseek/deepseek-v3.2-thinking": { - id: "deepseek/deepseek-v3.2-thinking", - name: "DeepSeek V3.2 Thinking", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.62, - output: 1.85, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8000, - } satisfies Model<"anthropic-messages">, - "deepseek/deepseek-v4-flash": { - id: "deepseek/deepseek-v4-flash", - name: "DeepSeek V4 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.14, - output: 0.28, - cacheRead: 0.0028, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 384000, - } satisfies Model<"anthropic-messages">, - "deepseek/deepseek-v4-pro": { - id: "deepseek/deepseek-v4-pro", - name: "DeepSeek V4 Pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.435, - output: 0.87, - cacheRead: 0.0036, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 384000, - } satisfies Model<"anthropic-messages">, - "google/gemini-2.5-flash": { - id: "google/gemini-2.5-flash", - name: "Gemini 2.5 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 2.5, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "google/gemini-2.5-flash-lite": { - id: "google/gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "google/gemini-2.5-pro": { - id: "google/gemini-2.5-pro", - name: "Gemini 2.5 Pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "google/gemini-3-flash": { - id: "google/gemini-3-flash", - name: "Gemini 3 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65000, - } satisfies Model<"anthropic-messages">, - "google/gemini-3-pro-preview": { - id: "google/gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "google/gemini-3.1-flash-lite": { - id: "google/gemini-3.1-flash-lite", - name: "Gemini 3.1 Flash Lite", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.5, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65000, - } satisfies Model<"anthropic-messages">, - "google/gemini-3.1-flash-lite-preview": { - id: "google/gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.5, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65000, - } satisfies Model<"anthropic-messages">, - "google/gemini-3.1-pro-preview": { - id: "google/gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "google/gemini-3.5-flash": { - id: "google/gemini-3.5-flash", - name: "Gemini 3.5 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.5, - output: 9, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "google/gemma-4-26b-a4b-it": { - id: "google/gemma-4-26b-a4b-it", - name: "Gemma 4 26B A4B IT", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.015, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "google/gemma-4-31b-it": { - id: "google/gemma-4-31b-it", - name: "Gemma 4 31B IT", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.14, - output: 0.39999999999999997, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "inception/mercury-2": { - id: "inception/mercury-2", - name: "Mercury 2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.25, - output: 0.75, - cacheRead: 0.024999999999999998, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "inception/mercury-coder-small": { - id: "inception/mercury-coder-small", - name: "Mercury Coder Small Beta", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.25, - output: 1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "kwaipilot/kat-coder-pro-v2": { - id: "kwaipilot/kat-coder-pro-v2", - name: "Kat Coder Pro V2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "meituan/longcat-flash-chat": { - id: "meituan/longcat-flash-chat", - name: "LongCat Flash Chat", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 100000, - } satisfies Model<"anthropic-messages">, - "meta/llama-3.1-70b": { - id: "meta/llama-3.1-70b", - name: "Llama 3.1 70B Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.72, - output: 0.72, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "meta/llama-3.1-8b": { - id: "meta/llama-3.1-8b", - name: "Llama 3.1 8B Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.22, - output: 0.22, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "meta/llama-3.2-11b": { - id: "meta/llama-3.2-11b", - name: "Llama 3.2 11B Vision Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.16, - output: 0.16, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "meta/llama-3.2-90b": { - id: "meta/llama-3.2-90b", - name: "Llama 3.2 90B Vision Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.72, - output: 0.72, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "meta/llama-3.3-70b": { - id: "meta/llama-3.3-70b", - name: "Llama 3.3 70B Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.72, - output: 0.72, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "meta/llama-4-maverick": { - id: "meta/llama-4-maverick", - name: "Llama 4 Maverick 17B Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.24, - output: 0.9700000000000001, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "meta/llama-4-scout": { - id: "meta/llama-4-scout", - name: "Llama 4 Scout 17B Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.16999999999999998, - output: 0.66, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "minimax/minimax-m2": { - id: "minimax/minimax-m2", - name: "MiniMax M2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.03, - cacheWrite: 0.375, - }, - contextWindow: 205000, - maxTokens: 205000, - } satisfies Model<"anthropic-messages">, - "minimax/minimax-m2.1": { - id: "minimax/minimax-m2.1", - name: "MiniMax M2.1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.03, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "minimax/minimax-m2.1-lightning": { - id: "minimax/minimax-m2.1-lightning", - name: "MiniMax M2.1 Lightning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 2.4, - cacheRead: 0.03, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "minimax/minimax-m2.5": { - id: "minimax/minimax-m2.5", - name: "MiniMax M2.5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.03, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131000, - } satisfies Model<"anthropic-messages">, - "minimax/minimax-m2.5-highspeed": { - id: "minimax/minimax-m2.5-highspeed", - name: "MiniMax M2.5 High Speed", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.4, - cacheRead: 0.03, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131000, - } satisfies Model<"anthropic-messages">, - "minimax/minimax-m2.7": { - id: "minimax/minimax-m2.7", - name: "MiniMax M2.7", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131000, - } satisfies Model<"anthropic-messages">, - "minimax/minimax-m2.7-highspeed": { - id: "minimax/minimax-m2.7-highspeed", - name: "MiniMax M2.7 High Speed", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.4, - cacheRead: 0.06, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131100, - } satisfies Model<"anthropic-messages">, - "minimax/minimax-m3": { - id: "minimax/minimax-m3", - name: "MiniMax M3", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 1000000, - } satisfies Model<"anthropic-messages">, - "mistral/codestral": { - id: "mistral/codestral", - name: "Mistral Codestral", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.3, - output: 0.8999999999999999, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4000, - } satisfies Model<"anthropic-messages">, - "mistral/devstral-2": { - id: "mistral/devstral-2", - name: "Devstral 2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.39999999999999997, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "mistral/devstral-small": { - id: "mistral/devstral-small", - name: "Devstral Small 1.1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.09999999999999999, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "mistral/devstral-small-2": { - id: "mistral/devstral-small-2", - name: "Devstral Small 2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.09999999999999999, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "mistral/ministral-3b": { - id: "mistral/ministral-3b", - name: "Ministral 3B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.09999999999999999, - output: 0.09999999999999999, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4000, - } satisfies Model<"anthropic-messages">, - "mistral/ministral-8b": { - id: "mistral/ministral-8b", - name: "Ministral 8B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4000, - } satisfies Model<"anthropic-messages">, - "mistral/mistral-medium": { - id: "mistral/mistral-medium", - name: "Mistral Medium 3.1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.39999999999999997, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "mistral/mistral-medium-3.5": { - id: "mistral/mistral-medium-3.5", - name: "Mistral Medium Latest", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.5, - output: 7.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "mistral/mistral-nemo": { - id: "mistral/mistral-nemo", - name: "Mistral Nemo 12B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.02, - output: 0.04, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "mistral/mistral-small": { - id: "mistral/mistral-small", - name: "Mistral Small", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32000, - maxTokens: 4000, - } satisfies Model<"anthropic-messages">, - "mistral/pixtral-12b": { - id: "mistral/pixtral-12b", - name: "Pixtral 12B 2409", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4000, - } satisfies Model<"anthropic-messages">, - "mistral/pixtral-large": { - id: "mistral/pixtral-large", - name: "Pixtral Large", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4000, - } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2": { - id: "moonshotai/kimi-k2", - name: "Kimi K2 Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.5700000000000001, - output: 2.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2-thinking": { - id: "moonshotai/kimi-k2-thinking", - name: "Kimi K2 Thinking", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262114, - maxTokens: 262114, - } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2-thinking-turbo": { - id: "moonshotai/kimi-k2-thinking-turbo", - name: "Kimi K2 Thinking Turbo", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.15, - output: 8, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262114, - maxTokens: 262114, - } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2-turbo": { - id: "moonshotai/kimi-k2-turbo", - name: "Kimi K2 Turbo", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 1.15, - output: 8, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "Kimi K2.5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0.09999999999999999, - cacheWrite: 0, - }, - contextWindow: 262114, - maxTokens: 262114, - } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2.6": { - id: "moonshotai/kimi-k2.6", - name: "Kimi K2.6", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.16, - cacheWrite: 0, - }, - contextWindow: 262000, - maxTokens: 262000, - } satisfies Model<"anthropic-messages">, - "nvidia/nemotron-3-super-120b-a12b": { - id: "nvidia/nemotron-3-super-120b-a12b", - name: "NVIDIA Nemotron 3 Super 120B A12B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.65, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "nvidia/nemotron-3-ultra-550b-a55b": { - id: "nvidia/nemotron-3-ultra-550b-a55b", - name: "Nemotron 3 Ultra", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.4, - cacheRead: 0.12, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65000, - } satisfies Model<"anthropic-messages">, - "nvidia/nemotron-nano-12b-v2-vl": { - id: "nvidia/nemotron-nano-12b-v2-vl", - name: "Nvidia Nemotron Nano 12B V2 VL", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "nvidia/nemotron-nano-9b-v2": { - id: "nvidia/nemotron-nano-9b-v2", - name: "Nvidia Nemotron Nano 9B V2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.06, - output: 0.22999999999999998, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "openai/gpt-4-turbo": { - id: "openai/gpt-4-turbo", - name: "GPT-4 Turbo", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 10, - output: 30, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"anthropic-messages">, - "openai/gpt-4.1": { - id: "openai/gpt-4.1", - name: "GPT-4.1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "openai/gpt-4.1-mini": { - id: "openai/gpt-4.1-mini", - name: "GPT-4.1 mini", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.39999999999999997, - output: 1.5999999999999999, - cacheRead: 0.09999999999999999, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "openai/gpt-4.1-nano": { - id: "openai/gpt-4.1-nano", - name: "GPT-4.1 nano", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, - cacheRead: 0.024999999999999998, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "openai/gpt-4o": { - id: "openai/gpt-4o", - name: "GPT-4o", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "openai/gpt-4o-mini": { - id: "openai/gpt-4o-mini", - name: "GPT-4o mini", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "GPT-5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5-chat": { - id: "openai/gpt-5-chat", - name: "GPT 5 Chat", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5-codex": { - id: "openai/gpt-5-codex", - name: "GPT-5-Codex", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5-mini": { - id: "openai/gpt-5-mini", - name: "GPT-5 mini", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.024999999999999998, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5-nano": { - id: "openai/gpt-5-nano", - name: "GPT-5 nano", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.049999999999999996, - output: 0.39999999999999997, - cacheRead: 0.005, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5-pro": { - id: "openai/gpt-5-pro", - name: "GPT-5 pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 120, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 272000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.1-codex": { - id: "openai/gpt-5.1-codex", - name: "GPT-5.1-Codex", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.1-codex-max": { - id: "openai/gpt-5.1-codex-max", - name: "GPT 5.1 Codex Max", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.1-codex-mini": { - id: "openai/gpt-5.1-codex-mini", - name: "GPT 5.1 Codex Mini", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.024999999999999998, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.1-instant": { - id: "openai/gpt-5.1-instant", - name: "GPT-5.1 Instant", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.1-thinking": { - id: "openai/gpt-5.1-thinking", - name: "GPT 5.1 Thinking", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "GPT 5.2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.2-chat": { - id: "openai/gpt-5.2-chat", - name: "GPT 5.2 Chat", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.2-codex": { - id: "openai/gpt-5.2-codex", - name: "GPT 5.2 Codex", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.2-pro": { - id: "openai/gpt-5.2-pro", - name: "GPT 5.2 ", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 21, - output: 168, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.3-chat": { - id: "openai/gpt-5.3-chat", - name: "GPT-5.3 Chat", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.3-codex": { - id: "openai/gpt-5.3-codex", - name: "GPT 5.3 Codex", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.4": { - id: "openai/gpt-5.4", - name: "GPT 5.4", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.4-mini": { - id: "openai/gpt-5.4-mini", - name: "GPT 5.4 Mini", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.75, - output: 4.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.4-nano": { - id: "openai/gpt-5.4-nano", - name: "GPT 5.4 Nano", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 1.25, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.4-pro": { - id: "openai/gpt-5.4-pro", - name: "GPT 5.4 Pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.5": { - id: "openai/gpt-5.5", - name: "GPT 5.5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.5-pro": { - id: "openai/gpt-5.5-pro", - name: "GPT 5.5 Pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh","off":null,"minimal":null,"low":null}, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.35, - output: 0.75, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.049999999999999996, - output: 0.19999999999999998, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "openai/gpt-oss-safeguard-20b": { - id: "openai/gpt-oss-safeguard-20b", - name: "GPT OSS Safeguard 20B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0.037, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "openai/o1": { - id: "openai/o1", - name: "o1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 60, - cacheRead: 7.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"anthropic-messages">, - "openai/o3": { - id: "openai/o3", - name: "o3", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"anthropic-messages">, - "openai/o3-deep-research": { - id: "openai/o3-deep-research", - name: "o3-deep-research", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 10, - output: 40, - cacheRead: 2.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"anthropic-messages">, - "openai/o3-mini": { - id: "openai/o3-mini", - name: "o3-mini", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.55, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"anthropic-messages">, - "openai/o3-pro": { - id: "openai/o3-pro", - name: "o3 Pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 20, - output: 80, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"anthropic-messages">, - "openai/o4-mini": { - id: "openai/o4-mini", - name: "o4-mini", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.275, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"anthropic-messages">, - "perplexity/sonar": { - id: "perplexity/sonar", - name: "Sonar", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 127000, - maxTokens: 8000, - } satisfies Model<"anthropic-messages">, - "perplexity/sonar-pro": { - id: "perplexity/sonar-pro", - name: "Sonar Pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 8000, - } satisfies Model<"anthropic-messages">, - "stepfun/step-3.5-flash": { - id: "stepfun/step-3.5-flash", - name: "StepFun 3.5 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.09, - output: 0.3, - cacheRead: 0, - cacheWrite: 0.02, - }, - contextWindow: 262114, - maxTokens: 262114, - } satisfies Model<"anthropic-messages">, - "stepfun/step-3.7-flash": { - id: "stepfun/step-3.7-flash", - name: "Step 3.7 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 1.15, - cacheRead: 0.04, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.1-fast-non-reasoning": { - id: "xai/grok-4.1-fast-non-reasoning", - name: "Grok 4.1 Fast Non-Reasoning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 0.5, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 1000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.1-fast-reasoning": { - id: "xai/grok-4.1-fast-reasoning", - name: "Grok 4.1 Fast Reasoning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 0.5, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 1000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.20-multi-agent": { - id: "xai/grok-4.20-multi-agent", - name: "Grok 4.20 Multi-Agent", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 2000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.20-multi-agent-beta": { - id: "xai/grok-4.20-multi-agent-beta", - name: "Grok 4.20 Multi Agent Beta", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 2000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.20-non-reasoning": { - id: "xai/grok-4.20-non-reasoning", - name: "Grok 4.20 Non-Reasoning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 2000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.20-non-reasoning-beta": { - id: "xai/grok-4.20-non-reasoning-beta", - name: "Grok 4.20 Beta Non-Reasoning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 2000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.20-reasoning": { - id: "xai/grok-4.20-reasoning", - name: "Grok 4.20 Reasoning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 2000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.20-reasoning-beta": { - id: "xai/grok-4.20-reasoning-beta", - name: "Grok 4.20 Beta Reasoning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 2000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.3": { - id: "xai/grok-4.3", - name: "Grok 4.3", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 1000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-build-0.1": { - id: "xai/grok-build-0.1", - name: "Grok Build 0.1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 2, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "xiaomi/mimo-v2-flash": { - id: "xiaomi/mimo-v2-flash", - name: "MiMo V2 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.09999999999999999, - output: 0.3, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "xiaomi/mimo-v2-pro": { - id: "xiaomi/mimo-v2-pro", - name: "MiMo V2 Pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "xiaomi/mimo-v2.5": { - id: "xiaomi/mimo-v2.5", - name: "MiMo M2.5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.14, - output: 0.28, - cacheRead: 0.0028, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 131100, - } satisfies Model<"anthropic-messages">, - "xiaomi/mimo-v2.5-pro": { - id: "xiaomi/mimo-v2.5-pro", - name: "MiMo V2.5 Pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.435, - output: 0.87, - cacheRead: 0.0036, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 131000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.5": { - id: "zai/glm-4.5", - name: "GLM-4.5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.2, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 96000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.5-air": { - id: "zai/glm-4.5-air", - name: "GLM 4.5 Air", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.19999999999999998, - output: 1.1, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 96000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.5v": { - id: "zai/glm-4.5v", - name: "GLM 4.5V", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 1.7999999999999998, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 66000, - maxTokens: 16000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.6": { - id: "zai/glm-4.6", - name: "GLM 4.6", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.2, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 96000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.6v": { - id: "zai/glm-4.6v", - name: "GLM-4.6V", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 0.8999999999999999, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 24000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.6v-flash": { - id: "zai/glm-4.6v-flash", - name: "GLM-4.6V-Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 24000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.7": { - id: "zai/glm-4.7", - name: "GLM 4.7", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 2.25, - output: 2.75, - cacheRead: 2.25, - cacheWrite: 0, - }, - contextWindow: 131000, - maxTokens: 40000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.7-flash": { - id: "zai/glm-4.7-flash", - name: "GLM 4.7 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.07, - output: 0.39999999999999997, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 131000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.7-flashx": { - id: "zai/glm-4.7-flashx", - name: "GLM 4.7 FlashX", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.06, - output: 0.39999999999999997, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "zai/glm-5": { - id: "zai/glm-5", - name: "GLM 5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3.1999999999999997, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 202800, - maxTokens: 131100, - } satisfies Model<"anthropic-messages">, - "zai/glm-5-turbo": { - id: "zai/glm-5-turbo", - name: "GLM 5 Turbo", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.2, - output: 4, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 202800, - maxTokens: 131100, - } satisfies Model<"anthropic-messages">, - "zai/glm-5.1": { - id: "zai/glm-5.1", - name: "GLM 5.1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.4, - output: 4.4, - cacheRead: 0.26, - cacheWrite: 0, - }, - contextWindow: 202800, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "zai/glm-5v-turbo": { - id: "zai/glm-5v-turbo", - name: "GLM 5V Turbo", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.2, - output: 4, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - }, - "xai": { - "grok-3": { - id: "grok-3", - name: "Grok 3", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 3, - output: 15, - cacheRead: 0.75, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-3-fast": { - id: "grok-3-fast", - name: "Grok 3 Fast", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 5, - output: 25, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-4.20-0309-non-reasoning": { - id: "grok-4.20-0309-non-reasoning", - name: "Grok 4.20 (Non-Reasoning)", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, - "grok-4.20-0309-reasoning": { - id: "grok-4.20-0309-reasoning", - name: "Grok 4.20 (Reasoning)", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, - "grok-4.3": { - id: "grok-4.3", - name: "Grok 4.3", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, - "grok-build-0.1": { - id: "grok-build-0.1", - name: "Grok Build 0.1", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"openai-completions">, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "Grok Code Fast 1", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.2, - output: 1.5, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - }, - "xiaomi": { - "mimo-v2-flash": { - id: "mimo-v2-flash", - name: "MiMo-V2-Flash", - api: "openai-completions", - provider: "xiaomi", - baseUrl: "https://api.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo-V2-Omni", - api: "openai-completions", - provider: "xiaomi", - baseUrl: "https://api.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo-V2-Pro", - api: "openai-completions", - provider: "xiaomi", - baseUrl: "https://api.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5": { - id: "mimo-v2.5", - name: "MiMo-V2.5", - api: "openai-completions", - provider: "xiaomi", - baseUrl: "https://api.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5-pro": { - id: "mimo-v2.5-pro", - name: "MiMo-V2.5-Pro", - api: "openai-completions", - provider: "xiaomi", - baseUrl: "https://api.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5-pro-ultraspeed": { - id: "mimo-v2.5-pro-ultraspeed", - name: "MiMo-V2.5-Pro-UltraSpeed", - api: "openai-completions", - provider: "xiaomi", - baseUrl: "https://api.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1.305, - output: 2.61, - cacheRead: 0.0108, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, - "xiaomi-token-plan-ams": { - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo-V2-Omni", - api: "openai-completions", - provider: "xiaomi-token-plan-ams", - baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo-V2-Pro", - api: "openai-completions", - provider: "xiaomi-token-plan-ams", - baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5": { - id: "mimo-v2.5", - name: "MiMo-V2.5", - api: "openai-completions", - provider: "xiaomi-token-plan-ams", - baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5-pro": { - id: "mimo-v2.5-pro", - name: "MiMo-V2.5-Pro", - api: "openai-completions", - provider: "xiaomi-token-plan-ams", - baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5-pro-ultraspeed": { - id: "mimo-v2.5-pro-ultraspeed", - name: "MiMo-V2.5-Pro-UltraSpeed", - api: "openai-completions", - provider: "xiaomi-token-plan-ams", - baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1.305, - output: 2.61, - cacheRead: 0.0108, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, - "xiaomi-token-plan-cn": { - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo-V2-Omni", - api: "openai-completions", - provider: "xiaomi-token-plan-cn", - baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo-V2-Pro", - api: "openai-completions", - provider: "xiaomi-token-plan-cn", - baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5": { - id: "mimo-v2.5", - name: "MiMo-V2.5", - api: "openai-completions", - provider: "xiaomi-token-plan-cn", - baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5-pro": { - id: "mimo-v2.5-pro", - name: "MiMo-V2.5-Pro", - api: "openai-completions", - provider: "xiaomi-token-plan-cn", - baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5-pro-ultraspeed": { - id: "mimo-v2.5-pro-ultraspeed", - name: "MiMo-V2.5-Pro-UltraSpeed", - api: "openai-completions", - provider: "xiaomi-token-plan-cn", - baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1.305, - output: 2.61, - cacheRead: 0.0108, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, - "xiaomi-token-plan-sgp": { - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo-V2-Omni", - api: "openai-completions", - provider: "xiaomi-token-plan-sgp", - baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo-V2-Pro", - api: "openai-completions", - provider: "xiaomi-token-plan-sgp", - baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5": { - id: "mimo-v2.5", - name: "MiMo-V2.5", - api: "openai-completions", - provider: "xiaomi-token-plan-sgp", - baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5-pro": { - id: "mimo-v2.5-pro", - name: "MiMo-V2.5-Pro", - api: "openai-completions", - provider: "xiaomi-token-plan-sgp", - baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5-pro-ultraspeed": { - id: "mimo-v2.5-pro-ultraspeed", - name: "MiMo-V2.5-Pro-UltraSpeed", - api: "openai-completions", - provider: "xiaomi-token-plan-sgp", - baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1.305, - output: 2.61, - cacheRead: 0.0108, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, - "zai": { - "glm-4.5-air": { - id: "glm-4.5-air", - name: "GLM-4.5-Air", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 98304, - } satisfies Model<"openai-completions">, - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-5-turbo": { - id: "glm-5-turbo", - name: "GLM-5-Turbo", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-5v-turbo": { - id: "glm-5v-turbo", - name: "GLM-5V-Turbo", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, - "zai-coding-cn": { - "glm-4.5-air": { - id: "glm-4.5-air", - name: "GLM-4.5-Air", - api: "openai-completions", - provider: "zai-coding-cn", - baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 98304, - } satisfies Model<"openai-completions">, - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - api: "openai-completions", - provider: "zai-coding-cn", - baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-5-turbo": { - id: "glm-5-turbo", - name: "GLM-5-Turbo", - api: "openai-completions", - provider: "zai-coding-cn", - baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - api: "openai-completions", - provider: "zai-coding-cn", - baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-5v-turbo": { - id: "glm-5v-turbo", - name: "GLM-5V-Turbo", - api: "openai-completions", - provider: "zai-coding-cn", - baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, + "amazon-bedrock": AMAZON_BEDROCK_MODELS, + "ant-ling": ANT_LING_MODELS, + "anthropic": ANTHROPIC_MODELS, + "azure-openai-responses": AZURE_OPENAI_RESPONSES_MODELS, + "cerebras": CEREBRAS_MODELS, + "cloudflare-ai-gateway": CLOUDFLARE_AI_GATEWAY_MODELS, + "cloudflare-workers-ai": CLOUDFLARE_WORKERS_AI_MODELS, + "deepseek": DEEPSEEK_MODELS, + "fireworks": FIREWORKS_MODELS, + "github-copilot": GITHUB_COPILOT_MODELS, + "google": GOOGLE_MODELS, + "google-vertex": GOOGLE_VERTEX_MODELS, + "groq": GROQ_MODELS, + "huggingface": HUGGINGFACE_MODELS, + "kimi-coding": KIMI_CODING_MODELS, + "minimax": MINIMAX_MODELS, + "minimax-cn": MINIMAX_CN_MODELS, + "mistral": MISTRAL_MODELS, + "moonshotai": MOONSHOTAI_MODELS, + "moonshotai-cn": MOONSHOTAI_CN_MODELS, + "nvidia": NVIDIA_MODELS, + "openai": OPENAI_MODELS, + "openai-codex": OPENAI_CODEX_MODELS, + "opencode": OPENCODE_MODELS, + "opencode-go": OPENCODE_GO_MODELS, + "openrouter": OPENROUTER_MODELS, + "together": TOGETHER_MODELS, + "vercel-ai-gateway": VERCEL_AI_GATEWAY_MODELS, + "xai": XAI_MODELS, + "xiaomi": XIAOMI_MODELS, + "xiaomi-token-plan-ams": XIAOMI_TOKEN_PLAN_AMS_MODELS, + "xiaomi-token-plan-cn": XIAOMI_TOKEN_PLAN_CN_MODELS, + "xiaomi-token-plan-sgp": XIAOMI_TOKEN_PLAN_SGP_MODELS, + "zai": ZAI_MODELS, + "zai-coding-cn": ZAI_CODING_CN_MODELS, } as const; diff --git a/packages/ai/src/models.ts b/packages/ai/src/models.ts index ddfdcc02..dd698831 100644 --- a/packages/ai/src/models.ts +++ b/packages/ai/src/models.ts @@ -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 { + id: string; + /** Display name. Default: `id`. */ + name?: string; + baseUrl?: string; + headers?: Record; + /** Required — every provider has auth semantics, even ambient/keyless ones. */ + auth: ProviderAuth; + models: + | readonly Model[] + | ((options?: { forceRefresh?: boolean }) => Promise[]> | readonly Model[]); + /** Single implementation, or map keyed by `model.api` for mixed-API providers. */ + api: ProviderStreams | Partial>; +} + +/** + * 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(input: CreateProviderOptions): Provider { + 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>); + + const apiFor = (model: Model): ProviderStreams | undefined => single ?? byApi?.[model.api]; + + const dispatch = ( + model: Model, + 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: * diff --git a/packages/ai/src/providers/all.ts b/packages/ai/src/providers/all.ts new file mode 100644 index 00000000..7097fcdd --- /dev/null +++ b/packages/ai/src/providers/all.ts @@ -0,0 +1,92 @@ +import { type CreateModelsOptions, createModels, type MutableModels, type Provider } from "../models.ts"; +import { amazonBedrockProvider } from "./amazon-bedrock.ts"; +import { antLingProvider } from "./ant-ling.ts"; +import { anthropicProvider } from "./anthropic.ts"; +import { azureOpenAIResponsesProvider } from "./azure-openai-responses.ts"; +import { cerebrasProvider } from "./cerebras.ts"; +import { cloudflareAIGatewayProvider } from "./cloudflare-ai-gateway.ts"; +import { cloudflareWorkersAIProvider } from "./cloudflare-workers-ai.ts"; +import { deepseekProvider } from "./deepseek.ts"; +import { fireworksProvider } from "./fireworks.ts"; +import { githubCopilotProvider } from "./github-copilot.ts"; +import { googleProvider } from "./google.ts"; +import { googleVertexProvider } from "./google-vertex.ts"; +import { groqProvider } from "./groq.ts"; +import { huggingfaceProvider } from "./huggingface.ts"; +import { kimiCodingProvider } from "./kimi-coding.ts"; +import { minimaxProvider } from "./minimax.ts"; +import { minimaxCnProvider } from "./minimax-cn.ts"; +import { mistralProvider } from "./mistral.ts"; +import { moonshotaiProvider } from "./moonshotai.ts"; +import { moonshotaiCnProvider } from "./moonshotai-cn.ts"; +import { nvidiaProvider } from "./nvidia.ts"; +import { openaiProvider } from "./openai.ts"; +import { openaiCodexProvider } from "./openai-codex.ts"; +import { opencodeProvider } from "./opencode.ts"; +import { opencodeGoProvider } from "./opencode-go.ts"; +import { openrouterProvider } from "./openrouter.ts"; +import { togetherProvider } from "./together.ts"; +import { vercelAIGatewayProvider } from "./vercel-ai-gateway.ts"; +import { xaiProvider } from "./xai.ts"; +import { xiaomiProvider } from "./xiaomi.ts"; +import { xiaomiTokenPlanAmsProvider } from "./xiaomi-token-plan-ams.ts"; +import { xiaomiTokenPlanCnProvider } from "./xiaomi-token-plan-cn.ts"; +import { xiaomiTokenPlanSgpProvider } from "./xiaomi-token-plan-sgp.ts"; +import { zaiProvider } from "./zai.ts"; +import { zaiCodingCnProvider } from "./zai-coding-cn.ts"; + +export { + getModel as getBuiltinModel, + getModels as getBuiltinModels, + getProviders as getBuiltinProviders, +} from "../models.ts"; + +/** All built-in providers, freshly constructed. */ +export function builtinProviders(): Provider[] { + return [ + amazonBedrockProvider(), + antLingProvider(), + anthropicProvider(), + azureOpenAIResponsesProvider(), + cerebrasProvider(), + cloudflareAIGatewayProvider(), + cloudflareWorkersAIProvider(), + deepseekProvider(), + fireworksProvider(), + githubCopilotProvider(), + googleProvider(), + googleVertexProvider(), + groqProvider(), + huggingfaceProvider(), + kimiCodingProvider(), + minimaxProvider(), + minimaxCnProvider(), + mistralProvider(), + moonshotaiProvider(), + moonshotaiCnProvider(), + nvidiaProvider(), + openaiProvider(), + openaiCodexProvider(), + opencodeProvider(), + opencodeGoProvider(), + openrouterProvider(), + togetherProvider(), + vercelAIGatewayProvider(), + xaiProvider(), + xiaomiProvider(), + xiaomiTokenPlanAmsProvider(), + xiaomiTokenPlanCnProvider(), + xiaomiTokenPlanSgpProvider(), + zaiProvider(), + zaiCodingCnProvider(), + ]; +} + +/** A `Models` collection with every built-in provider registered. */ +export function builtinModels(options?: CreateModelsOptions): MutableModels { + const models = createModels(options); + for (const provider of builtinProviders()) { + models.setProvider(provider); + } + return models; +} diff --git a/packages/ai/src/providers/amazon-bedrock.models.ts b/packages/ai/src/providers/amazon-bedrock.models.ts new file mode 100644 index 00000000..37c21dce --- /dev/null +++ b/packages/ai/src/providers/amazon-bedrock.models.ts @@ -0,0 +1,1677 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const AMAZON_BEDROCK_MODELS = { + "amazon.nova-2-lite-v1:0": { + id: "amazon.nova-2-lite-v1:0", + name: "Nova 2 Lite", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.33, + output: 2.75, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"bedrock-converse-stream">, + "amazon.nova-lite-v1:0": { + id: "amazon.nova-lite-v1:0", + name: "Nova Lite", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.06, + output: 0.24, + cacheRead: 0.015, + cacheWrite: 0, + }, + contextWindow: 300000, + maxTokens: 8192, + } satisfies Model<"bedrock-converse-stream">, + "amazon.nova-micro-v1:0": { + id: "amazon.nova-micro-v1:0", + name: "Nova Micro", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.035, + output: 0.14, + cacheRead: 0.00875, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"bedrock-converse-stream">, + "amazon.nova-pro-v1:0": { + id: "amazon.nova-pro-v1:0", + name: "Nova Pro", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.8, + output: 3.2, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 300000, + maxTokens: 8192, + } satisfies Model<"bedrock-converse-stream">, + "anthropic.claude-haiku-4-5-20251001-v1:0": { + id: "anthropic.claude-haiku-4-5-20251001-v1:0", + name: "Claude Haiku 4.5", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "anthropic.claude-opus-4-1-20250805-v1:0": { + id: "anthropic.claude-opus-4-1-20250805-v1:0", + name: "Claude Opus 4.1", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"bedrock-converse-stream">, + "anthropic.claude-opus-4-5-20251101-v1:0": { + id: "anthropic.claude-opus-4-5-20251101-v1:0", + name: "Claude Opus 4.5", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "anthropic.claude-opus-4-6-v1": { + id: "anthropic.claude-opus-4-6-v1", + name: "Claude Opus 4.6", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "anthropic.claude-opus-4-7": { + id: "anthropic.claude-opus-4-7", + name: "Claude Opus 4.7", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "anthropic.claude-opus-4-8": { + id: "anthropic.claude-opus-4-8", + name: "Claude Opus 4.8", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "anthropic.claude-sonnet-4-5-20250929-v1:0": { + id: "anthropic.claude-sonnet-4-5-20250929-v1:0", + name: "Claude Sonnet 4.5", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "anthropic.claude-sonnet-4-6": { + id: "anthropic.claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "au.anthropic.claude-haiku-4-5-20251001-v1:0": { + id: "au.anthropic.claude-haiku-4-5-20251001-v1:0", + name: "Claude Haiku 4.5 (AU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "au.anthropic.claude-opus-4-6-v1": { + id: "au.anthropic.claude-opus-4-6-v1", + name: "AU Anthropic Claude Opus 4.6", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 16.5, + output: 82.5, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "au.anthropic.claude-opus-4-8": { + id: "au.anthropic.claude-opus-4-8", + name: "Claude Opus 4.8 (AU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "au.anthropic.claude-sonnet-4-5-20250929-v1:0": { + id: "au.anthropic.claude-sonnet-4-5-20250929-v1:0", + name: "Claude Sonnet 4.5 (AU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "au.anthropic.claude-sonnet-4-6": { + id: "au.anthropic.claude-sonnet-4-6", + name: "AU Anthropic Claude Sonnet 4.6", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3.3, + output: 16.5, + cacheRead: 0.33, + cacheWrite: 4.125, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "deepseek.r1-v1:0": { + id: "deepseek.r1-v1:0", + name: "DeepSeek-R1", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 1.35, + output: 5.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 32768, + } satisfies Model<"bedrock-converse-stream">, + "deepseek.v3-v1:0": { + id: "deepseek.v3-v1:0", + name: "DeepSeek-V3.1", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.58, + output: 1.68, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 81920, + } satisfies Model<"bedrock-converse-stream">, + "deepseek.v3.2": { + id: "deepseek.v3.2", + name: "DeepSeek-V3.2", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.62, + output: 1.85, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 81920, + } satisfies Model<"bedrock-converse-stream">, + "eu.anthropic.claude-fable-5": { + id: "eu.anthropic.claude-fable-5", + name: "Claude Fable 5 (EU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 11, + output: 55, + cacheRead: 1.1, + cacheWrite: 13.75, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "eu.anthropic.claude-haiku-4-5-20251001-v1:0": { + id: "eu.anthropic.claude-haiku-4-5-20251001-v1:0", + name: "Claude Haiku 4.5 (EU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "eu.anthropic.claude-opus-4-5-20251101-v1:0": { + id: "eu.anthropic.claude-opus-4-5-20251101-v1:0", + name: "Claude Opus 4.5 (EU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "eu.anthropic.claude-opus-4-6-v1": { + id: "eu.anthropic.claude-opus-4-6-v1", + name: "Claude Opus 4.6 (EU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 5.5, + output: 27.5, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "eu.anthropic.claude-opus-4-7": { + id: "eu.anthropic.claude-opus-4-7", + name: "Claude Opus 4.7 (EU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5.5, + output: 27.5, + cacheRead: 0.55, + cacheWrite: 6.875, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "eu.anthropic.claude-opus-4-8": { + id: "eu.anthropic.claude-opus-4-8", + name: "Claude Opus 4.8 (EU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5.5, + output: 27.5, + cacheRead: 0.55, + cacheWrite: 6.875, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "eu.anthropic.claude-sonnet-4-5-20250929-v1:0": { + id: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0", + name: "Claude Sonnet 4.5 (EU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3.3, + output: 16.5, + cacheRead: 0.33, + cacheWrite: 4.125, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "eu.anthropic.claude-sonnet-4-6": { + id: "eu.anthropic.claude-sonnet-4-6", + name: "Claude Sonnet 4.6 (EU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3.3, + output: 16.5, + cacheRead: 0.33, + cacheWrite: 4.125, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "global.anthropic.claude-fable-5": { + id: "global.anthropic.claude-fable-5", + name: "Claude Fable 5 (Global)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 1, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "global.anthropic.claude-haiku-4-5-20251001-v1:0": { + id: "global.anthropic.claude-haiku-4-5-20251001-v1:0", + name: "Claude Haiku 4.5 (Global)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "global.anthropic.claude-opus-4-5-20251101-v1:0": { + id: "global.anthropic.claude-opus-4-5-20251101-v1:0", + name: "Claude Opus 4.5 (Global)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "global.anthropic.claude-opus-4-6-v1": { + id: "global.anthropic.claude-opus-4-6-v1", + name: "Claude Opus 4.6 (Global)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "global.anthropic.claude-opus-4-7": { + id: "global.anthropic.claude-opus-4-7", + name: "Claude Opus 4.7 (Global)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "global.anthropic.claude-opus-4-8": { + id: "global.anthropic.claude-opus-4-8", + name: "Claude Opus 4.8 (Global)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "global.anthropic.claude-sonnet-4-5-20250929-v1:0": { + id: "global.anthropic.claude-sonnet-4-5-20250929-v1:0", + name: "Claude Sonnet 4.5 (Global)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "global.anthropic.claude-sonnet-4-6": { + id: "global.anthropic.claude-sonnet-4-6", + name: "Claude Sonnet 4.6 (Global)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "google.gemma-3-27b-it": { + id: "google.gemma-3-27b-it", + name: "Google Gemma 3 27B Instruct", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.12, + output: 0.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 8192, + } satisfies Model<"bedrock-converse-stream">, + "google.gemma-3-4b-it": { + id: "google.gemma-3-4b-it", + name: "Gemma 3 4B IT", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.04, + output: 0.08, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"bedrock-converse-stream">, + "jp.anthropic.claude-opus-4-7": { + id: "jp.anthropic.claude-opus-4-7", + name: "Claude Opus 4.7 (JP)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "jp.anthropic.claude-opus-4-8": { + id: "jp.anthropic.claude-opus-4-8", + name: "Claude Opus 4.8 (JP)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "jp.anthropic.claude-sonnet-4-5-20250929-v1:0": { + id: "jp.anthropic.claude-sonnet-4-5-20250929-v1:0", + name: "Claude Sonnet 4.5 (JP)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "jp.anthropic.claude-sonnet-4-6": { + id: "jp.anthropic.claude-sonnet-4-6", + name: "Claude Sonnet 4.6 (JP)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "meta.llama3-1-70b-instruct-v1:0": { + id: "meta.llama3-1-70b-instruct-v1:0", + name: "Llama 3.1 70B Instruct", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.72, + output: 0.72, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"bedrock-converse-stream">, + "meta.llama3-1-8b-instruct-v1:0": { + id: "meta.llama3-1-8b-instruct-v1:0", + name: "Llama 3.1 8B Instruct", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.22, + output: 0.22, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"bedrock-converse-stream">, + "meta.llama3-3-70b-instruct-v1:0": { + id: "meta.llama3-3-70b-instruct-v1:0", + name: "Llama 3.3 70B Instruct", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.72, + output: 0.72, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"bedrock-converse-stream">, + "meta.llama4-maverick-17b-instruct-v1:0": { + id: "meta.llama4-maverick-17b-instruct-v1:0", + name: "Llama 4 Maverick 17B Instruct", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.24, + output: 0.97, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "meta.llama4-scout-17b-instruct-v1:0": { + id: "meta.llama4-scout-17b-instruct-v1:0", + name: "Llama 4 Scout 17B Instruct", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.17, + output: 0.66, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 3500000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "minimax.minimax-m2": { + id: "minimax.minimax-m2", + name: "MiniMax M2", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 204608, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "minimax.minimax-m2.1": { + id: "minimax.minimax-m2.1", + name: "MiniMax M2.1", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"bedrock-converse-stream">, + "minimax.minimax-m2.5": { + id: "minimax.minimax-m2.5", + name: "MiniMax M2.5", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 196608, + maxTokens: 98304, + } satisfies Model<"bedrock-converse-stream">, + "mistral.devstral-2-123b": { + id: "mistral.devstral-2-123b", + name: "Devstral 2 123B", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 8192, + } satisfies Model<"bedrock-converse-stream">, + "mistral.magistral-small-2509": { + id: "mistral.magistral-small-2509", + name: "Magistral Small 1.2", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.5, + output: 1.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 40000, + } satisfies Model<"bedrock-converse-stream">, + "mistral.ministral-3-14b-instruct": { + id: "mistral.ministral-3-14b-instruct", + name: "Ministral 14B 3.0", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.2, + output: 0.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"bedrock-converse-stream">, + "mistral.ministral-3-3b-instruct": { + id: "mistral.ministral-3-3b-instruct", + name: "Ministral 3 3B", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.1, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 8192, + } satisfies Model<"bedrock-converse-stream">, + "mistral.ministral-3-8b-instruct": { + id: "mistral.ministral-3-8b-instruct", + name: "Ministral 3 8B", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.15, + output: 0.15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"bedrock-converse-stream">, + "mistral.mistral-large-3-675b-instruct": { + id: "mistral.mistral-large-3-675b-instruct", + name: "Mistral Large 3", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.5, + output: 1.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 8192, + } satisfies Model<"bedrock-converse-stream">, + "mistral.pixtral-large-2502-v1:0": { + id: "mistral.pixtral-large-2502-v1:0", + name: "Pixtral Large (25.02)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2, + output: 6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"bedrock-converse-stream">, + "mistral.voxtral-mini-3b-2507": { + id: "mistral.voxtral-mini-3b-2507", + name: "Voxtral Mini 3B 2507", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.04, + output: 0.04, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"bedrock-converse-stream">, + "mistral.voxtral-small-24b-2507": { + id: "mistral.voxtral-small-24b-2507", + name: "Voxtral Small 24B 2507", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.15, + output: 0.35, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 32000, + maxTokens: 8192, + } satisfies Model<"bedrock-converse-stream">, + "moonshot.kimi-k2-thinking": { + id: "moonshot.kimi-k2-thinking", + name: "Kimi K2 Thinking", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262143, + maxTokens: 16000, + } satisfies Model<"bedrock-converse-stream">, + "moonshotai.kimi-k2.5": { + id: "moonshotai.kimi-k2.5", + name: "Kimi K2.5", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262143, + maxTokens: 16000, + } satisfies Model<"bedrock-converse-stream">, + "nvidia.nemotron-nano-12b-v2": { + id: "nvidia.nemotron-nano-12b-v2", + name: "NVIDIA Nemotron Nano 12B v2 VL BF16", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.2, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"bedrock-converse-stream">, + "nvidia.nemotron-nano-3-30b": { + id: "nvidia.nemotron-nano-3-30b", + name: "NVIDIA Nemotron Nano 3 30B", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.06, + output: 0.24, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"bedrock-converse-stream">, + "nvidia.nemotron-nano-9b-v2": { + id: "nvidia.nemotron-nano-9b-v2", + name: "NVIDIA Nemotron Nano 9B v2", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.06, + output: 0.23, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"bedrock-converse-stream">, + "nvidia.nemotron-super-3-120b": { + id: "nvidia.nemotron-super-3-120b", + name: "NVIDIA Nemotron 3 Super 120B A12B", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.15, + output: 0.65, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"bedrock-converse-stream">, + "openai.gpt-5.4": { + id: "openai.gpt-5.4", + name: "GPT-5.4", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 2.75, + output: 16.5, + cacheRead: 0.275, + cacheWrite: 0, + }, + contextWindow: 272000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "openai.gpt-5.5": { + id: "openai.gpt-5.5", + name: "GPT-5.5", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5.5, + output: 33, + cacheRead: 0.55, + cacheWrite: 0, + }, + contextWindow: 272000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "openai.gpt-oss-120b": { + id: "openai.gpt-oss-120b", + name: "gpt-oss-120b", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "openai.gpt-oss-120b-1:0": { + id: "openai.gpt-oss-120b-1:0", + name: "gpt-oss-120b", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "openai.gpt-oss-20b": { + id: "openai.gpt-oss-20b", + name: "gpt-oss-20b", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.07, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "openai.gpt-oss-20b-1:0": { + id: "openai.gpt-oss-20b-1:0", + name: "gpt-oss-20b", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.07, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "openai.gpt-oss-safeguard-120b": { + id: "openai.gpt-oss-safeguard-120b", + name: "GPT OSS Safeguard 120B", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "openai.gpt-oss-safeguard-20b": { + id: "openai.gpt-oss-safeguard-20b", + name: "GPT OSS Safeguard 20B", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.07, + output: 0.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "qwen.qwen3-235b-a22b-2507-v1:0": { + id: "qwen.qwen3-235b-a22b-2507-v1:0", + name: "Qwen3 235B A22B 2507", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.22, + output: 0.88, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"bedrock-converse-stream">, + "qwen.qwen3-32b-v1:0": { + id: "qwen.qwen3-32b-v1:0", + name: "Qwen3 32B (dense)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 16384, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "qwen.qwen3-coder-30b-a3b-v1:0": { + id: "qwen.qwen3-coder-30b-a3b-v1:0", + name: "Qwen3 Coder 30B A3B Instruct", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"bedrock-converse-stream">, + "qwen.qwen3-coder-480b-a35b-v1:0": { + id: "qwen.qwen3-coder-480b-a35b-v1:0", + name: "Qwen3 Coder 480B A35B Instruct", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.22, + output: 1.8, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 65536, + } satisfies Model<"bedrock-converse-stream">, + "qwen.qwen3-coder-next": { + id: "qwen.qwen3-coder-next", + name: "Qwen3 Coder Next", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.22, + output: 1.8, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 65536, + } satisfies Model<"bedrock-converse-stream">, + "qwen.qwen3-next-80b-a3b": { + id: "qwen.qwen3-next-80b-a3b", + name: "Qwen/Qwen3-Next-80B-A3B-Instruct", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.14, + output: 1.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262000, + maxTokens: 262000, + } satisfies Model<"bedrock-converse-stream">, + "qwen.qwen3-vl-235b-a22b": { + id: "qwen.qwen3-vl-235b-a22b", + name: "Qwen/Qwen3-VL-235B-A22B-Instruct", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.3, + output: 1.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262000, + maxTokens: 262000, + } satisfies Model<"bedrock-converse-stream">, + "us.anthropic.claude-fable-5": { + id: "us.anthropic.claude-fable-5", + name: "Claude Fable 5 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 1, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "us.anthropic.claude-haiku-4-5-20251001-v1:0": { + id: "us.anthropic.claude-haiku-4-5-20251001-v1:0", + name: "Claude Haiku 4.5 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "us.anthropic.claude-opus-4-1-20250805-v1:0": { + id: "us.anthropic.claude-opus-4-1-20250805-v1:0", + name: "Claude Opus 4.1 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"bedrock-converse-stream">, + "us.anthropic.claude-opus-4-5-20251101-v1:0": { + id: "us.anthropic.claude-opus-4-5-20251101-v1:0", + name: "Claude Opus 4.5 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "us.anthropic.claude-opus-4-6-v1": { + id: "us.anthropic.claude-opus-4-6-v1", + name: "Claude Opus 4.6 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "us.anthropic.claude-opus-4-7": { + id: "us.anthropic.claude-opus-4-7", + name: "Claude Opus 4.7 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "us.anthropic.claude-opus-4-8": { + id: "us.anthropic.claude-opus-4-8", + name: "Claude Opus 4.8 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "us.anthropic.claude-sonnet-4-5-20250929-v1:0": { + id: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", + name: "Claude Sonnet 4.5 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "us.anthropic.claude-sonnet-4-6": { + id: "us.anthropic.claude-sonnet-4-6", + name: "Claude Sonnet 4.6 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "us.deepseek.r1-v1:0": { + id: "us.deepseek.r1-v1:0", + name: "DeepSeek-R1 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 1.35, + output: 5.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 32768, + } satisfies Model<"bedrock-converse-stream">, + "us.meta.llama4-maverick-17b-instruct-v1:0": { + id: "us.meta.llama4-maverick-17b-instruct-v1:0", + name: "Llama 4 Maverick 17B Instruct (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.24, + output: 0.97, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "us.meta.llama4-scout-17b-instruct-v1:0": { + id: "us.meta.llama4-scout-17b-instruct-v1:0", + name: "Llama 4 Scout 17B Instruct (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.17, + output: 0.66, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 3500000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "writer.palmyra-x4-v1:0": { + id: "writer.palmyra-x4-v1:0", + name: "Palmyra X4", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 2.5, + output: 10, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 122880, + maxTokens: 8192, + } satisfies Model<"bedrock-converse-stream">, + "writer.palmyra-x5-v1:0": { + id: "writer.palmyra-x5-v1:0", + name: "Palmyra X5", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1040000, + maxTokens: 8192, + } satisfies Model<"bedrock-converse-stream">, + "zai.glm-4.7": { + id: "zai.glm-4.7", + name: "GLM-4.7", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"bedrock-converse-stream">, + "zai.glm-4.7-flash": { + id: "zai.glm-4.7-flash", + name: "GLM-4.7-Flash", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.07, + output: 0.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 131072, + } satisfies Model<"bedrock-converse-stream">, + "zai.glm-5": { + id: "zai.glm-5", + name: "GLM-5", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 101376, + } satisfies Model<"bedrock-converse-stream">, +} as const; diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts new file mode 100644 index 00000000..d839ab6a --- /dev/null +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -0,0 +1,35 @@ +import { bedrockConverseStreamApi } from "../api/bedrock-converse-stream.lazy.ts"; +import type { ApiKeyAuth } from "../auth/types.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { AMAZON_BEDROCK_MODELS } from "./amazon-bedrock.models.ts"; + +/** + * Bedrock auth is ambient: the AWS SDK's default credential chain handles the + * actual signing, so `resolve` only reports whether the provider is + * configured. A stored credential key is surfaced as the bearer token. + */ +const bedrockAuth: ApiKeyAuth = { + name: "AWS credentials", + resolve: async ({ ctx, credential }) => { + if (credential?.key) return { auth: { apiKey: credential.key }, source: "stored credential" }; + if (await ctx.env("AWS_BEARER_TOKEN_BEDROCK")) return { auth: {}, source: "AWS_BEARER_TOKEN_BEDROCK" }; + if (await ctx.env("AWS_PROFILE")) return { auth: {}, source: "AWS_PROFILE" }; + if ((await ctx.env("AWS_ACCESS_KEY_ID")) && (await ctx.env("AWS_SECRET_ACCESS_KEY"))) { + return { auth: {}, source: "AWS access keys" }; + } + if (await ctx.env("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")) return { auth: {}, source: "ECS task role" }; + if (await ctx.env("AWS_CONTAINER_CREDENTIALS_FULL_URI")) return { auth: {}, source: "ECS task role" }; + if (await ctx.env("AWS_WEB_IDENTITY_TOKEN_FILE")) return { auth: {}, source: "web identity token" }; + return undefined; + }, +}; + +export function amazonBedrockProvider(): Provider<"bedrock-converse-stream"> { + return createProvider({ + id: "amazon-bedrock", + name: "Amazon Bedrock", + auth: { apiKey: bedrockAuth }, + models: Object.values(AMAZON_BEDROCK_MODELS), + api: bedrockConverseStreamApi(), + }); +} diff --git a/packages/ai/src/providers/ant-ling.models.ts b/packages/ai/src/providers/ant-ling.models.ts new file mode 100644 index 00000000..6acdebef --- /dev/null +++ b/packages/ai/src/providers/ant-ling.models.ts @@ -0,0 +1,62 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const ANT_LING_MODELS = { + "Ling-2.6-1T": { + id: "Ling-2.6-1T", + name: "Ling 2.6 1T", + api: "openai-completions", + provider: "ant-ling", + baseUrl: "https://api.ant-ling.com/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0.06, + output: 0.25, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "Ling-2.6-flash": { + id: "Ling-2.6-flash", + name: "Ling 2.6 Flash", + api: "openai-completions", + provider: "ant-ling", + baseUrl: "https://api.ant-ling.com/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0.01, + output: 0.02, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "Ring-2.6-1T": { + id: "Ring-2.6-1T", + name: "Ring 2.6 1T", + api: "openai-completions", + provider: "ant-ling", + baseUrl: "https://api.ant-ling.com/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false,"thinkingFormat":"ant-ling"}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh"}, + input: ["text"], + cost: { + input: 0.06, + output: 0.25, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/ant-ling.ts b/packages/ai/src/providers/ant-ling.ts new file mode 100644 index 00000000..03bb314d --- /dev/null +++ b/packages/ai/src/providers/ant-ling.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { ANT_LING_MODELS } from "./ant-ling.models.ts"; + +export function antLingProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "ant-ling", + name: "Ant Ling", + baseUrl: "https://api.ant-ling.com/v1", + auth: { apiKey: envApiKeyAuth("Ant Ling API key", ["ANT_LING_API_KEY"]) }, + models: Object.values(ANT_LING_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/anthropic.models.ts b/packages/ai/src/providers/anthropic.models.ts new file mode 100644 index 00000000..3db30f74 --- /dev/null +++ b/packages/ai/src/providers/anthropic.models.ts @@ -0,0 +1,441 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const ANTHROPIC_MODELS = { + "claude-3-5-haiku-20241022": { + id: "claude-3-5-haiku-20241022", + name: "Claude Haiku 3.5", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.8, + output: 4, + cacheRead: 0.08, + cacheWrite: 1, + }, + contextWindow: 200000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "claude-3-5-haiku-latest": { + id: "claude-3-5-haiku-latest", + name: "Claude Haiku 3.5 (latest)", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.8, + output: 4, + cacheRead: 0.08, + cacheWrite: 1, + }, + contextWindow: 200000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "claude-3-5-sonnet-20240620": { + id: "claude-3-5-sonnet-20240620", + name: "Claude Sonnet 3.5", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "claude-3-5-sonnet-20241022": { + id: "claude-3-5-sonnet-20241022", + name: "Claude Sonnet 3.5 v2", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "claude-3-7-sonnet-20250219": { + id: "claude-3-7-sonnet-20250219", + name: "Claude Sonnet 3.7", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-3-haiku-20240307": { + id: "claude-3-haiku-20240307", + name: "Claude Haiku 3", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.25, + cacheRead: 0.03, + cacheWrite: 0.3, + }, + contextWindow: 200000, + maxTokens: 4096, + } satisfies Model<"anthropic-messages">, + "claude-3-opus-20240229": { + id: "claude-3-opus-20240229", + name: "Claude Opus 3", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 4096, + } satisfies Model<"anthropic-messages">, + "claude-3-sonnet-20240229": { + id: "claude-3-sonnet-20240229", + name: "Claude Sonnet 3", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 0.3, + }, + contextWindow: 200000, + maxTokens: 4096, + } satisfies Model<"anthropic-messages">, + "claude-fable-5": { + id: "claude-fable-5", + name: "Claude Fable 5", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + compat: {"forceAdaptiveThinking":true}, + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 1, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-haiku-4-5": { + id: "claude-haiku-4-5", + name: "Claude Haiku 4.5 (latest)", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-haiku-4-5-20251001": { + id: "claude-haiku-4-5-20251001", + name: "Claude Haiku 4.5", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-0": { + id: "claude-opus-4-0", + name: "Claude Opus 4 (latest)", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-1": { + id: "claude-opus-4-1", + name: "Claude Opus 4.1 (latest)", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-1-20250805": { + id: "claude-opus-4-1-20250805", + name: "Claude Opus 4.1", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-20250514": { + id: "claude-opus-4-20250514", + name: "Claude Opus 4", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-5": { + id: "claude-opus-4-5", + name: "Claude Opus 4.5 (latest)", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-5-20251101": { + id: "claude-opus-4-5-20251101", + name: "Claude Opus 4.5", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-6": { + id: "claude-opus-4-6", + name: "Claude Opus 4.6", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + compat: {"forceAdaptiveThinking":true}, + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-7": { + id: "claude-opus-4-7", + name: "Claude Opus 4.7", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-8": { + id: "claude-opus-4-8", + name: "Claude Opus 4.8", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4-0": { + id: "claude-sonnet-4-0", + name: "Claude Sonnet 4 (latest)", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4-20250514": { + id: "claude-sonnet-4-20250514", + name: "Claude Sonnet 4", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4-5": { + id: "claude-sonnet-4-5", + name: "Claude Sonnet 4.5 (latest)", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4-5-20250929": { + id: "claude-sonnet-4-5-20250929", + name: "Claude Sonnet 4.5", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4-6": { + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + compat: {"forceAdaptiveThinking":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, +} as const; diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts new file mode 100644 index 00000000..6570fc38 --- /dev/null +++ b/packages/ai/src/providers/anthropic.ts @@ -0,0 +1,20 @@ +import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts"; +import { envApiKeyAuth, lazyOAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { loadAnthropicOAuth } from "../utils/oauth/load.ts"; +import { ANTHROPIC_MODELS } from "./anthropic.models.ts"; + +export function anthropicProvider(): Provider<"anthropic-messages"> { + return createProvider({ + id: "anthropic", + name: "Anthropic", + baseUrl: "https://api.anthropic.com", + auth: { + // ANTHROPIC_OAUTH_TOKEN takes precedence over ANTHROPIC_API_KEY + apiKey: envApiKeyAuth("Anthropic API key", ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"]), + oauth: lazyOAuth({ name: "Anthropic (Claude Pro/Max)", load: loadAnthropicOAuth }), + }, + models: Object.values(ANTHROPIC_MODELS), + api: anthropicMessagesApi(), + }); +} diff --git a/packages/ai/src/providers/azure-openai-responses.models.ts b/packages/ai/src/providers/azure-openai-responses.models.ts new file mode 100644 index 00000000..b35eec5c --- /dev/null +++ b/packages/ai/src/providers/azure-openai-responses.models.ts @@ -0,0 +1,745 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const AZURE_OPENAI_RESPONSES_MODELS = { + "gpt-4": { + id: "gpt-4", + name: "GPT-4", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + input: ["text"], + cost: { + input: 30, + output: 60, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 8192, + maxTokens: 8192, + } satisfies Model<"azure-openai-responses">, + "gpt-4-turbo": { + id: "gpt-4-turbo", + name: "GPT-4 Turbo", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + input: ["text", "image"], + cost: { + input: 10, + output: 30, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"azure-openai-responses">, + "gpt-4.1": { + id: "gpt-4.1", + name: "GPT-4.1", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 32768, + } satisfies Model<"azure-openai-responses">, + "gpt-4.1-mini": { + id: "gpt-4.1-mini", + name: "GPT-4.1 mini", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.4, + output: 1.6, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 32768, + } satisfies Model<"azure-openai-responses">, + "gpt-4.1-nano": { + id: "gpt-4.1-nano", + name: "GPT-4.1 nano", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.4, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 32768, + } satisfies Model<"azure-openai-responses">, + "gpt-4o": { + id: "gpt-4o", + name: "GPT-4o", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 10, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"azure-openai-responses">, + "gpt-4o-2024-05-13": { + id: "gpt-4o-2024-05-13", + name: "GPT-4o (2024-05-13)", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + input: ["text", "image"], + cost: { + input: 5, + output: 15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"azure-openai-responses">, + "gpt-4o-2024-08-06": { + id: "gpt-4o-2024-08-06", + name: "GPT-4o (2024-08-06)", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 10, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"azure-openai-responses">, + "gpt-4o-2024-11-20": { + id: "gpt-4o-2024-11-20", + name: "GPT-4o (2024-11-20)", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 10, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"azure-openai-responses">, + "gpt-4o-mini": { + id: "gpt-4o-mini", + name: "GPT-4o mini", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"azure-openai-responses">, + "gpt-5": { + id: "gpt-5", + name: "GPT-5", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5-chat-latest": { + id: "gpt-5-chat-latest", + name: "GPT-5 Chat Latest", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"azure-openai-responses">, + "gpt-5-codex": { + id: "gpt-5-codex", + name: "GPT-5-Codex", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5-mini": { + id: "gpt-5-mini", + name: "GPT-5 Mini", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5-nano": { + id: "gpt-5-nano", + name: "GPT-5 Nano", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.05, + output: 0.4, + cacheRead: 0.005, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5-pro": { + id: "gpt-5-pro", + name: "GPT-5 Pro", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 15, + output: 120, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.1": { + id: "gpt-5.1", + name: "GPT-5.1", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.1-chat-latest": { + id: "gpt-5.1-chat-latest", + name: "GPT-5.1 Chat", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"azure-openai-responses">, + "gpt-5.1-codex": { + id: "gpt-5.1-codex", + name: "GPT-5.1 Codex", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.1-codex-max": { + id: "gpt-5.1-codex-max", + name: "GPT-5.1 Codex Max", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.1-codex-mini": { + id: "gpt-5.1-codex-mini", + name: "GPT-5.1 Codex mini", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.2": { + id: "gpt-5.2", + name: "GPT-5.2", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.2-chat-latest": { + id: "gpt-5.2-chat-latest", + name: "GPT-5.2 Chat", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"azure-openai-responses">, + "gpt-5.2-codex": { + id: "gpt-5.2-codex", + name: "GPT-5.2 Codex", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.2-pro": { + id: "gpt-5.2-pro", + name: "GPT-5.2 Pro", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 21, + output: 168, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.3-chat-latest": { + id: "gpt-5.3-chat-latest", + name: "GPT-5.3 Chat (latest)", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"azure-openai-responses">, + "gpt-5.3-codex": { + id: "gpt-5.3-codex", + name: "GPT-5.3 Codex", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.3-codex-spark": { + id: "gpt-5.3-codex-spark", + name: "GPT-5.3 Codex Spark", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 32000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.4": { + id: "gpt-5.4", + name: "GPT-5.4", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 2.5, + output: 15, + cacheRead: 0.25, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.4-mini": { + id: "gpt-5.4-mini", + name: "GPT-5.4 mini", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.75, + output: 4.5, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.4-nano": { + id: "gpt-5.4-nano", + name: "GPT-5.4 nano", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.2, + output: 1.25, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.4-pro": { + id: "gpt-5.4-pro", + name: "GPT-5.4 Pro", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 30, + output: 180, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.5": { + id: "gpt-5.5", + name: "GPT-5.5", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.5-pro": { + id: "gpt-5.5-pro", + name: "GPT-5.5 Pro", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh","minimal":null,"low":null}, + input: ["text", "image"], + cost: { + input: 30, + output: 180, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "o1": { + id: "o1", + name: "o1", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 60, + cacheRead: 7.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"azure-openai-responses">, + "o1-pro": { + id: "o1-pro", + name: "o1-pro", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + input: ["text", "image"], + cost: { + input: 150, + output: 600, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"azure-openai-responses">, + "o3": { + id: "o3", + name: "o3", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"azure-openai-responses">, + "o3-deep-research": { + id: "o3-deep-research", + name: "o3-deep-research", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + input: ["text", "image"], + cost: { + input: 10, + output: 40, + cacheRead: 2.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"azure-openai-responses">, + "o3-mini": { + id: "o3-mini", + name: "o3-mini", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + input: ["text"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.55, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"azure-openai-responses">, + "o3-pro": { + id: "o3-pro", + name: "o3-pro", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + input: ["text", "image"], + cost: { + input: 20, + output: 80, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"azure-openai-responses">, + "o4-mini": { + id: "o4-mini", + name: "o4-mini", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.275, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"azure-openai-responses">, + "o4-mini-deep-research": { + id: "o4-mini-deep-research", + name: "o4-mini-deep-research", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"azure-openai-responses">, +} as const; diff --git a/packages/ai/src/providers/azure-openai-responses.ts b/packages/ai/src/providers/azure-openai-responses.ts new file mode 100644 index 00000000..78351dea --- /dev/null +++ b/packages/ai/src/providers/azure-openai-responses.ts @@ -0,0 +1,14 @@ +import { azureOpenAIResponsesApi } from "../api/azure-openai-responses.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { AZURE_OPENAI_RESPONSES_MODELS } from "./azure-openai-responses.models.ts"; + +export function azureOpenAIResponsesProvider(): Provider<"azure-openai-responses"> { + return createProvider({ + id: "azure-openai-responses", + name: "Azure OpenAI", + auth: { apiKey: envApiKeyAuth("Azure OpenAI API key", ["AZURE_OPENAI_API_KEY"]) }, + models: Object.values(AZURE_OPENAI_RESPONSES_MODELS), + api: azureOpenAIResponsesApi(), + }); +} diff --git a/packages/ai/src/providers/cerebras.models.ts b/packages/ai/src/providers/cerebras.models.ts new file mode 100644 index 00000000..f074d712 --- /dev/null +++ b/packages/ai/src/providers/cerebras.models.ts @@ -0,0 +1,58 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const CEREBRAS_MODELS = { + "gpt-oss-120b": { + id: "gpt-oss-120b", + name: "GPT OSS 120B", + api: "openai-completions", + provider: "cerebras", + baseUrl: "https://api.cerebras.ai/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.25, + output: 0.69, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "llama3.1-8b": { + id: "llama3.1-8b", + name: "Llama 3.1 8B", + api: "openai-completions", + provider: "cerebras", + baseUrl: "https://api.cerebras.ai/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.1, + output: 0.1, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 32000, + maxTokens: 8000, + } satisfies Model<"openai-completions">, + "zai-glm-4.7": { + id: "zai-glm-4.7", + name: "Z.AI GLM-4.7", + api: "openai-completions", + provider: "cerebras", + baseUrl: "https://api.cerebras.ai/v1", + reasoning: false, + input: ["text"], + cost: { + input: 2.25, + output: 2.75, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 40000, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/cerebras.ts b/packages/ai/src/providers/cerebras.ts new file mode 100644 index 00000000..9ffc7375 --- /dev/null +++ b/packages/ai/src/providers/cerebras.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { CEREBRAS_MODELS } from "./cerebras.models.ts"; + +export function cerebrasProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "cerebras", + name: "Cerebras", + baseUrl: "https://api.cerebras.ai/v1", + auth: { apiKey: envApiKeyAuth("Cerebras API key", ["CEREBRAS_API_KEY"]) }, + models: Object.values(CEREBRAS_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/cloudflare-ai-gateway.models.ts b/packages/ai/src/providers/cloudflare-ai-gateway.models.ts new file mode 100644 index 00000000..c54752be --- /dev/null +++ b/packages/ai/src/providers/cloudflare-ai-gateway.models.ts @@ -0,0 +1,656 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const CLOUDFLARE_AI_GATEWAY_MODELS = { + "claude-3-5-haiku": { + id: "claude-3-5-haiku", + name: "Claude Haiku 3.5 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.8, + output: 4, + cacheRead: 0.08, + cacheWrite: 1, + }, + contextWindow: 200000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "claude-3-haiku": { + id: "claude-3-haiku", + name: "Claude Haiku 3", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.25, + cacheRead: 0.03, + cacheWrite: 0.3, + }, + contextWindow: 200000, + maxTokens: 4096, + } satisfies Model<"anthropic-messages">, + "claude-3-opus": { + id: "claude-3-opus", + name: "Claude Opus 3", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: false, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 4096, + } satisfies Model<"anthropic-messages">, + "claude-3-sonnet": { + id: "claude-3-sonnet", + name: "Claude Sonnet 3", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: false, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 0.3, + }, + contextWindow: 200000, + maxTokens: 4096, + } satisfies Model<"anthropic-messages">, + "claude-3.5-haiku": { + id: "claude-3.5-haiku", + name: "Claude Haiku 3.5 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.8, + output: 4, + cacheRead: 0.08, + cacheWrite: 1, + }, + contextWindow: 200000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "claude-3.5-sonnet": { + id: "claude-3.5-sonnet", + name: "Claude Sonnet 3.5 v2", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: false, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "claude-fable-5": { + id: "claude-fable-5", + name: "Claude Fable 5", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"forceAdaptiveThinking":true}, + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 1, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-haiku-4-5": { + id: "claude-haiku-4-5", + name: "Claude Haiku 4.5 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4": { + id: "claude-opus-4", + name: "Claude Opus 4 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-1": { + id: "claude-opus-4-1", + name: "Claude Opus 4.1 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-5": { + id: "claude-opus-4-5", + name: "Claude Opus 4.5 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-6": { + id: "claude-opus-4-6", + name: "Claude Opus 4.6 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"forceAdaptiveThinking":true}, + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-7": { + id: "claude-opus-4-7", + name: "Claude Opus 4.7", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-8": { + id: "claude-opus-4-8", + name: "Claude Opus 4.8", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4": { + id: "claude-sonnet-4", + name: "Claude Sonnet 4 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4-5": { + id: "claude-sonnet-4-5", + name: "Claude Sonnet 4.5 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4-6": { + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"forceAdaptiveThinking":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "gpt-4": { + id: "gpt-4", + name: "GPT-4", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: false, + input: ["text"], + cost: { + input: 30, + output: 60, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 8192, + maxTokens: 8192, + } satisfies Model<"openai-responses">, + "gpt-4-turbo": { + id: "gpt-4-turbo", + name: "GPT-4 Turbo", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 10, + output: 30, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-responses">, + "gpt-4o": { + id: "gpt-4o", + name: "GPT-4o", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 10, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-responses">, + "gpt-4o-mini": { + id: "gpt-4o-mini", + name: "GPT-4o mini", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-responses">, + "gpt-5.1": { + id: "gpt-5.1", + name: "GPT-5.1", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.13, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.1-codex": { + id: "gpt-5.1-codex", + name: "GPT-5.1 Codex", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.2": { + id: "gpt-5.2", + name: "GPT-5.2", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.2-codex": { + id: "gpt-5.2-codex", + name: "GPT-5.2 Codex", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.3-codex": { + id: "gpt-5.3-codex", + name: "GPT-5.3 Codex", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.4": { + id: "gpt-5.4", + name: "GPT-5.4", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 2.5, + output: 15, + cacheRead: 0.25, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.5": { + id: "gpt-5.5", + name: "GPT-5.5", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "o1": { + id: "o1", + name: "o1", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 60, + cacheRead: 7.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o3": { + id: "o3", + name: "o3", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o3-mini": { + id: "o3-mini", + name: "o3-mini", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + input: ["text"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.55, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o3-pro": { + id: "o3-pro", + name: "o3-pro", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 20, + output: 80, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o4-mini": { + id: "o4-mini", + name: "o4-mini", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.28, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "workers-ai/@cf/moonshotai/kimi-k2.5": { + id: "workers-ai/@cf/moonshotai/kimi-k2.5", + name: "Kimi K2.5", + api: "openai-completions", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 3, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"openai-completions">, + "workers-ai/@cf/moonshotai/kimi-k2.6": { + id: "workers-ai/@cf/moonshotai/kimi-k2.6", + name: "Kimi K2.6", + api: "openai-completions", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.16, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"openai-completions">, + "workers-ai/@cf/nvidia/nemotron-3-120b-a12b": { + id: "workers-ai/@cf/nvidia/nemotron-3-120b-a12b", + name: "Nemotron 3 Super 120B", + api: "openai-completions", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0.5, + output: 1.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"openai-completions">, + "workers-ai/@cf/zai-org/glm-4.7-flash": { + id: "workers-ai/@cf/zai-org/glm-4.7-flash", + name: "GLM-4.7-Flash", + api: "openai-completions", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0.06, + output: 0.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/cloudflare-ai-gateway.ts b/packages/ai/src/providers/cloudflare-ai-gateway.ts new file mode 100644 index 00000000..e8538f7c --- /dev/null +++ b/packages/ai/src/providers/cloudflare-ai-gateway.ts @@ -0,0 +1,22 @@ +import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts"; +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { openAIResponsesApi } from "../api/openai-responses.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { CLOUDFLARE_AI_GATEWAY_MODELS } from "./cloudflare-ai-gateway.models.ts"; + +export function cloudflareAIGatewayProvider(): Provider< + "anthropic-messages" | "openai-completions" | "openai-responses" +> { + return createProvider({ + id: "cloudflare-ai-gateway", + name: "Cloudflare AI Gateway", + auth: { apiKey: envApiKeyAuth("Cloudflare API key", ["CLOUDFLARE_API_KEY"]) }, + models: Object.values(CLOUDFLARE_AI_GATEWAY_MODELS), + api: { + "anthropic-messages": anthropicMessagesApi(), + "openai-completions": openAICompletionsApi(), + "openai-responses": openAIResponsesApi(), + }, + }); +} diff --git a/packages/ai/src/providers/cloudflare-workers-ai.models.ts b/packages/ai/src/providers/cloudflare-workers-ai.models.ts new file mode 100644 index 00000000..76cf435d --- /dev/null +++ b/packages/ai/src/providers/cloudflare-workers-ai.models.ts @@ -0,0 +1,205 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const CLOUDFLARE_WORKERS_AI_MODELS = { + "@cf/google/gemma-4-26b-a4b-it": { + id: "@cf/google/gemma-4-26b-a4b-it", + name: "Gemma 4 26B A4B IT", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "@cf/ibm-granite/granite-4.0-h-micro": { + id: "@cf/ibm-granite/granite-4.0-h-micro", + name: "Granite 4.0 H Micro", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: false, + input: ["text"], + cost: { + input: 0.017, + output: 0.112, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131000, + maxTokens: 131000, + } satisfies Model<"openai-completions">, + "@cf/meta/llama-3.3-70b-instruct-fp8-fast": { + id: "@cf/meta/llama-3.3-70b-instruct-fp8-fast", + name: "Llama 3.3 70B Instruct fp8 Fast", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: false, + input: ["text"], + cost: { + input: 0.293, + output: 2.253, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 24000, + maxTokens: 24000, + } satisfies Model<"openai-completions">, + "@cf/meta/llama-4-scout-17b-16e-instruct": { + id: "@cf/meta/llama-4-scout-17b-16e-instruct", + name: "Llama 4 Scout 17B 16E Instruct", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.27, + output: 0.85, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "@cf/mistralai/mistral-small-3.1-24b-instruct": { + id: "@cf/mistralai/mistral-small-3.1-24b-instruct", + name: "Mistral Small 3.1 24B Instruct", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: false, + input: ["text"], + cost: { + input: 0.351, + output: 0.555, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "@cf/moonshotai/kimi-k2.6": { + id: "@cf/moonshotai/kimi-k2.6", + name: "Kimi K2.6", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.16, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 256000, + } satisfies Model<"openai-completions">, + "@cf/nvidia/nemotron-3-120b-a12b": { + id: "@cf/nvidia/nemotron-3-120b-a12b", + name: "Nemotron 3 Super 120B", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0.5, + output: 1.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"openai-completions">, + "@cf/openai/gpt-oss-120b": { + id: "@cf/openai/gpt-oss-120b", + name: "GPT OSS 120B", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0.35, + output: 0.75, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "@cf/openai/gpt-oss-20b": { + id: "@cf/openai/gpt-oss-20b", + name: "GPT OSS 20B", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0.2, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "@cf/qwen/qwen3-30b-a3b-fp8": { + id: "@cf/qwen/qwen3-30b-a3b-fp8", + name: "Qwen3 30B A3b fp8", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0.0509, + output: 0.335, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 32768, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "@cf/zai-org/glm-4.7-flash": { + id: "@cf/zai-org/glm-4.7-flash", + name: "GLM-4.7-Flash", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0.0605, + output: 0.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/cloudflare-workers-ai.ts b/packages/ai/src/providers/cloudflare-workers-ai.ts new file mode 100644 index 00000000..81d18e34 --- /dev/null +++ b/packages/ai/src/providers/cloudflare-workers-ai.ts @@ -0,0 +1,14 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { CLOUDFLARE_WORKERS_AI_MODELS } from "./cloudflare-workers-ai.models.ts"; + +export function cloudflareWorkersAIProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "cloudflare-workers-ai", + name: "Cloudflare Workers AI", + auth: { apiKey: envApiKeyAuth("Cloudflare API key", ["CLOUDFLARE_API_KEY"]) }, + models: Object.values(CLOUDFLARE_WORKERS_AI_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/deepseek.models.ts b/packages/ai/src/providers/deepseek.models.ts new file mode 100644 index 00000000..7d41a0a0 --- /dev/null +++ b/packages/ai/src/providers/deepseek.models.ts @@ -0,0 +1,45 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const DEEPSEEK_MODELS = { + "deepseek-v4-flash": { + id: "deepseek-v4-flash", + name: "DeepSeek V4 Flash", + api: "openai-completions", + provider: "deepseek", + baseUrl: "https://api.deepseek.com", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 0.14, + output: 0.28, + cacheRead: 0.0028, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"openai-completions">, + "deepseek-v4-pro": { + id: "deepseek-v4-pro", + name: "DeepSeek V4 Pro", + api: "openai-completions", + provider: "deepseek", + baseUrl: "https://api.deepseek.com", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 0.435, + output: 0.87, + cacheRead: 0.003625, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/deepseek.ts b/packages/ai/src/providers/deepseek.ts new file mode 100644 index 00000000..580e25c2 --- /dev/null +++ b/packages/ai/src/providers/deepseek.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { DEEPSEEK_MODELS } from "./deepseek.models.ts"; + +export function deepseekProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "deepseek", + name: "DeepSeek", + baseUrl: "https://api.deepseek.com", + auth: { apiKey: envApiKeyAuth("DeepSeek API key", ["DEEPSEEK_API_KEY"]) }, + models: Object.values(DEEPSEEK_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/faux.ts b/packages/ai/src/providers/faux.ts index 7e629847..6fbdbc54 100644 --- a/packages/ai/src/providers/faux.ts +++ b/packages/ai/src/providers/faux.ts @@ -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, ...Model[]]; + getModel(): Model; + getModel(modelId: string): Model | 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 = (streamModel, context, streamOptions) => stream(streamModel, context, streamOptions); - registerApiProvider({ api, stream, streamSimple }, sourceId); - function getModel(): Model; function getModel(requestedModelId: string): Model | undefined; function getModel(requestedModelId?: string): Model | 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, + }; +} diff --git a/packages/ai/src/providers/fireworks.models.ts b/packages/ai/src/providers/fireworks.models.ts new file mode 100644 index 00000000..c515b811 --- /dev/null +++ b/packages/ai/src/providers/fireworks.models.ts @@ -0,0 +1,241 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const FIREWORKS_MODELS = { + "accounts/fireworks/models/deepseek-v4-flash": { + id: "accounts/fireworks/models/deepseek-v4-flash", + name: "DeepSeek V4 Flash", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.14, + output: 0.28, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/deepseek-v4-pro": { + id: "accounts/fireworks/models/deepseek-v4-pro", + name: "DeepSeek V4 Pro", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 1.74, + output: 3.48, + cacheRead: 0.145, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/glm-5p1": { + id: "accounts/fireworks/models/glm-5p1", + name: "GLM 5.1", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0.26, + cacheWrite: 0, + }, + contextWindow: 202800, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/gpt-oss-120b": { + id: "accounts/fireworks/models/gpt-oss-120b", + name: "GPT OSS 120B", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.015, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/gpt-oss-20b": { + id: "accounts/fireworks/models/gpt-oss-20b", + name: "GPT OSS 20B", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.07, + output: 0.3, + cacheRead: 0.035, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/kimi-k2p5": { + id: "accounts/fireworks/models/kimi-k2p5", + name: "Kimi K2.5", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 3, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/kimi-k2p6": { + id: "accounts/fireworks/models/kimi-k2p6", + name: "Kimi K2.6", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.16, + cacheWrite: 0, + }, + contextWindow: 262000, + maxTokens: 262000, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/minimax-m2p5": { + id: "accounts/fireworks/models/minimax-m2p5", + name: "MiniMax-M2.5", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 196608, + maxTokens: 196608, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/minimax-m2p7": { + id: "accounts/fireworks/models/minimax-m2p7", + name: "MiniMax-M2.7", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 196608, + maxTokens: 196608, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/qwen3p6-plus": { + id: "accounts/fireworks/models/qwen3p6-plus", + name: "Qwen 3.6 Plus", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/routers/glm-5p1-fast": { + id: "accounts/fireworks/routers/glm-5p1-fast", + name: "GLM 5.1 Fast", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 2.8, + output: 8.8, + cacheRead: 0.52, + cacheWrite: 0, + }, + contextWindow: 202800, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/routers/kimi-k2p6-fast": { + id: "accounts/fireworks/routers/kimi-k2p6-fast", + name: "Kimi K2.6 Fast", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.3, + cacheWrite: 0, + }, + contextWindow: 262000, + maxTokens: 262000, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/routers/kimi-k2p6-turbo": { + id: "accounts/fireworks/routers/kimi-k2p6-turbo", + name: "Kimi K2.6 Turbo", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.3, + cacheWrite: 0, + }, + contextWindow: 262000, + maxTokens: 262000, + } satisfies Model<"anthropic-messages">, +} as const; diff --git a/packages/ai/src/providers/fireworks.ts b/packages/ai/src/providers/fireworks.ts new file mode 100644 index 00000000..9c0b5091 --- /dev/null +++ b/packages/ai/src/providers/fireworks.ts @@ -0,0 +1,15 @@ +import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { FIREWORKS_MODELS } from "./fireworks.models.ts"; + +export function fireworksProvider(): Provider<"anthropic-messages"> { + return createProvider({ + id: "fireworks", + name: "Fireworks", + baseUrl: "https://api.fireworks.ai/inference", + auth: { apiKey: envApiKeyAuth("Fireworks API key", ["FIREWORKS_API_KEY"]) }, + models: Object.values(FIREWORKS_MODELS), + api: anthropicMessagesApi(), + }); +} diff --git a/packages/ai/src/providers/github-copilot.models.ts b/packages/ai/src/providers/github-copilot.models.ts new file mode 100644 index 00000000..471a0b95 --- /dev/null +++ b/packages/ai/src/providers/github-copilot.models.ts @@ -0,0 +1,427 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const GITHUB_COPILOT_MODELS = { + "claude-haiku-4.5": { + id: "claude-haiku-4.5", + name: "Claude Haiku 4.5 (latest)", + api: "anthropic-messages", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"supportsEagerToolInputStreaming":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4.5": { + id: "claude-opus-4.5", + name: "Claude Opus 4.5 (latest)", + api: "anthropic-messages", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4.6": { + id: "claude-opus-4.6", + name: "Claude Opus 4.6", + api: "anthropic-messages", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"forceAdaptiveThinking":true}, + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4.7": { + id: "claude-opus-4.7", + name: "Claude Opus 4.7", + api: "anthropic-messages", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4.8": { + id: "claude-opus-4.8", + name: "Claude Opus 4.8", + api: "anthropic-messages", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4": { + id: "claude-sonnet-4", + name: "Claude Sonnet 4 (latest)", + api: "anthropic-messages", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"supportsEagerToolInputStreaming":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 216000, + maxTokens: 16000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4.5": { + id: "claude-sonnet-4.5", + name: "Claude Sonnet 4.5 (latest)", + api: "anthropic-messages", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"supportsEagerToolInputStreaming":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4.6": { + id: "claude-sonnet-4.6", + name: "Claude Sonnet 4.6", + api: "anthropic-messages", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"forceAdaptiveThinking":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "gemini-2.5-pro": { + id: "gemini-2.5-pro", + name: "Gemini 2.5 Pro", + api: "openai-completions", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "gemini-3-flash-preview": { + id: "gemini-3-flash-preview", + name: "Gemini 3 Flash Preview", + api: "openai-completions", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "gemini-3.1-pro-preview": { + id: "gemini-3.1-pro-preview", + name: "Gemini 3.1 Pro Preview", + api: "openai-completions", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "gemini-3.5-flash": { + id: "gemini-3.5-flash", + name: "Gemini 3.5 Flash", + api: "openai-completions", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.5, + output: 9, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "gpt-4.1": { + id: "gpt-4.1", + name: "GPT-4.1", + api: "openai-completions", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "gpt-5-mini": { + id: "gpt-5-mini", + name: "GPT-5 Mini", + api: "openai-responses", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"low"}, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 264000, + maxTokens: 64000, + } satisfies Model<"openai-responses">, + "gpt-5.2": { + id: "gpt-5.2", + name: "GPT-5.2", + api: "openai-responses", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.2-codex": { + id: "gpt-5.2-codex", + name: "GPT-5.2 Codex", + api: "openai-responses", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.3-codex": { + id: "gpt-5.3-codex", + name: "GPT-5.3 Codex", + api: "openai-responses", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.4": { + id: "gpt-5.4", + name: "GPT-5.4", + api: "openai-responses", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 2.5, + output: 15, + cacheRead: 0.25, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.4-mini": { + id: "gpt-5.4-mini", + name: "GPT-5.4 mini", + api: "openai-responses", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.75, + output: 4.5, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.4-nano": { + id: "gpt-5.4-nano", + name: "GPT-5.4 nano", + api: "openai-responses", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.2, + output: 1.25, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.5": { + id: "gpt-5.5", + name: "GPT-5.5", + api: "openai-responses", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "raptor-mini": { + id: "raptor-mini", + name: "Raptor mini", + api: "openai-completions", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/github-copilot.ts b/packages/ai/src/providers/github-copilot.ts new file mode 100644 index 00000000..c935ad5d --- /dev/null +++ b/packages/ai/src/providers/github-copilot.ts @@ -0,0 +1,25 @@ +import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts"; +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { openAIResponsesApi } from "../api/openai-responses.lazy.ts"; +import { envApiKeyAuth, lazyOAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { loadGitHubCopilotOAuth } from "../utils/oauth/load.ts"; +import { GITHUB_COPILOT_MODELS } from "./github-copilot.models.ts"; + +export function githubCopilotProvider(): Provider<"anthropic-messages" | "openai-completions" | "openai-responses"> { + return createProvider({ + id: "github-copilot", + name: "GitHub Copilot", + baseUrl: "https://api.individual.githubcopilot.com", + auth: { + apiKey: envApiKeyAuth("GitHub Copilot token", ["COPILOT_GITHUB_TOKEN"]), + oauth: lazyOAuth({ name: "GitHub Copilot", load: loadGitHubCopilotOAuth }), + }, + models: Object.values(GITHUB_COPILOT_MODELS), + api: { + "anthropic-messages": anthropicMessagesApi(), + "openai-completions": openAICompletionsApi(), + "openai-responses": openAIResponsesApi(), + }, + }); +} diff --git a/packages/ai/src/providers/google-vertex.models.ts b/packages/ai/src/providers/google-vertex.models.ts new file mode 100644 index 00000000..8e72a237 --- /dev/null +++ b/packages/ai/src/providers/google-vertex.models.ts @@ -0,0 +1,232 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const GOOGLE_VERTEX_MODELS = { + "gemini-1.5-flash": { + id: "gemini-1.5-flash", + name: "Gemini 1.5 Flash (Vertex)", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.075, + output: 0.3, + cacheRead: 0.01875, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 8192, + } satisfies Model<"google-vertex">, + "gemini-1.5-flash-8b": { + id: "gemini-1.5-flash-8b", + name: "Gemini 1.5 Flash-8B (Vertex)", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.0375, + output: 0.15, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 8192, + } satisfies Model<"google-vertex">, + "gemini-1.5-pro": { + id: "gemini-1.5-pro", + name: "Gemini 1.5 Pro (Vertex)", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 1.25, + output: 5, + cacheRead: 0.3125, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 8192, + } satisfies Model<"google-vertex">, + "gemini-2.0-flash": { + id: "gemini-2.0-flash", + name: "Gemini 2.0 Flash (Vertex)", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.0375, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 8192, + } satisfies Model<"google-vertex">, + "gemini-2.0-flash-lite": { + id: "gemini-2.0-flash-lite", + name: "Gemini 2.0 Flash Lite (Vertex)", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.075, + output: 0.3, + cacheRead: 0.01875, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-vertex">, + "gemini-2.5-flash": { + id: "gemini-2.5-flash", + name: "Gemini 2.5 Flash (Vertex)", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 2.5, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-vertex">, + "gemini-2.5-flash-lite": { + id: "gemini-2.5-flash-lite", + name: "Gemini 2.5 Flash Lite (Vertex)", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.4, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-vertex">, + "gemini-2.5-flash-lite-preview-09-2025": { + id: "gemini-2.5-flash-lite-preview-09-2025", + name: "Gemini 2.5 Flash Lite Preview 09-25 (Vertex)", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.4, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-vertex">, + "gemini-2.5-pro": { + id: "gemini-2.5-pro", + name: "Gemini 2.5 Pro (Vertex)", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-vertex">, + "gemini-3-flash-preview": { + id: "gemini-3-flash-preview", + name: "Gemini 3 Flash Preview (Vertex)", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-vertex">, + "gemini-3-pro-preview": { + id: "gemini-3-pro-preview", + name: "Gemini 3 Pro Preview (Vertex)", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"google-vertex">, + "gemini-3.1-pro-preview": { + id: "gemini-3.1-pro-preview", + name: "Gemini 3.1 Pro Preview (Vertex)", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-vertex">, + "gemini-3.1-pro-preview-customtools": { + id: "gemini-3.1-pro-preview-customtools", + name: "Gemini 3.1 Pro Preview Custom Tools (Vertex)", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-vertex">, +} as const; diff --git a/packages/ai/src/providers/google-vertex.ts b/packages/ai/src/providers/google-vertex.ts new file mode 100644 index 00000000..af84fc70 --- /dev/null +++ b/packages/ai/src/providers/google-vertex.ts @@ -0,0 +1,38 @@ +import { googleVertexApi } from "../api/google-vertex.lazy.ts"; +import type { ApiKeyAuth } from "../auth/types.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { GOOGLE_VERTEX_MODELS } from "./google-vertex.models.ts"; + +const VERTEX_ADC_PATH = "~/.config/gcloud/application_default_credentials.json"; + +/** + * Vertex accepts an explicit API key or Application Default Credentials + * (`gcloud auth application-default login`). ADC additionally requires + * project and location env vars, which the implementation reads itself. + */ +const vertexAuth: ApiKeyAuth = { + name: "Google Cloud credentials", + resolve: async ({ ctx, credential }) => { + const key = credential?.key ?? (await ctx.env("GOOGLE_CLOUD_API_KEY")); + if (key) return { auth: { apiKey: key }, source: credential?.key ? "stored credential" : "GOOGLE_CLOUD_API_KEY" }; + + const adcPath = await ctx.env("GOOGLE_APPLICATION_CREDENTIALS"); + const hasCredentials = await ctx.fileExists(adcPath ?? VERTEX_ADC_PATH); + const hasProject = Boolean((await ctx.env("GOOGLE_CLOUD_PROJECT")) ?? (await ctx.env("GCLOUD_PROJECT"))); + const hasLocation = Boolean(await ctx.env("GOOGLE_CLOUD_LOCATION")); + if (hasCredentials && hasProject && hasLocation) { + return { auth: {}, source: "gcloud application default credentials" }; + } + return undefined; + }, +}; + +export function googleVertexProvider(): Provider<"google-vertex"> { + return createProvider({ + id: "google-vertex", + name: "Google Vertex AI", + auth: { apiKey: vertexAuth }, + models: Object.values(GOOGLE_VERTEX_MODELS), + api: googleVertexApi(), + }); +} diff --git a/packages/ai/src/providers/google.models.ts b/packages/ai/src/providers/google.models.ts new file mode 100644 index 00000000..7a6b5d38 --- /dev/null +++ b/packages/ai/src/providers/google.models.ts @@ -0,0 +1,288 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const GOOGLE_MODELS = { + "gemini-2.0-flash": { + id: "gemini-2.0-flash", + name: "Gemini 2.0 Flash", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.4, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 8192, + } satisfies Model<"google-generative-ai">, + "gemini-2.0-flash-lite": { + id: "gemini-2.0-flash-lite", + name: "Gemini 2.0 Flash-Lite", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.075, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 8192, + } satisfies Model<"google-generative-ai">, + "gemini-2.5-flash": { + id: "gemini-2.5-flash", + name: "Gemini 2.5 Flash", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 2.5, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-2.5-flash-lite": { + id: "gemini-2.5-flash-lite", + name: "Gemini 2.5 Flash-Lite", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.4, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-2.5-pro": { + id: "gemini-2.5-pro", + name: "Gemini 2.5 Pro", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-3-flash-preview": { + id: "gemini-3-flash-preview", + name: "Gemini 3 Flash Preview", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-3-pro-preview": { + id: "gemini-3-pro-preview", + name: "Gemini 3 Pro Preview", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-3.1-flash-lite": { + id: "gemini-3.1-flash-lite", + name: "Gemini 3.1 Flash Lite", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.5, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-3.1-flash-lite-preview": { + id: "gemini-3.1-flash-lite-preview", + name: "Gemini 3.1 Flash Lite Preview", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.5, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-3.1-pro-preview": { + id: "gemini-3.1-pro-preview", + name: "Gemini 3.1 Pro Preview", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-3.1-pro-preview-customtools": { + id: "gemini-3.1-pro-preview-customtools", + name: "Gemini 3.1 Pro Preview Custom Tools", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-3.5-flash": { + id: "gemini-3.5-flash", + name: "Gemini 3.5 Flash", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.5, + output: 9, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-flash-latest": { + id: "gemini-flash-latest", + name: "Gemini Flash Latest", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 2.5, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-flash-lite-latest": { + id: "gemini-flash-lite-latest", + name: "Gemini Flash-Lite Latest", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.4, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemma-4-26b-a4b-it": { + id: "gemma-4-26b-a4b-it", + name: "Gemma 4 26B A4B IT", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"MINIMAL","low":null,"medium":null,"high":"HIGH"}, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"google-generative-ai">, + "gemma-4-31b-it": { + id: "gemma-4-31b-it", + name: "Gemma 4 31B IT", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"MINIMAL","low":null,"medium":null,"high":"HIGH"}, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"google-generative-ai">, +} as const; diff --git a/packages/ai/src/providers/google.ts b/packages/ai/src/providers/google.ts new file mode 100644 index 00000000..0bd45237 --- /dev/null +++ b/packages/ai/src/providers/google.ts @@ -0,0 +1,15 @@ +import { googleGenerativeAIApi } from "../api/google-generative-ai.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { GOOGLE_MODELS } from "./google.models.ts"; + +export function googleProvider(): Provider<"google-generative-ai"> { + return createProvider({ + id: "google", + name: "Google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + auth: { apiKey: envApiKeyAuth("Gemini API key", ["GEMINI_API_KEY"]) }, + models: Object.values(GOOGLE_MODELS), + api: googleGenerativeAIApi(), + }); +} diff --git a/packages/ai/src/providers/groq.models.ts b/packages/ai/src/providers/groq.models.ts new file mode 100644 index 00000000..857048c7 --- /dev/null +++ b/packages/ai/src/providers/groq.models.ts @@ -0,0 +1,127 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const GROQ_MODELS = { + "llama-3.1-8b-instant": { + id: "llama-3.1-8b-instant", + name: "Llama 3.1 8B", + api: "openai-completions", + provider: "groq", + baseUrl: "https://api.groq.com/openai/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.05, + output: 0.08, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "llama-3.3-70b-versatile": { + id: "llama-3.3-70b-versatile", + name: "Llama 3.3 70B", + api: "openai-completions", + provider: "groq", + baseUrl: "https://api.groq.com/openai/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.59, + output: 0.79, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "meta-llama/llama-4-scout-17b-16e-instruct": { + id: "meta-llama/llama-4-scout-17b-16e-instruct", + name: "Llama 4 Scout 17B 16E", + api: "openai-completions", + provider: "groq", + baseUrl: "https://api.groq.com/openai/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.11, + output: 0.34, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 8192, + } satisfies Model<"openai-completions">, + "openai/gpt-oss-120b": { + id: "openai/gpt-oss-120b", + name: "GPT OSS 120B", + api: "openai-completions", + provider: "groq", + baseUrl: "https://api.groq.com/openai/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "openai/gpt-oss-20b": { + id: "openai/gpt-oss-20b", + name: "GPT OSS 20B", + api: "openai-completions", + provider: "groq", + baseUrl: "https://api.groq.com/openai/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.075, + output: 0.3, + cacheRead: 0.0375, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "openai/gpt-oss-safeguard-20b": { + id: "openai/gpt-oss-safeguard-20b", + name: "Safety GPT OSS 20B", + api: "openai-completions", + provider: "groq", + baseUrl: "https://api.groq.com/openai/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.075, + output: 0.3, + cacheRead: 0.037, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3-32b": { + id: "qwen/qwen3-32b", + name: "Qwen3-32B", + api: "openai-completions", + provider: "groq", + baseUrl: "https://api.groq.com/openai/v1", + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"default"}, + input: ["text"], + cost: { + input: 0.29, + output: 0.59, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 40960, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/groq.ts b/packages/ai/src/providers/groq.ts new file mode 100644 index 00000000..5892e048 --- /dev/null +++ b/packages/ai/src/providers/groq.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { GROQ_MODELS } from "./groq.models.ts"; + +export function groqProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "groq", + name: "Groq", + baseUrl: "https://api.groq.com/openai/v1", + auth: { apiKey: envApiKeyAuth("Groq API key", ["GROQ_API_KEY"]) }, + models: Object.values(GROQ_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/huggingface.models.ts b/packages/ai/src/providers/huggingface.models.ts new file mode 100644 index 00000000..282b8d82 --- /dev/null +++ b/packages/ai/src/providers/huggingface.models.ts @@ -0,0 +1,403 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const HUGGINGFACE_MODELS = { + "MiniMaxAI/MiniMax-M2.1": { + id: "MiniMaxAI/MiniMax-M2.1", + name: "MiniMax-M2.1", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "MiniMaxAI/MiniMax-M2.5": { + id: "MiniMaxAI/MiniMax-M2.5", + name: "MiniMax-M2.5", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "MiniMaxAI/MiniMax-M2.7": { + id: "MiniMaxAI/MiniMax-M2.7", + name: "MiniMax-M2.7", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3-235B-A22B-Thinking-2507": { + id: "Qwen/Qwen3-235B-A22B-Thinking-2507", + name: "Qwen3-235B-A22B-Thinking-2507", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3-Coder-480B-A35B-Instruct": { + id: "Qwen/Qwen3-Coder-480B-A35B-Instruct", + name: "Qwen3-Coder-480B-A35B-Instruct", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: false, + input: ["text"], + cost: { + input: 2, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 66536, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3-Coder-Next": { + id: "Qwen/Qwen3-Coder-Next", + name: "Qwen3-Coder-Next", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0.2, + output: 1.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3-Next-80B-A3B-Instruct": { + id: "Qwen/Qwen3-Next-80B-A3B-Instruct", + name: "Qwen3-Next-80B-A3B-Instruct", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0.25, + output: 1, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 66536, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3-Next-80B-A3B-Thinking": { + id: "Qwen/Qwen3-Next-80B-A3B-Thinking", + name: "Qwen3-Next-80B-A3B-Thinking", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0.3, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3.5-397B-A17B": { + id: "Qwen/Qwen3.5-397B-A17B", + name: "Qwen3.5-397B-A17B", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 3.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "XiaomiMiMo/MiMo-V2-Flash": { + id: "XiaomiMiMo/MiMo-V2-Flash", + name: "MiMo-V2-Flash", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.1, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "deepseek-ai/DeepSeek-R1-0528": { + id: "deepseek-ai/DeepSeek-R1-0528", + name: "DeepSeek-R1-0528", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 3, + output: 5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 163840, + } satisfies Model<"openai-completions">, + "deepseek-ai/DeepSeek-V3.2": { + id: "deepseek-ai/DeepSeek-V3.2", + name: "DeepSeek-V3.2", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.28, + output: 0.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "deepseek-ai/DeepSeek-V4-Pro": { + id: "deepseek-ai/DeepSeek-V4-Pro", + name: "DeepSeek V4 Pro", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.435, + output: 0.87, + cacheRead: 0.003625, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 393216, + } satisfies Model<"openai-completions">, + "moonshotai/Kimi-K2-Instruct": { + id: "moonshotai/Kimi-K2-Instruct", + name: "Kimi-K2-Instruct", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: false, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "moonshotai/Kimi-K2-Instruct-0905": { + id: "moonshotai/Kimi-K2-Instruct-0905", + name: "Kimi-K2-Instruct-0905", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: false, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "moonshotai/Kimi-K2-Thinking": { + id: "moonshotai/Kimi-K2-Thinking", + name: "Kimi-K2-Thinking", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.5, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "moonshotai/Kimi-K2.5": { + id: "moonshotai/Kimi-K2.5", + name: "Kimi-K2.5", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 3, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "moonshotai/Kimi-K2.6": { + id: "moonshotai/Kimi-K2.6", + name: "Kimi-K2.6", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.16, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "zai-org/GLM-4.7": { + id: "zai-org/GLM-4.7", + name: "GLM-4.7", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.2, + cacheRead: 0.11, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "zai-org/GLM-4.7-Flash": { + id: "zai-org/GLM-4.7-Flash", + name: "GLM-4.7-Flash", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "zai-org/GLM-5": { + id: "zai-org/GLM-5", + name: "GLM-5", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3.2, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "zai-org/GLM-5.1": { + id: "zai-org/GLM-5.1", + name: "GLM-5.1", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3.2, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 131072, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/huggingface.ts b/packages/ai/src/providers/huggingface.ts new file mode 100644 index 00000000..e8fb628e --- /dev/null +++ b/packages/ai/src/providers/huggingface.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { HUGGINGFACE_MODELS } from "./huggingface.models.ts"; + +export function huggingfaceProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "huggingface", + name: "Hugging Face", + baseUrl: "https://router.huggingface.co/v1", + auth: { apiKey: envApiKeyAuth("Hugging Face token", ["HF_TOKEN"]) }, + models: Object.values(HUGGINGFACE_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/kimi-coding.models.ts b/packages/ai/src/providers/kimi-coding.models.ts new file mode 100644 index 00000000..dd6f444c --- /dev/null +++ b/packages/ai/src/providers/kimi-coding.models.ts @@ -0,0 +1,43 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const KIMI_CODING_MODELS = { + "kimi-for-coding": { + id: "kimi-for-coding", + name: "Kimi For Coding", + api: "anthropic-messages", + provider: "kimi-coding", + baseUrl: "https://api.kimi.com/coding", + headers: {"User-Agent":"KimiCLI/1.5"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "kimi-k2-thinking": { + id: "kimi-k2-thinking", + name: "Kimi K2 Thinking", + api: "anthropic-messages", + provider: "kimi-coding", + baseUrl: "https://api.kimi.com/coding", + headers: {"User-Agent":"KimiCLI/1.5"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, +} as const; diff --git a/packages/ai/src/providers/kimi-coding.ts b/packages/ai/src/providers/kimi-coding.ts new file mode 100644 index 00000000..865ae28c --- /dev/null +++ b/packages/ai/src/providers/kimi-coding.ts @@ -0,0 +1,15 @@ +import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { KIMI_CODING_MODELS } from "./kimi-coding.models.ts"; + +export function kimiCodingProvider(): Provider<"anthropic-messages"> { + return createProvider({ + id: "kimi-coding", + name: "Kimi For Coding", + baseUrl: "https://api.kimi.com/coding", + auth: { apiKey: envApiKeyAuth("Kimi API key", ["KIMI_API_KEY"]) }, + models: Object.values(KIMI_CODING_MODELS), + api: anthropicMessagesApi(), + }); +} diff --git a/packages/ai/src/providers/minimax-cn.models.ts b/packages/ai/src/providers/minimax-cn.models.ts new file mode 100644 index 00000000..d1f90c21 --- /dev/null +++ b/packages/ai/src/providers/minimax-cn.models.ts @@ -0,0 +1,58 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const MINIMAX_CN_MODELS = { + "MiniMax-M2.7": { + id: "MiniMax-M2.7", + name: "MiniMax-M2.7", + api: "anthropic-messages", + provider: "minimax-cn", + baseUrl: "https://api.minimaxi.com/anthropic", + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0.375, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "MiniMax-M2.7-highspeed": { + id: "MiniMax-M2.7-highspeed", + name: "MiniMax-M2.7-highspeed", + api: "anthropic-messages", + provider: "minimax-cn", + baseUrl: "https://api.minimaxi.com/anthropic", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.4, + cacheRead: 0.06, + cacheWrite: 0.375, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "MiniMax-M3": { + id: "MiniMax-M3", + name: "MiniMax-M3", + api: "anthropic-messages", + provider: "minimax-cn", + baseUrl: "https://api.minimaxi.com/anthropic", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 2.4, + cacheRead: 0.12, + cacheWrite: 0, + }, + contextWindow: 512000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, +} as const; diff --git a/packages/ai/src/providers/minimax-cn.ts b/packages/ai/src/providers/minimax-cn.ts new file mode 100644 index 00000000..5cbe5acc --- /dev/null +++ b/packages/ai/src/providers/minimax-cn.ts @@ -0,0 +1,15 @@ +import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { MINIMAX_CN_MODELS } from "./minimax-cn.models.ts"; + +export function minimaxCnProvider(): Provider<"anthropic-messages"> { + return createProvider({ + id: "minimax-cn", + name: "MiniMax CN", + baseUrl: "https://api.minimaxi.com/anthropic", + auth: { apiKey: envApiKeyAuth("MiniMax CN API key", ["MINIMAX_CN_API_KEY"]) }, + models: Object.values(MINIMAX_CN_MODELS), + api: anthropicMessagesApi(), + }); +} diff --git a/packages/ai/src/providers/minimax.models.ts b/packages/ai/src/providers/minimax.models.ts new file mode 100644 index 00000000..0ff346c7 --- /dev/null +++ b/packages/ai/src/providers/minimax.models.ts @@ -0,0 +1,58 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const MINIMAX_MODELS = { + "MiniMax-M2.7": { + id: "MiniMax-M2.7", + name: "MiniMax-M2.7", + api: "anthropic-messages", + provider: "minimax", + baseUrl: "https://api.minimax.io/anthropic", + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0.375, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "MiniMax-M2.7-highspeed": { + id: "MiniMax-M2.7-highspeed", + name: "MiniMax-M2.7-highspeed", + api: "anthropic-messages", + provider: "minimax", + baseUrl: "https://api.minimax.io/anthropic", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.4, + cacheRead: 0.06, + cacheWrite: 0.375, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "MiniMax-M3": { + id: "MiniMax-M3", + name: "MiniMax-M3", + api: "anthropic-messages", + provider: "minimax", + baseUrl: "https://api.minimax.io/anthropic", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 2.4, + cacheRead: 0.12, + cacheWrite: 0, + }, + contextWindow: 512000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, +} as const; diff --git a/packages/ai/src/providers/minimax.ts b/packages/ai/src/providers/minimax.ts new file mode 100644 index 00000000..6a956bd6 --- /dev/null +++ b/packages/ai/src/providers/minimax.ts @@ -0,0 +1,15 @@ +import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { MINIMAX_MODELS } from "./minimax.models.ts"; + +export function minimaxProvider(): Provider<"anthropic-messages"> { + return createProvider({ + id: "minimax", + name: "MiniMax", + baseUrl: "https://api.minimax.io/anthropic", + auth: { apiKey: envApiKeyAuth("MiniMax API key", ["MINIMAX_API_KEY"]) }, + models: Object.values(MINIMAX_MODELS), + api: anthropicMessagesApi(), + }); +} diff --git a/packages/ai/src/providers/mistral.models.ts b/packages/ai/src/providers/mistral.models.ts new file mode 100644 index 00000000..689a092d --- /dev/null +++ b/packages/ai/src/providers/mistral.models.ts @@ -0,0 +1,517 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const MISTRAL_MODELS = { + "codestral-latest": { + id: "codestral-latest", + name: "Codestral (latest)", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.3, + output: 0.9, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 4096, + } satisfies Model<"mistral-conversations">, + "devstral-2512": { + id: "devstral-2512", + name: "Devstral 2", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"mistral-conversations">, + "devstral-latest": { + id: "devstral-latest", + name: "Devstral 2", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"mistral-conversations">, + "devstral-medium-2507": { + id: "devstral-medium-2507", + name: "Devstral Medium", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"mistral-conversations">, + "devstral-medium-latest": { + id: "devstral-medium-latest", + name: "Devstral 2 (latest)", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"mistral-conversations">, + "devstral-small-2505": { + id: "devstral-small-2505", + name: "Devstral Small 2505", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.1, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"mistral-conversations">, + "devstral-small-2507": { + id: "devstral-small-2507", + name: "Devstral Small", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.1, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"mistral-conversations">, + "labs-devstral-small-2512": { + id: "labs-devstral-small-2512", + name: "Devstral Small 2", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"mistral-conversations">, + "magistral-medium-latest": { + id: "magistral-medium-latest", + name: "Magistral Medium (latest)", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: true, + input: ["text"], + cost: { + input: 2, + output: 5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"mistral-conversations">, + "magistral-small": { + id: "magistral-small", + name: "Magistral Small", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: true, + input: ["text"], + cost: { + input: 0.5, + output: 1.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"mistral-conversations">, + "ministral-3b-latest": { + id: "ministral-3b-latest", + name: "Ministral 3B (latest)", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.04, + output: 0.04, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"mistral-conversations">, + "ministral-8b-latest": { + id: "ministral-8b-latest", + name: "Ministral 8B (latest)", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.1, + output: 0.1, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"mistral-conversations">, + "mistral-large-2411": { + id: "mistral-large-2411", + name: "Mistral Large 2.1", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 2, + output: 6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"mistral-conversations">, + "mistral-large-2512": { + id: "mistral-large-2512", + name: "Mistral Large 3", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.5, + output: 1.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"mistral-conversations">, + "mistral-large-latest": { + id: "mistral-large-latest", + name: "Mistral Large (latest)", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.5, + output: 1.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"mistral-conversations">, + "mistral-medium-2505": { + id: "mistral-medium-2505", + name: "Mistral Medium 3", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"mistral-conversations">, + "mistral-medium-2508": { + id: "mistral-medium-2508", + name: "Mistral Medium 3.1", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"mistral-conversations">, + "mistral-medium-2604": { + id: "mistral-medium-2604", + name: "Mistral Medium 3.5", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.5, + output: 7.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"mistral-conversations">, + "mistral-medium-3.5": { + id: "mistral-medium-3.5", + name: "Mistral Medium 3.5", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.5, + output: 7.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"mistral-conversations">, + "mistral-medium-latest": { + id: "mistral-medium-latest", + name: "Mistral Medium (latest)", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"mistral-conversations">, + "mistral-nemo": { + id: "mistral-nemo", + name: "Mistral Nemo", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.15, + output: 0.15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"mistral-conversations">, + "mistral-small-2506": { + id: "mistral-small-2506", + name: "Mistral Small 3.2", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"mistral-conversations">, + "mistral-small-2603": { + id: "mistral-small-2603", + name: "Mistral Small 4", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"mistral-conversations">, + "mistral-small-latest": { + id: "mistral-small-latest", + name: "Mistral Small (latest)", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"mistral-conversations">, + "open-mistral-7b": { + id: "open-mistral-7b", + name: "Mistral 7B", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.25, + output: 0.25, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 8000, + maxTokens: 8000, + } satisfies Model<"mistral-conversations">, + "open-mistral-nemo": { + id: "open-mistral-nemo", + name: "Open Mistral Nemo", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.15, + output: 0.15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"mistral-conversations">, + "open-mixtral-8x22b": { + id: "open-mixtral-8x22b", + name: "Mixtral 8x22B", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 2, + output: 6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 64000, + maxTokens: 64000, + } satisfies Model<"mistral-conversations">, + "open-mixtral-8x7b": { + id: "open-mixtral-8x7b", + name: "Mixtral 8x7B", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.7, + output: 0.7, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 32000, + maxTokens: 32000, + } satisfies Model<"mistral-conversations">, + "pixtral-12b": { + id: "pixtral-12b", + name: "Pixtral 12B", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"mistral-conversations">, + "pixtral-large-latest": { + id: "pixtral-large-latest", + name: "Pixtral Large (latest)", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2, + output: 6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"mistral-conversations">, +} as const; diff --git a/packages/ai/src/providers/mistral.ts b/packages/ai/src/providers/mistral.ts new file mode 100644 index 00000000..9b84a71f --- /dev/null +++ b/packages/ai/src/providers/mistral.ts @@ -0,0 +1,15 @@ +import { mistralConversationsApi } from "../api/mistral-conversations.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { MISTRAL_MODELS } from "./mistral.models.ts"; + +export function mistralProvider(): Provider<"mistral-conversations"> { + return createProvider({ + id: "mistral", + name: "Mistral", + baseUrl: "https://api.mistral.ai", + auth: { apiKey: envApiKeyAuth("Mistral API key", ["MISTRAL_API_KEY"]) }, + models: Object.values(MISTRAL_MODELS), + api: mistralConversationsApi(), + }); +} diff --git a/packages/ai/src/providers/moonshotai-cn.models.ts b/packages/ai/src/providers/moonshotai-cn.models.ts new file mode 100644 index 00000000..ea2c9e7a --- /dev/null +++ b/packages/ai/src/providers/moonshotai-cn.models.ts @@ -0,0 +1,133 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const MOONSHOTAI_CN_MODELS = { + "kimi-k2-0711-preview": { + id: "kimi-k2-0711-preview", + name: "Kimi K2 0711", + api: "openai-completions", + provider: "moonshotai-cn", + baseUrl: "https://api.moonshot.cn/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.6, + output: 2.5, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "kimi-k2-0905-preview": { + id: "kimi-k2-0905-preview", + name: "Kimi K2 0905", + api: "openai-completions", + provider: "moonshotai-cn", + baseUrl: "https://api.moonshot.cn/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.6, + output: 2.5, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2-thinking": { + id: "kimi-k2-thinking", + name: "Kimi K2 Thinking", + api: "openai-completions", + provider: "moonshotai-cn", + baseUrl: "https://api.moonshot.cn/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.5, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2-thinking-turbo": { + id: "kimi-k2-thinking-turbo", + name: "Kimi K2 Thinking Turbo", + api: "openai-completions", + provider: "moonshotai-cn", + baseUrl: "https://api.moonshot.cn/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.15, + output: 8, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2-turbo-preview": { + id: "kimi-k2-turbo-preview", + name: "Kimi K2 Turbo", + api: "openai-completions", + provider: "moonshotai-cn", + baseUrl: "https://api.moonshot.cn/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: false, + input: ["text"], + cost: { + input: 2.4, + output: 10, + cacheRead: 0.6, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2.5": { + id: "kimi-k2.5", + name: "Kimi K2.5", + api: "openai-completions", + provider: "moonshotai-cn", + baseUrl: "https://api.moonshot.cn/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 3, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2.6": { + id: "kimi-k2.6", + name: "Kimi K2.6", + api: "openai-completions", + provider: "moonshotai-cn", + baseUrl: "https://api.moonshot.cn/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.16, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/moonshotai-cn.ts b/packages/ai/src/providers/moonshotai-cn.ts new file mode 100644 index 00000000..b813734b --- /dev/null +++ b/packages/ai/src/providers/moonshotai-cn.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { MOONSHOTAI_CN_MODELS } from "./moonshotai-cn.models.ts"; + +export function moonshotaiCnProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "moonshotai-cn", + name: "Moonshot AI CN", + baseUrl: "https://api.moonshot.cn/v1", + auth: { apiKey: envApiKeyAuth("Moonshot AI API key", ["MOONSHOT_API_KEY"]) }, + models: Object.values(MOONSHOTAI_CN_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/moonshotai.models.ts b/packages/ai/src/providers/moonshotai.models.ts new file mode 100644 index 00000000..b04a7bcb --- /dev/null +++ b/packages/ai/src/providers/moonshotai.models.ts @@ -0,0 +1,133 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const MOONSHOTAI_MODELS = { + "kimi-k2-0711-preview": { + id: "kimi-k2-0711-preview", + name: "Kimi K2 0711", + api: "openai-completions", + provider: "moonshotai", + baseUrl: "https://api.moonshot.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.6, + output: 2.5, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "kimi-k2-0905-preview": { + id: "kimi-k2-0905-preview", + name: "Kimi K2 0905", + api: "openai-completions", + provider: "moonshotai", + baseUrl: "https://api.moonshot.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.6, + output: 2.5, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2-thinking": { + id: "kimi-k2-thinking", + name: "Kimi K2 Thinking", + api: "openai-completions", + provider: "moonshotai", + baseUrl: "https://api.moonshot.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.5, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2-thinking-turbo": { + id: "kimi-k2-thinking-turbo", + name: "Kimi K2 Thinking Turbo", + api: "openai-completions", + provider: "moonshotai", + baseUrl: "https://api.moonshot.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.15, + output: 8, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2-turbo-preview": { + id: "kimi-k2-turbo-preview", + name: "Kimi K2 Turbo", + api: "openai-completions", + provider: "moonshotai", + baseUrl: "https://api.moonshot.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: false, + input: ["text"], + cost: { + input: 2.4, + output: 10, + cacheRead: 0.6, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2.5": { + id: "kimi-k2.5", + name: "Kimi K2.5", + api: "openai-completions", + provider: "moonshotai", + baseUrl: "https://api.moonshot.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 3, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2.6": { + id: "kimi-k2.6", + name: "Kimi K2.6", + api: "openai-completions", + provider: "moonshotai", + baseUrl: "https://api.moonshot.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.16, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/moonshotai.ts b/packages/ai/src/providers/moonshotai.ts new file mode 100644 index 00000000..dc15c570 --- /dev/null +++ b/packages/ai/src/providers/moonshotai.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { MOONSHOTAI_MODELS } from "./moonshotai.models.ts"; + +export function moonshotaiProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "moonshotai", + name: "Moonshot AI", + baseUrl: "https://api.moonshot.ai/v1", + auth: { apiKey: envApiKeyAuth("Moonshot AI API key", ["MOONSHOT_API_KEY"]) }, + models: Object.values(MOONSHOTAI_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/nvidia.models.ts b/packages/ai/src/providers/nvidia.models.ts new file mode 100644 index 00000000..76590901 --- /dev/null +++ b/packages/ai/src/providers/nvidia.models.ts @@ -0,0 +1,387 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const NVIDIA_MODELS = { + "meta/llama-3.1-70b-instruct": { + id: "meta/llama-3.1-70b-instruct", + name: "Llama 3.1 70b Instruct", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "meta/llama-3.1-8b-instruct": { + id: "meta/llama-3.1-8b-instruct", + name: "Llama 3.1 8B Instruct", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 16000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "meta/llama-3.2-11b-vision-instruct": { + id: "meta/llama-3.2-11b-vision-instruct", + name: "Llama 3.2 11b Vision Instruct", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "meta/llama-3.2-90b-vision-instruct": { + id: "meta/llama-3.2-90b-vision-instruct", + name: "Llama-3.2-90B-Vision-Instruct", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"openai-completions">, + "meta/llama-3.3-70b-instruct": { + id: "meta/llama-3.3-70b-instruct", + name: "Llama 3.3 70b Instruct", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/mistral-large-3-675b-instruct-2512": { + id: "mistralai/mistral-large-3-675b-instruct-2512", + name: "Mistral Large 3 675B Instruct 2512", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "mistralai/mistral-small-4-119b-2603": { + id: "mistralai/mistral-small-4-119b-2603", + name: "mistral-small-4-119b-2603", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"openai-completions">, + "moonshotai/kimi-k2.6": { + id: "moonshotai/kimi-k2.6", + name: "Kimi K2.6", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-nano-30b-a3b": { + id: "nvidia/nemotron-3-nano-30b-a3b", + name: "nemotron-3-nano-30b-a3b", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning": { + id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning", + name: "Nemotron 3 Nano Omni", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-super-120b-a12b": { + id: "nvidia/nemotron-3-super-120b-a12b", + name: "Nemotron 3 Super", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.2, + output: 0.8, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-ultra-550b-a55b": { + id: "nvidia/nemotron-3-ultra-550b-a55b", + name: "Nemotron 3 Ultra 550B A55B", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.5, + output: 2.5, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "nvidia/nvidia-nemotron-nano-9b-v2": { + id: "nvidia/nvidia-nemotron-nano-9b-v2", + name: "nvidia-nemotron-nano-9b-v2", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "openai/gpt-oss-120b": { + id: "openai/gpt-oss-120b", + name: "GPT-OSS-120B", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"openai-completions">, + "openai/gpt-oss-20b": { + id: "openai/gpt-oss-20b", + name: "GPT OSS 20B", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3-coder-480b-a35b-instruct": { + id: "qwen/qwen3-coder-480b-a35b-instruct", + name: "Qwen3 Coder 480B A35B Instruct", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 66536, + } satisfies Model<"openai-completions">, + "qwen/qwen3.5-122b-a10b": { + id: "qwen/qwen3.5-122b-a10b", + name: "Qwen3.5 122B-A10B", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "stepfun-ai/step-3.5-flash": { + id: "stepfun-ai/step-3.5-flash", + name: "Step 3.5 Flash", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "stepfun-ai/step-3.7-flash": { + id: "stepfun-ai/step-3.7-flash", + name: "Step 3.7 Flash", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "z-ai/glm-5.1": { + id: "z-ai/glm-5.1", + name: "GLM-5.1", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/nvidia.ts b/packages/ai/src/providers/nvidia.ts new file mode 100644 index 00000000..dc539f60 --- /dev/null +++ b/packages/ai/src/providers/nvidia.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { NVIDIA_MODELS } from "./nvidia.models.ts"; + +export function nvidiaProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "nvidia", + name: "NVIDIA", + baseUrl: "https://integrate.api.nvidia.com/v1", + auth: { apiKey: envApiKeyAuth("NVIDIA API key", ["NVIDIA_API_KEY"]) }, + models: Object.values(NVIDIA_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/openai-codex.models.ts b/packages/ai/src/providers/openai-codex.models.ts new file mode 100644 index 00000000..c849c24c --- /dev/null +++ b/packages/ai/src/providers/openai-codex.models.ts @@ -0,0 +1,79 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const OPENAI_CODEX_MODELS = { + "gpt-5.3-codex-spark": { + id: "gpt-5.3-codex-spark", + name: "GPT-5.3 Codex Spark", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, + input: ["text"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"openai-codex-responses">, + "gpt-5.4": { + id: "gpt-5.4", + name: "GPT-5.4", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, + input: ["text", "image"], + cost: { + input: 2.5, + output: 15, + cacheRead: 0.25, + cacheWrite: 0, + }, + contextWindow: 272000, + maxTokens: 128000, + } satisfies Model<"openai-codex-responses">, + "gpt-5.4-mini": { + id: "gpt-5.4-mini", + name: "GPT-5.4 mini", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, + input: ["text", "image"], + cost: { + input: 0.75, + output: 4.5, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 272000, + maxTokens: 128000, + } satisfies Model<"openai-codex-responses">, + "gpt-5.5": { + id: "gpt-5.5", + name: "GPT-5.5", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 272000, + maxTokens: 128000, + } satisfies Model<"openai-codex-responses">, +} as const; diff --git a/packages/ai/src/providers/openai-codex.ts b/packages/ai/src/providers/openai-codex.ts new file mode 100644 index 00000000..6ccdb6ef --- /dev/null +++ b/packages/ai/src/providers/openai-codex.ts @@ -0,0 +1,18 @@ +import { openAICodexResponsesApi } from "../api/openai-codex-responses.lazy.ts"; +import { lazyOAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { loadOpenAICodexOAuth } from "../utils/oauth/load.ts"; +import { OPENAI_CODEX_MODELS } from "./openai-codex.models.ts"; + +export function openaiCodexProvider(): Provider<"openai-codex-responses"> { + return createProvider({ + id: "openai-codex", + name: "OpenAI Codex", + baseUrl: "https://chatgpt.com/backend-api", + auth: { + oauth: lazyOAuth({ name: "OpenAI (ChatGPT Plus/Pro)", load: loadOpenAICodexOAuth }), + }, + models: Object.values(OPENAI_CODEX_MODELS), + api: openAICodexResponsesApi(), + }); +} diff --git a/packages/ai/src/providers/openai.models.ts b/packages/ai/src/providers/openai.models.ts new file mode 100644 index 00000000..fcf9a76c --- /dev/null +++ b/packages/ai/src/providers/openai.models.ts @@ -0,0 +1,745 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const OPENAI_MODELS = { + "gpt-4": { + id: "gpt-4", + name: "GPT-4", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + input: ["text"], + cost: { + input: 30, + output: 60, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 8192, + maxTokens: 8192, + } satisfies Model<"openai-responses">, + "gpt-4-turbo": { + id: "gpt-4-turbo", + name: "GPT-4 Turbo", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 10, + output: 30, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-responses">, + "gpt-4.1": { + id: "gpt-4.1", + name: "GPT-4.1", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 32768, + } satisfies Model<"openai-responses">, + "gpt-4.1-mini": { + id: "gpt-4.1-mini", + name: "GPT-4.1 mini", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.4, + output: 1.6, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 32768, + } satisfies Model<"openai-responses">, + "gpt-4.1-nano": { + id: "gpt-4.1-nano", + name: "GPT-4.1 nano", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.4, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 32768, + } satisfies Model<"openai-responses">, + "gpt-4o": { + id: "gpt-4o", + name: "GPT-4o", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 10, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-responses">, + "gpt-4o-2024-05-13": { + id: "gpt-4o-2024-05-13", + name: "GPT-4o (2024-05-13)", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 5, + output: 15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-responses">, + "gpt-4o-2024-08-06": { + id: "gpt-4o-2024-08-06", + name: "GPT-4o (2024-08-06)", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 10, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-responses">, + "gpt-4o-2024-11-20": { + id: "gpt-4o-2024-11-20", + name: "GPT-4o (2024-11-20)", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 10, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-responses">, + "gpt-4o-mini": { + id: "gpt-4o-mini", + name: "GPT-4o mini", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-responses">, + "gpt-5": { + id: "gpt-5", + name: "GPT-5", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5-chat-latest": { + id: "gpt-5-chat-latest", + name: "GPT-5 Chat Latest", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-responses">, + "gpt-5-codex": { + id: "gpt-5-codex", + name: "GPT-5-Codex", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5-mini": { + id: "gpt-5-mini", + name: "GPT-5 Mini", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5-nano": { + id: "gpt-5-nano", + name: "GPT-5 Nano", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.05, + output: 0.4, + cacheRead: 0.005, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5-pro": { + id: "gpt-5-pro", + name: "GPT-5 Pro", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 15, + output: 120, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.1": { + id: "gpt-5.1", + name: "GPT-5.1", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":"none"}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.1-chat-latest": { + id: "gpt-5.1-chat-latest", + name: "GPT-5.1 Chat", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-responses">, + "gpt-5.1-codex": { + id: "gpt-5.1-codex", + name: "GPT-5.1 Codex", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.1-codex-max": { + id: "gpt-5.1-codex-max", + name: "GPT-5.1 Codex Max", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.1-codex-mini": { + id: "gpt-5.1-codex-mini", + name: "GPT-5.1 Codex mini", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.2": { + id: "gpt-5.2", + name: "GPT-5.2", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":"none","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.2-chat-latest": { + id: "gpt-5.2-chat-latest", + name: "GPT-5.2 Chat", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-responses">, + "gpt-5.2-codex": { + id: "gpt-5.2-codex", + name: "GPT-5.2 Codex", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.2-pro": { + id: "gpt-5.2-pro", + name: "GPT-5.2 Pro", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 21, + output: 168, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.3-chat-latest": { + id: "gpt-5.3-chat-latest", + name: "GPT-5.3 Chat (latest)", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-responses">, + "gpt-5.3-codex": { + id: "gpt-5.3-codex", + name: "GPT-5.3 Codex", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":"none","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.3-codex-spark": { + id: "gpt-5.3-codex-spark", + name: "GPT-5.3 Codex Spark", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 32000, + } satisfies Model<"openai-responses">, + "gpt-5.4": { + id: "gpt-5.4", + name: "GPT-5.4", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":"none","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 2.5, + output: 15, + cacheRead: 0.25, + cacheWrite: 0, + }, + contextWindow: 272000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.4-mini": { + id: "gpt-5.4-mini", + name: "GPT-5.4 mini", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":"none","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.75, + output: 4.5, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.4-nano": { + id: "gpt-5.4-nano", + name: "GPT-5.4 nano", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":"none","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.2, + output: 1.25, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.4-pro": { + id: "gpt-5.4-pro", + name: "GPT-5.4 Pro", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 30, + output: 180, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.5": { + id: "gpt-5.5", + name: "GPT-5.5", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":"none","xhigh":"xhigh","minimal":null}, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 272000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.5-pro": { + id: "gpt-5.5-pro", + name: "GPT-5.5 Pro", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh","minimal":null,"low":null}, + input: ["text", "image"], + cost: { + input: 30, + output: 180, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "o1": { + id: "o1", + name: "o1", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 60, + cacheRead: 7.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o1-pro": { + id: "o1-pro", + name: "o1-pro", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 150, + output: 600, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o3": { + id: "o3", + name: "o3", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o3-deep-research": { + id: "o3-deep-research", + name: "o3-deep-research", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 10, + output: 40, + cacheRead: 2.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o3-mini": { + id: "o3-mini", + name: "o3-mini", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + input: ["text"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.55, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o3-pro": { + id: "o3-pro", + name: "o3-pro", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 20, + output: 80, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o4-mini": { + id: "o4-mini", + name: "o4-mini", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.275, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o4-mini-deep-research": { + id: "o4-mini-deep-research", + name: "o4-mini-deep-research", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, +} as const; diff --git a/packages/ai/src/providers/openai.ts b/packages/ai/src/providers/openai.ts new file mode 100644 index 00000000..43f6671f --- /dev/null +++ b/packages/ai/src/providers/openai.ts @@ -0,0 +1,15 @@ +import { openAIResponsesApi } from "../api/openai-responses.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { OPENAI_MODELS } from "./openai.models.ts"; + +export function openaiProvider(): Provider<"openai-responses"> { + return createProvider({ + id: "openai", + name: "OpenAI", + baseUrl: "https://api.openai.com/v1", + auth: { apiKey: envApiKeyAuth("OpenAI API key", ["OPENAI_API_KEY"]) }, + models: Object.values(OPENAI_MODELS), + api: openAIResponsesApi(), + }); +} diff --git a/packages/ai/src/providers/opencode-go.models.ts b/packages/ai/src/providers/opencode-go.models.ts new file mode 100644 index 00000000..7dcb726f --- /dev/null +++ b/packages/ai/src/providers/opencode-go.models.ts @@ -0,0 +1,258 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const OPENCODE_GO_MODELS = { + "deepseek-v4-flash": { + id: "deepseek-v4-flash", + name: "DeepSeek V4 Flash", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 0.14, + output: 0.28, + cacheRead: 0.0028, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"openai-completions">, + "deepseek-v4-pro": { + id: "deepseek-v4-pro", + name: "DeepSeek V4 Pro", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 1.74, + output: 3.48, + cacheRead: 0.0145, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"openai-completions">, + "glm-5": { + id: "glm-5", + name: "GLM-5", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3.2, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "glm-5.1": { + id: "glm-5.1", + name: "GLM-5.1", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0.26, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "kimi-k2.5": { + id: "kimi-k2.5", + name: "Kimi K2.5", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 3, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "kimi-k2.6": { + id: "kimi-k2.6", + name: "Kimi K2.6", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.16, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "mimo-v2.5": { + id: "mimo-v2.5", + name: "MiMo V2.5", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.14, + output: 0.28, + cacheRead: 0.0028, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "mimo-v2.5-pro": { + id: "mimo-v2.5-pro", + name: "MiMo V2.5 Pro", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.74, + output: 3.48, + cacheRead: 0.0145, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "minimax-m2.5": { + id: "minimax-m2.5", + name: "MiniMax M2.5", + api: "anthropic-messages", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go", + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, + "minimax-m2.7": { + id: "minimax-m2.7", + name: "MiniMax M2.7", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "minimax-m3": { + id: "minimax-m3", + name: "MiniMax M3", + api: "anthropic-messages", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 512000, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "qwen3.6-plus": { + id: "qwen3.6-plus", + name: "Qwen3.6 Plus", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"thinkingFormat":"qwen","maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0.05, + cacheWrite: 0.625, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen3.7-max": { + id: "qwen3.7-max", + name: "Qwen3.7 Max", + api: "anthropic-messages", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go", + reasoning: true, + input: ["text"], + cost: { + input: 2.5, + output: 7.5, + cacheRead: 0.5, + cacheWrite: 3.125, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, + "qwen3.7-plus": { + id: "qwen3.7-plus", + name: "Qwen3.7 Plus", + api: "anthropic-messages", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 1.6, + cacheRead: 0.04, + cacheWrite: 0.5, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, +} as const; diff --git a/packages/ai/src/providers/opencode-go.ts b/packages/ai/src/providers/opencode-go.ts new file mode 100644 index 00000000..608f579b --- /dev/null +++ b/packages/ai/src/providers/opencode-go.ts @@ -0,0 +1,18 @@ +import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts"; +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { OPENCODE_GO_MODELS } from "./opencode-go.models.ts"; + +export function opencodeGoProvider(): Provider<"anthropic-messages" | "openai-completions"> { + return createProvider({ + id: "opencode-go", + name: "OpenCode Zen Go", + auth: { apiKey: envApiKeyAuth("OpenCode API key", ["OPENCODE_API_KEY"]) }, + models: Object.values(OPENCODE_GO_MODELS), + api: { + "anthropic-messages": anthropicMessagesApi(), + "openai-completions": openAICompletionsApi(), + }, + }); +} diff --git a/packages/ai/src/providers/opencode.models.ts b/packages/ai/src/providers/opencode.models.ts new file mode 100644 index 00000000..6138758d --- /dev/null +++ b/packages/ai/src/providers/opencode.models.ts @@ -0,0 +1,818 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const OPENCODE_MODELS = { + "big-pickle": { + id: "big-pickle", + name: "Big Pickle", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"openai-completions">, + "claude-fable-5": { + id: "claude-fable-5", + name: "Claude Fable 5", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + compat: {"forceAdaptiveThinking":true}, + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 1, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-haiku-4-5": { + id: "claude-haiku-4-5", + name: "Claude Haiku 4.5", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-1": { + id: "claude-opus-4-1", + name: "Claude Opus 4.1", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-5": { + id: "claude-opus-4-5", + name: "Claude Opus 4.5", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-6": { + id: "claude-opus-4-6", + name: "Claude Opus 4.6", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + compat: {"forceAdaptiveThinking":true}, + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-7": { + id: "claude-opus-4-7", + name: "Claude Opus 4.7", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-8": { + id: "claude-opus-4-8", + name: "Claude Opus 4.8", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4": { + id: "claude-sonnet-4", + name: "Claude Sonnet 4", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4-5": { + id: "claude-sonnet-4-5", + name: "Claude Sonnet 4.5", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4-6": { + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + compat: {"forceAdaptiveThinking":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "deepseek-v4-flash": { + id: "deepseek-v4-flash", + name: "DeepSeek V4 Flash", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 0.14, + output: 0.28, + cacheRead: 0.028, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"openai-completions">, + "deepseek-v4-flash-free": { + id: "deepseek-v4-flash-free", + name: "DeepSeek V4 Flash Free", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "deepseek-v4-pro": { + id: "deepseek-v4-pro", + name: "DeepSeek V4 Pro", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 1.74, + output: 3.84, + cacheRead: 0.145, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"openai-completions">, + "gemini-3-flash": { + id: "gemini-3-flash", + name: "Gemini 3 Flash", + api: "google-generative-ai", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-3.1-pro": { + id: "gemini-3.1-pro", + name: "Gemini 3.1 Pro Preview", + api: "google-generative-ai", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-3.5-flash": { + id: "gemini-3.5-flash", + name: "Gemini 3.5 Flash", + api: "google-generative-ai", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.5, + output: 9, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "glm-5": { + id: "glm-5", + name: "GLM-5", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3.2, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "glm-5.1": { + id: "glm-5.1", + name: "GLM-5.1", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0.26, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "gpt-5": { + id: "gpt-5", + name: "GPT-5", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.07, + output: 8.5, + cacheRead: 0.107, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5-codex": { + id: "gpt-5-codex", + name: "GPT-5 Codex", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.07, + output: 8.5, + cacheRead: 0.107, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5-nano": { + id: "gpt-5-nano", + name: "GPT-5 Nano", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.05, + output: 0.4, + cacheRead: 0.005, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.1": { + id: "gpt-5.1", + name: "GPT-5.1", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.07, + output: 8.5, + cacheRead: 0.107, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.1-codex": { + id: "gpt-5.1-codex", + name: "GPT-5.1 Codex", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.07, + output: 8.5, + cacheRead: 0.107, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.1-codex-max": { + id: "gpt-5.1-codex-max", + name: "GPT-5.1 Codex Max", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.1-codex-mini": { + id: "gpt-5.1-codex-mini", + name: "GPT-5.1 Codex Mini", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.2": { + id: "gpt-5.2", + name: "GPT-5.2", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.2-codex": { + id: "gpt-5.2-codex", + name: "GPT-5.2 Codex", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.3-codex": { + id: "gpt-5.3-codex", + name: "GPT-5.3 Codex", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.4": { + id: "gpt-5.4", + name: "GPT-5.4", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 2.5, + output: 15, + cacheRead: 0.25, + cacheWrite: 0, + }, + contextWindow: 272000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.4-mini": { + id: "gpt-5.4-mini", + name: "GPT-5.4 Mini", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.75, + output: 4.5, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.4-nano": { + id: "gpt-5.4-nano", + name: "GPT-5.4 Nano", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.2, + output: 1.25, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.4-pro": { + id: "gpt-5.4-pro", + name: "GPT-5.4 Pro", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 30, + output: 180, + cacheRead: 30, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.5": { + id: "gpt-5.5", + name: "GPT-5.5", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.5-pro": { + id: "gpt-5.5-pro", + name: "GPT-5.5 Pro", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh","minimal":null,"low":null}, + input: ["text", "image"], + cost: { + input: 30, + output: 180, + cacheRead: 30, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "grok-build-0.1": { + id: "grok-build-0.1", + name: "Grok Build 0.1", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"supportsReasoningEffort":false,"maxTokensField":"max_tokens"}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null}, + input: ["text", "image"], + cost: { + input: 1, + output: 2, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"openai-completions">, + "kimi-k2.5": { + id: "kimi-k2.5", + name: "Kimi K2.5", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 3, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "kimi-k2.6": { + id: "kimi-k2.6", + name: "Kimi K2.6", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.16, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "mimo-v2.5-free": { + id: "mimo-v2.5-free", + name: "MiMo V2.5 Free", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"openai-completions">, + "minimax-m2.5": { + id: "minimax-m2.5", + name: "MiniMax M2.5", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "minimax-m2.7": { + id: "minimax-m2.7", + name: "MiniMax M2.7", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "nemotron-3-ultra-free": { + id: "nemotron-3-ultra-free", + name: "Nemotron 3 Ultra Free", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "north-mini-code-free": { + id: "north-mini-code-free", + name: "North Mini Code Free", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "qwen3.5-plus": { + id: "qwen3.5-plus", + name: "Qwen3.5 Plus", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.2, + output: 1.2, + cacheRead: 0.02, + cacheWrite: 0.25, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, + "qwen3.6-plus": { + id: "qwen3.6-plus", + name: "Qwen3.6 Plus", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0.05, + cacheWrite: 0.625, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, +} as const; diff --git a/packages/ai/src/providers/opencode.ts b/packages/ai/src/providers/opencode.ts new file mode 100644 index 00000000..7d6d2cf7 --- /dev/null +++ b/packages/ai/src/providers/opencode.ts @@ -0,0 +1,24 @@ +import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts"; +import { googleGenerativeAIApi } from "../api/google-generative-ai.lazy.ts"; +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { openAIResponsesApi } from "../api/openai-responses.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { OPENCODE_MODELS } from "./opencode.models.ts"; + +export function opencodeProvider(): Provider< + "anthropic-messages" | "google-generative-ai" | "openai-completions" | "openai-responses" +> { + return createProvider({ + id: "opencode", + name: "OpenCode Zen", + auth: { apiKey: envApiKeyAuth("OpenCode API key", ["OPENCODE_API_KEY"]) }, + models: Object.values(OPENCODE_MODELS), + api: { + "anthropic-messages": anthropicMessagesApi(), + "google-generative-ai": googleGenerativeAIApi(), + "openai-completions": openAICompletionsApi(), + "openai-responses": openAIResponsesApi(), + }, + }); +} diff --git a/packages/ai/src/providers/openrouter.models.ts b/packages/ai/src/providers/openrouter.models.ts new file mode 100644 index 00000000..82f78734 --- /dev/null +++ b/packages/ai/src/providers/openrouter.models.ts @@ -0,0 +1,4332 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const OPENROUTER_MODELS = { + "ai21/jamba-large-1.7": { + id: "ai21/jamba-large-1.7", + name: "AI21: Jamba Large 1.7", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 2, + output: 8, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "amazon/nova-2-lite-v1": { + id: "amazon/nova-2-lite-v1", + name: "Amazon: Nova 2 Lite", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 2.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65535, + } satisfies Model<"openai-completions">, + "amazon/nova-lite-v1": { + id: "amazon/nova-lite-v1", + name: "Amazon: Nova Lite 1.0", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.06, + output: 0.24, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 300000, + maxTokens: 5120, + } satisfies Model<"openai-completions">, + "amazon/nova-micro-v1": { + id: "amazon/nova-micro-v1", + name: "Amazon: Nova Micro 1.0", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.035, + output: 0.14, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 5120, + } satisfies Model<"openai-completions">, + "amazon/nova-premier-v1": { + id: "amazon/nova-premier-v1", + name: "Amazon: Nova Premier 1.0", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 12.5, + cacheRead: 0.625, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 32000, + } satisfies Model<"openai-completions">, + "amazon/nova-pro-v1": { + id: "amazon/nova-pro-v1", + name: "Amazon: Nova Pro 1.0", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.7999999999999999, + output: 3.1999999999999997, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 300000, + maxTokens: 5120, + } satisfies Model<"openai-completions">, + "anthropic/claude-3-haiku": { + id: "anthropic/claude-3-haiku", + name: "Anthropic: Claude 3 Haiku", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.25, + cacheRead: 0.03, + cacheWrite: 0.3, + }, + contextWindow: 200000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "anthropic/claude-3.5-haiku": { + id: "anthropic/claude-3.5-haiku", + name: "Anthropic: Claude 3.5 Haiku", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.7999999999999999, + output: 4, + cacheRead: 0.08, + cacheWrite: 1, + }, + contextWindow: 200000, + maxTokens: 8192, + } satisfies Model<"openai-completions">, + "anthropic/claude-fable-5": { + id: "anthropic/claude-fable-5", + name: "Anthropic: Claude Fable 5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 1, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "anthropic/claude-haiku-4.5": { + id: "anthropic/claude-haiku-4.5", + name: "Anthropic: Claude Haiku 4.5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.09999999999999999, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "anthropic/claude-opus-4": { + id: "anthropic/claude-opus-4", + name: "Anthropic: Claude Opus 4", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"openai-completions">, + "anthropic/claude-opus-4.1": { + id: "anthropic/claude-opus-4.1", + name: "Anthropic: Claude Opus 4.1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"openai-completions">, + "anthropic/claude-opus-4.5": { + id: "anthropic/claude-opus-4.5", + name: "Anthropic: Claude Opus 4.5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "anthropic/claude-opus-4.6": { + id: "anthropic/claude-opus-4.6", + name: "Anthropic: Claude Opus 4.6", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "anthropic/claude-opus-4.6-fast": { + id: "anthropic/claude-opus-4.6-fast", + name: "Anthropic: Claude Opus 4.6 (Fast)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 30, + output: 150, + cacheRead: 3, + cacheWrite: 37.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "anthropic/claude-opus-4.7": { + id: "anthropic/claude-opus-4.7", + name: "Anthropic: Claude Opus 4.7", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "anthropic/claude-opus-4.7-fast": { + id: "anthropic/claude-opus-4.7-fast", + name: "Anthropic: Claude Opus 4.7 (Fast)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 30, + output: 150, + cacheRead: 3, + cacheWrite: 37.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "anthropic/claude-opus-4.8": { + id: "anthropic/claude-opus-4.8", + name: "Anthropic: Claude Opus 4.8", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "anthropic/claude-opus-4.8-fast": { + id: "anthropic/claude-opus-4.8-fast", + name: "Anthropic: Claude Opus 4.8 (Fast)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 1, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "anthropic/claude-sonnet-4": { + id: "anthropic/claude-sonnet-4", + name: "Anthropic: Claude Sonnet 4", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "anthropic/claude-sonnet-4.5": { + id: "anthropic/claude-sonnet-4.5", + name: "Anthropic: Claude Sonnet 4.5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "anthropic/claude-sonnet-4.6": { + id: "anthropic/claude-sonnet-4.6", + name: "Anthropic: Claude Sonnet 4.6", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "arcee-ai/trinity-large-thinking": { + id: "arcee-ai/trinity-large-thinking", + name: "Arcee AI: Trinity Large Thinking", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.22, + output: 0.85, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "arcee-ai/trinity-mini": { + id: "arcee-ai/trinity-mini", + name: "Arcee AI: Trinity Mini", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.045, + output: 0.15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "arcee-ai/virtuoso-large": { + id: "arcee-ai/virtuoso-large", + name: "Arcee AI: Virtuoso Large", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.75, + output: 1.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "auto": { + id: "auto", + name: "Auto", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 2000000, + maxTokens: 30000, + } satisfies Model<"openai-completions">, + "bytedance-seed/seed-1.6": { + id: "bytedance-seed/seed-1.6", + name: "ByteDance Seed: Seed 1.6", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "bytedance-seed/seed-1.6-flash": { + id: "bytedance-seed/seed-1.6-flash", + name: "ByteDance Seed: Seed 1.6 Flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.075, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "bytedance-seed/seed-2.0-lite": { + id: "bytedance-seed/seed-2.0-lite", + name: "ByteDance Seed: Seed-2.0-Lite", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "bytedance-seed/seed-2.0-mini": { + id: "bytedance-seed/seed-2.0-mini", + name: "ByteDance Seed: Seed-2.0-Mini", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.09999999999999999, + output: 0.39999999999999997, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "cohere/command-r-08-2024": { + id: "cohere/command-r-08-2024", + name: "Cohere: Command R (08-2024)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4000, + } satisfies Model<"openai-completions">, + "cohere/command-r-plus-08-2024": { + id: "cohere/command-r-plus-08-2024", + name: "Cohere: Command R+ (08-2024)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 2.5, + output: 10, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4000, + } satisfies Model<"openai-completions">, + "deepseek/deepseek-chat": { + id: "deepseek/deepseek-chat", + name: "DeepSeek: DeepSeek V3", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.20020000000000002, + output: 0.8000999999999999, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16000, + } satisfies Model<"openai-completions">, + "deepseek/deepseek-chat-v3-0324": { + id: "deepseek/deepseek-chat-v3-0324", + name: "DeepSeek: DeepSeek V3 0324", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.19999999999999998, + output: 0.77, + cacheRead: 0.135, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "deepseek/deepseek-chat-v3.1": { + id: "deepseek/deepseek-chat-v3.1", + name: "DeepSeek: DeepSeek V3.1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.21, + output: 0.7899999999999999, + cacheRead: 0.13, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "deepseek/deepseek-r1": { + id: "deepseek/deepseek-r1", + name: "DeepSeek: R1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.7, + output: 2.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 16000, + } satisfies Model<"openai-completions">, + "deepseek/deepseek-r1-0528": { + id: "deepseek/deepseek-r1-0528", + name: "DeepSeek: R1 0528", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.5, + output: 2.1500000000000004, + cacheRead: 0.35, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "deepseek/deepseek-v3.1-terminus": { + id: "deepseek/deepseek-v3.1-terminus", + name: "DeepSeek: DeepSeek V3.1 Terminus", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.27, + output: 0.95, + cacheRead: 0.13, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "deepseek/deepseek-v3.2": { + id: "deepseek/deepseek-v3.2", + name: "DeepSeek: DeepSeek V3.2", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.2288, + output: 0.3432, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "deepseek/deepseek-v3.2-exp": { + id: "deepseek/deepseek-v3.2-exp", + name: "DeepSeek: DeepSeek V3.2 Exp", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.27, + output: 0.41, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "deepseek/deepseek-v4-flash": { + id: "deepseek/deepseek-v4-flash", + name: "DeepSeek: DeepSeek V4 Flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh"}, + input: ["text"], + cost: { + input: 0.0983, + output: 0.1966, + cacheRead: 0.019700000000000002, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "deepseek/deepseek-v4-pro": { + id: "deepseek/deepseek-v4-pro", + name: "DeepSeek: DeepSeek V4 Pro", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh"}, + input: ["text"], + cost: { + input: 0.435, + output: 0.87, + cacheRead: 0.003625, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 384000, + } satisfies Model<"openai-completions">, + "essentialai/rnj-1-instruct": { + id: "essentialai/rnj-1-instruct", + name: "EssentialAI: Rnj 1 Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.15, + output: 0.15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 32768, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "google/gemini-2.5-flash": { + id: "google/gemini-2.5-flash", + name: "Google: Gemini 2.5 Flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 2.5, + cacheRead: 0.03, + cacheWrite: 0.08333333333333334, + }, + contextWindow: 1048576, + maxTokens: 65535, + } satisfies Model<"openai-completions">, + "google/gemini-2.5-flash-lite": { + id: "google/gemini-2.5-flash-lite", + name: "Google: Gemini 2.5 Flash Lite", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.09999999999999999, + output: 0.39999999999999997, + cacheRead: 0.01, + cacheWrite: 0.08333333333333334, + }, + contextWindow: 1048576, + maxTokens: 65535, + } satisfies Model<"openai-completions">, + "google/gemini-2.5-flash-lite-preview-09-2025": { + id: "google/gemini-2.5-flash-lite-preview-09-2025", + name: "Google: Gemini 2.5 Flash Lite Preview 09-2025", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.09999999999999999, + output: 0.39999999999999997, + cacheRead: 0.01, + cacheWrite: 0.08333333333333334, + }, + contextWindow: 1048576, + maxTokens: 65535, + } satisfies Model<"openai-completions">, + "google/gemini-2.5-pro": { + id: "google/gemini-2.5-pro", + name: "Google: Gemini 2.5 Pro", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0.375, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "google/gemini-2.5-pro-preview": { + id: "google/gemini-2.5-pro-preview", + name: "Google: Gemini 2.5 Pro Preview 06-05", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0.375, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "google/gemini-2.5-pro-preview-05-06": { + id: "google/gemini-2.5-pro-preview-05-06", + name: "Google: Gemini 2.5 Pro Preview 05-06", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0.375, + }, + contextWindow: 1048576, + maxTokens: 65535, + } satisfies Model<"openai-completions">, + "google/gemini-3-flash-preview": { + id: "google/gemini-3-flash-preview", + name: "Google: Gemini 3 Flash Preview", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0.049999999999999996, + cacheWrite: 0.08333333333333334, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "google/gemini-3.1-flash-lite": { + id: "google/gemini-3.1-flash-lite", + name: "Google: Gemini 3.1 Flash Lite", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.5, + cacheRead: 0.024999999999999998, + cacheWrite: 0.08333333333333334, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "google/gemini-3.1-flash-lite-preview": { + id: "google/gemini-3.1-flash-lite-preview", + name: "Google: Gemini 3.1 Flash Lite Preview", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.5, + cacheRead: 0.024999999999999998, + cacheWrite: 0.08333333333333334, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "google/gemini-3.1-pro-preview": { + id: "google/gemini-3.1-pro-preview", + name: "Google: Gemini 3.1 Pro Preview", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.19999999999999998, + cacheWrite: 0.375, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "google/gemini-3.1-pro-preview-customtools": { + id: "google/gemini-3.1-pro-preview-customtools", + name: "Google: Gemini 3.1 Pro Preview Custom Tools", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.19999999999999998, + cacheWrite: 0.375, + }, + contextWindow: 1048756, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "google/gemini-3.5-flash": { + id: "google/gemini-3.5-flash", + name: "Google: Gemini 3.5 Flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.5, + output: 9, + cacheRead: 0.15, + cacheWrite: 0.08333333333333334, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "google/gemma-3-12b-it": { + id: "google/gemma-3-12b-it", + name: "Google: Gemma 3 12B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.049999999999999996, + output: 0.15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "google/gemma-3-27b-it": { + id: "google/gemma-3-27b-it", + name: "Google: Gemma 3 27B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.08, + output: 0.16, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "google/gemma-4-26b-a4b-it": { + id: "google/gemma-4-26b-a4b-it", + name: "Google: Gemma 4 26B A4B ", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.06, + output: 0.33, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "google/gemma-4-26b-a4b-it:free": { + id: "google/gemma-4-26b-a4b-it:free", + name: "Google: Gemma 4 26B A4B (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "google/gemma-4-31b-it": { + id: "google/gemma-4-31b-it", + name: "Google: Gemma 4 31B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.12, + output: 0.36, + cacheRead: 0.09, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 8192, + } satisfies Model<"openai-completions">, + "google/gemma-4-31b-it:free": { + id: "google/gemma-4-31b-it:free", + name: "Google: Gemma 4 31B (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "ibm-granite/granite-4.1-8b": { + id: "ibm-granite/granite-4.1-8b", + name: "IBM: Granite 4.1 8B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.049999999999999996, + output: 0.09999999999999999, + cacheRead: 0.049999999999999996, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "inception/mercury-2": { + id: "inception/mercury-2", + name: "Inception: Mercury 2", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text"], + cost: { + input: 0.25, + output: 0.75, + cacheRead: 0.024999999999999998, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 50000, + } satisfies Model<"openai-completions">, + "inclusionai/ling-2.6-1t": { + id: "inclusionai/ling-2.6-1t", + name: "inclusionAI: Ling-2.6-1T", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.075, + output: 0.625, + cacheRead: 0.015, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "inclusionai/ling-2.6-flash": { + id: "inclusionai/ling-2.6-flash", + name: "inclusionAI: Ling-2.6-flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.01, + output: 0.03, + cacheRead: 0.002, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "inclusionai/ring-2.6-1t": { + id: "inclusionai/ring-2.6-1t", + name: "inclusionAI: Ring-2.6-1T", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.075, + output: 0.625, + cacheRead: 0.015, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "kwaipilot/kat-coder-pro-v2": { + id: "kwaipilot/kat-coder-pro-v2", + name: "Kwaipilot: KAT-Coder-Pro V2", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 80000, + } satisfies Model<"openai-completions">, + "meta-llama/llama-3.1-70b-instruct": { + id: "meta-llama/llama-3.1-70b-instruct", + name: "Meta: Llama 3.1 70B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.39999999999999997, + output: 0.39999999999999997, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "meta-llama/llama-3.1-8b-instruct": { + id: "meta-llama/llama-3.1-8b-instruct", + name: "Meta: Llama 3.1 8B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.02, + output: 0.03, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "meta-llama/llama-3.3-70b-instruct": { + id: "meta-llama/llama-3.3-70b-instruct", + name: "Meta: Llama 3.3 70B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.09999999999999999, + output: 0.32, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "meta-llama/llama-3.3-70b-instruct:free": { + id: "meta-llama/llama-3.3-70b-instruct:free", + name: "Meta: Llama 3.3 70B Instruct (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "meta-llama/llama-4-maverick": { + id: "meta-llama/llama-4-maverick", + name: "Meta: Llama 4 Maverick", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "meta-llama/llama-4-scout": { + id: "meta-llama/llama-4-scout", + name: "Meta: Llama 4 Scout", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.09999999999999999, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 10000000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "minimax/minimax-m1": { + id: "minimax/minimax-m1", + name: "MiniMax: MiniMax M1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.39999999999999997, + output: 2.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 40000, + } satisfies Model<"openai-completions">, + "minimax/minimax-m2": { + id: "minimax/minimax-m2", + name: "MiniMax: MiniMax M2", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.255, + output: 1, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 196608, + } satisfies Model<"openai-completions">, + "minimax/minimax-m2.1": { + id: "minimax/minimax-m2.1", + name: "MiniMax: MiniMax M2.1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.29, + output: 0.95, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 196608, + } satisfies Model<"openai-completions">, + "minimax/minimax-m2.5": { + id: "minimax/minimax-m2.5", + name: "MiniMax: MiniMax M2.5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.15, + output: 0.8999999999999999, + cacheRead: 0.049999999999999996, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 196608, + } satisfies Model<"openai-completions">, + "minimax/minimax-m2.7": { + id: "minimax/minimax-m2.7", + name: "MiniMax: MiniMax M2.7", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.27, + output: 1.08, + cacheRead: 0.054, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "minimax/minimax-m3": { + id: "minimax/minimax-m3", + name: "MiniMax: MiniMax M3", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 512000, + } satisfies Model<"openai-completions">, + "mistralai/codestral-2508": { + id: "mistralai/codestral-2508", + name: "Mistral: Codestral 2508", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.3, + output: 0.8999999999999999, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/devstral-2512": { + id: "mistralai/devstral-2512", + name: "Mistral: Devstral 2 2512", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.39999999999999997, + output: 2, + cacheRead: 0.04, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/ministral-14b-2512": { + id: "mistralai/ministral-14b-2512", + name: "Mistral: Ministral 3 14B 2512", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.19999999999999998, + output: 0.19999999999999998, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/ministral-3b-2512": { + id: "mistralai/ministral-3b-2512", + name: "Mistral: Ministral 3 3B 2512", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.09999999999999999, + output: 0.09999999999999999, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/ministral-8b-2512": { + id: "mistralai/ministral-8b-2512", + name: "Mistral: Ministral 3 8B 2512", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.15, + cacheRead: 0.015, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/mistral-large": { + id: "mistralai/mistral-large", + name: "Mistral Large", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 2, + output: 6, + cacheRead: 0.19999999999999998, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/mistral-large-2407": { + id: "mistralai/mistral-large-2407", + name: "Mistral Large 2407", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 2, + output: 6, + cacheRead: 0.19999999999999998, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/mistral-large-2512": { + id: "mistralai/mistral-large-2512", + name: "Mistral: Mistral Large 3 2512", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.5, + output: 1.5, + cacheRead: 0.049999999999999996, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/mistral-medium-3": { + id: "mistralai/mistral-medium-3", + name: "Mistral: Mistral Medium 3", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.39999999999999997, + output: 2, + cacheRead: 0.04, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/mistral-medium-3-5": { + id: "mistralai/mistral-medium-3-5", + name: "Mistral: Mistral Medium 3.5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.5, + output: 7.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/mistral-medium-3.1": { + id: "mistralai/mistral-medium-3.1", + name: "Mistral: Mistral Medium 3.1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.39999999999999997, + output: 2, + cacheRead: 0.04, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/mistral-nemo": { + id: "mistralai/mistral-nemo", + name: "Mistral: Mistral Nemo", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.02, + output: 0.03, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/mistral-saba": { + id: "mistralai/mistral-saba", + name: "Mistral: Saba", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.19999999999999998, + output: 0.6, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 32768, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/mistral-small-2603": { + id: "mistralai/mistral-small-2603", + name: "Mistral: Mistral Small 4", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.015, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/mistral-small-3.2-24b-instruct": { + id: "mistralai/mistral-small-3.2-24b-instruct", + name: "Mistral: Mistral Small 3.2 24B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.075, + output: 0.19999999999999998, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "mistralai/mixtral-8x22b-instruct": { + id: "mistralai/mixtral-8x22b-instruct", + name: "Mistral: Mixtral 8x22B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 2, + output: 6, + cacheRead: 0.19999999999999998, + cacheWrite: 0, + }, + contextWindow: 65536, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/voxtral-small-24b-2507": { + id: "mistralai/voxtral-small-24b-2507", + name: "Mistral: Voxtral Small 24B 2507", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.09999999999999999, + output: 0.3, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 32000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "moonshotai/kimi-k2": { + id: "moonshotai/kimi-k2", + name: "MoonshotAI: Kimi K2 0711", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.5700000000000001, + output: 2.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "moonshotai/kimi-k2-0905": { + id: "moonshotai/kimi-k2-0905", + name: "MoonshotAI: Kimi K2 0905", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.6, + output: 2.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "moonshotai/kimi-k2-thinking": { + id: "moonshotai/kimi-k2-thinking", + name: "MoonshotAI: Kimi K2 Thinking", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "moonshotai/kimi-k2.5": { + id: "moonshotai/kimi-k2.5", + name: "MoonshotAI: Kimi K2.5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.41, + output: 2.06, + cacheRead: 0.07, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "moonshotai/kimi-k2.6": { + id: "moonshotai/kimi-k2.6", + name: "MoonshotAI: Kimi K2.6", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"requiresReasoningContentOnAssistantMessages":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6799999999999999, + output: 3.41, + cacheRead: 0.33999999999999997, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262142, + } satisfies Model<"openai-completions">, + "nex-agi/nex-n2-pro:free": { + id: "nex-agi/nex-n2-pro:free", + name: "Nex AGI: Nex-N2-Pro (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "nvidia/llama-3.3-nemotron-super-49b-v1.5": { + id: "nvidia/llama-3.3-nemotron-super-49b-v1.5", + name: "NVIDIA: Llama 3.3 Nemotron Super 49B V1.5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.39999999999999997, + output: 0.39999999999999997, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-nano-30b-a3b": { + id: "nvidia/nemotron-3-nano-30b-a3b", + name: "NVIDIA: Nemotron 3 Nano 30B A3B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.049999999999999996, + output: 0.19999999999999998, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 228000, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-nano-30b-a3b:free": { + id: "nvidia/nemotron-3-nano-30b-a3b:free", + name: "NVIDIA: Nemotron 3 Nano 30B A3B (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free": { + id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free", + name: "NVIDIA: Nemotron 3 Nano Omni (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-super-120b-a12b": { + id: "nvidia/nemotron-3-super-120b-a12b", + name: "NVIDIA: Nemotron 3 Super", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.09, + output: 0.44999999999999996, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-super-120b-a12b:free": { + id: "nvidia/nemotron-3-super-120b-a12b:free", + name: "NVIDIA: Nemotron 3 Super (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-ultra-550b-a55b": { + id: "nvidia/nemotron-3-ultra-550b-a55b", + name: "NVIDIA: Nemotron 3 Ultra", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.5, + output: 2.5, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-ultra-550b-a55b:free": { + id: "nvidia/nemotron-3-ultra-550b-a55b:free", + name: "NVIDIA: Nemotron 3 Ultra (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-nano-12b-v2-vl:free": { + id: "nvidia/nemotron-nano-12b-v2-vl:free", + name: "NVIDIA: Nemotron Nano 12B 2 VL (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-nano-9b-v2": { + id: "nvidia/nemotron-nano-9b-v2", + name: "NVIDIA: Nemotron Nano 9B V2", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.04, + output: 0.16, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-nano-9b-v2:free": { + id: "nvidia/nemotron-nano-9b-v2:free", + name: "NVIDIA: Nemotron Nano 9B V2 (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-3.5-turbo": { + id: "openai/gpt-3.5-turbo", + name: "OpenAI: GPT-3.5 Turbo", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.5, + output: 1.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 16385, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-3.5-turbo-0613": { + id: "openai/gpt-3.5-turbo-0613", + name: "OpenAI: GPT-3.5 Turbo (older v0613)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 1, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 4095, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-3.5-turbo-16k": { + id: "openai/gpt-3.5-turbo-16k", + name: "OpenAI: GPT-3.5 Turbo 16k", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 3, + output: 4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 16385, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-4": { + id: "openai/gpt-4", + name: "OpenAI: GPT-4", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 30, + output: 60, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 8191, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-4-turbo": { + id: "openai/gpt-4-turbo", + name: "OpenAI: GPT-4 Turbo", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 10, + output: 30, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-4-turbo-preview": { + id: "openai/gpt-4-turbo-preview", + name: "OpenAI: GPT-4 Turbo Preview", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 10, + output: 30, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-4.1": { + id: "openai/gpt-4.1", + name: "OpenAI: GPT-4.1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-4.1-mini": { + id: "openai/gpt-4.1-mini", + name: "OpenAI: GPT-4.1 Mini", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.39999999999999997, + output: 1.5999999999999999, + cacheRead: 0.09999999999999999, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "openai/gpt-4.1-nano": { + id: "openai/gpt-4.1-nano", + name: "OpenAI: GPT-4.1 Nano", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.09999999999999999, + output: 0.39999999999999997, + cacheRead: 0.024999999999999998, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "openai/gpt-4o": { + id: "openai/gpt-4o", + name: "OpenAI: GPT-4o", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 10, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "openai/gpt-4o-2024-05-13": { + id: "openai/gpt-4o-2024-05-13", + name: "OpenAI: GPT-4o (2024-05-13)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 5, + output: 15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-4o-2024-08-06": { + id: "openai/gpt-4o-2024-08-06", + name: "OpenAI: GPT-4o (2024-08-06)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 10, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "openai/gpt-4o-2024-11-20": { + id: "openai/gpt-4o-2024-11-20", + name: "OpenAI: GPT-4o (2024-11-20)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 10, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "openai/gpt-4o-mini": { + id: "openai/gpt-4o-mini", + name: "OpenAI: GPT-4o-mini", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "openai/gpt-4o-mini-2024-07-18": { + id: "openai/gpt-4o-mini-2024-07-18", + name: "OpenAI: GPT-4o-mini (2024-07-18)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "openai/gpt-5": { + id: "openai/gpt-5", + name: "OpenAI: GPT-5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5-codex": { + id: "openai/gpt-5-codex", + name: "OpenAI: GPT-5 Codex", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5-mini": { + id: "openai/gpt-5-mini", + name: "OpenAI: GPT-5 Mini", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.024999999999999998, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5-nano": { + id: "openai/gpt-5-nano", + name: "OpenAI: GPT-5 Nano", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.049999999999999996, + output: 0.39999999999999997, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-5-pro": { + id: "openai/gpt-5-pro", + name: "OpenAI: GPT-5 Pro", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 120, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.1": { + id: "openai/gpt-5.1", + name: "OpenAI: GPT-5.1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.13, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.1-chat": { + id: "openai/gpt-5.1-chat", + name: "OpenAI: GPT-5.1 Chat", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.13, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 32000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.1-codex": { + id: "openai/gpt-5.1-codex", + name: "OpenAI: GPT-5.1-Codex", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.13, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.1-codex-max": { + id: "openai/gpt-5.1-codex-max", + name: "OpenAI: GPT-5.1-Codex-Max", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.1-codex-mini": { + id: "openai/gpt-5.1-codex-mini", + name: "OpenAI: GPT-5.1-Codex-Mini", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.024999999999999998, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 100000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.2": { + id: "openai/gpt-5.2", + name: "OpenAI: GPT-5.2", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.2-chat": { + id: "openai/gpt-5.2-chat", + name: "OpenAI: GPT-5.2 Chat", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "openai/gpt-5.2-codex": { + id: "openai/gpt-5.2-codex", + name: "OpenAI: GPT-5.2-Codex", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.2-pro": { + id: "openai/gpt-5.2-pro", + name: "OpenAI: GPT-5.2 Pro", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 21, + output: 168, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.3-chat": { + id: "openai/gpt-5.3-chat", + name: "OpenAI: GPT-5.3 Chat", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "openai/gpt-5.3-codex": { + id: "openai/gpt-5.3-codex", + name: "OpenAI: GPT-5.3-Codex", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.4": { + id: "openai/gpt-5.4", + name: "OpenAI: GPT-5.4", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 2.5, + output: 15, + cacheRead: 0.25, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.4-mini": { + id: "openai/gpt-5.4-mini", + name: "OpenAI: GPT-5.4 Mini", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.75, + output: 4.5, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.4-nano": { + id: "openai/gpt-5.4-nano", + name: "OpenAI: GPT-5.4 Nano", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.19999999999999998, + output: 1.25, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.4-pro": { + id: "openai/gpt-5.4-pro", + name: "OpenAI: GPT-5.4 Pro", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 30, + output: 180, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.5": { + id: "openai/gpt-5.5", + name: "OpenAI: GPT-5.5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.5-pro": { + id: "openai/gpt-5.5-pro", + name: "OpenAI: GPT-5.5 Pro", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","off":null,"minimal":null,"low":null}, + input: ["text", "image"], + cost: { + input: 30, + output: 180, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-audio": { + id: "openai/gpt-audio", + name: "OpenAI: GPT Audio", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 2.5, + output: 10, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "openai/gpt-audio-mini": { + id: "openai/gpt-audio-mini", + name: "OpenAI: GPT Audio Mini", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.6, + output: 2.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "openai/gpt-chat-latest": { + id: "openai/gpt-chat-latest", + name: "OpenAI: GPT Chat Latest", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-oss-120b": { + id: "openai/gpt-oss-120b", + name: "OpenAI: gpt-oss-120b", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.039, + output: 0.18, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-oss-120b:free": { + id: "openai/gpt-oss-120b:free", + name: "OpenAI: gpt-oss-120b (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "openai/gpt-oss-20b": { + id: "openai/gpt-oss-20b", + name: "OpenAI: gpt-oss-20b", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.029, + output: 0.14, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-oss-20b:free": { + id: "openai/gpt-oss-20b:free", + name: "OpenAI: gpt-oss-20b (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 8192, + } satisfies Model<"openai-completions">, + "openai/gpt-oss-safeguard-20b": { + id: "openai/gpt-oss-safeguard-20b", + name: "OpenAI: gpt-oss-safeguard-20b", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.075, + output: 0.3, + cacheRead: 0.037, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "openai/o1": { + id: "openai/o1", + name: "OpenAI: o1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 60, + cacheRead: 7.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-completions">, + "openai/o3": { + id: "openai/o3", + name: "OpenAI: o3", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-completions">, + "openai/o3-deep-research": { + id: "openai/o3-deep-research", + name: "OpenAI: o3 Deep Research", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 10, + output: 40, + cacheRead: 2.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-completions">, + "openai/o3-mini": { + id: "openai/o3-mini", + name: "OpenAI: o3 Mini", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.55, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-completions">, + "openai/o3-mini-high": { + id: "openai/o3-mini-high", + name: "OpenAI: o3 Mini High", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.55, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-completions">, + "openai/o3-pro": { + id: "openai/o3-pro", + name: "OpenAI: o3 Pro", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 20, + output: 80, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-completions">, + "openai/o4-mini": { + id: "openai/o4-mini", + name: "OpenAI: o4 Mini", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.275, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-completions">, + "openai/o4-mini-deep-research": { + id: "openai/o4-mini-deep-research", + name: "OpenAI: o4 Mini Deep Research", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-completions">, + "openai/o4-mini-high": { + id: "openai/o4-mini-high", + name: "OpenAI: o4 Mini High", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.275, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-completions">, + "openrouter/auto": { + id: "openrouter/auto", + name: "Auto Router", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: -1000000, + output: -1000000, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 2000000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openrouter/free": { + id: "openrouter/free", + name: "Free Models Router", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openrouter/owl-alpha": { + id: "openrouter/owl-alpha", + name: "Owl Alpha", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1048756, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "poolside/laguna-m.1:free": { + id: "poolside/laguna-m.1:free", + name: "Poolside: Laguna M.1 (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "poolside/laguna-xs.2:free": { + id: "poolside/laguna-xs.2:free", + name: "Poolside: Laguna XS.2 (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "prime-intellect/intellect-3": { + id: "prime-intellect/intellect-3", + name: "Prime Intellect: INTELLECT-3", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.19999999999999998, + output: 1.1, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "qwen/qwen-2.5-72b-instruct": { + id: "qwen/qwen-2.5-72b-instruct", + name: "Qwen2.5 72B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.36, + output: 0.39999999999999997, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "qwen/qwen-plus": { + id: "qwen/qwen-plus", + name: "Qwen: Qwen-Plus", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.26, + output: 0.78, + cacheRead: 0.052000000000000005, + cacheWrite: 0.325, + }, + contextWindow: 1000000, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen-plus-2025-07-28": { + id: "qwen/qwen-plus-2025-07-28", + name: "Qwen: Qwen Plus 0728", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.26, + output: 0.78, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen-plus-2025-07-28:thinking": { + id: "qwen/qwen-plus-2025-07-28:thinking", + name: "Qwen: Qwen Plus 0728 (thinking)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.26, + output: 0.78, + cacheRead: 0, + cacheWrite: 0.325, + }, + contextWindow: 1000000, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3-14b": { + id: "qwen/qwen3-14b", + name: "Qwen: Qwen3 14B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.09999999999999999, + output: 0.24, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131702, + maxTokens: 40960, + } satisfies Model<"openai-completions">, + "qwen/qwen3-235b-a22b": { + id: "qwen/qwen3-235b-a22b", + name: "Qwen: Qwen3 235B A22B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.45499999999999996, + output: 1.8199999999999998, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 8192, + } satisfies Model<"openai-completions">, + "qwen/qwen3-235b-a22b-2507": { + id: "qwen/qwen3-235b-a22b-2507", + name: "Qwen: Qwen3 235B A22B Instruct 2507", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.09, + output: 0.09999999999999999, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "qwen/qwen3-235b-a22b-thinking-2507": { + id: "qwen/qwen3-235b-a22b-thinking-2507", + name: "Qwen: Qwen3 235B A22B Thinking 2507", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.09999999999999999, + output: 0.09999999999999999, + cacheRead: 0.09999999999999999, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "qwen/qwen3-30b-a3b": { + id: "qwen/qwen3-30b-a3b", + name: "Qwen: Qwen3 30B A3B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.12, + output: 0.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "qwen/qwen3-30b-a3b-instruct-2507": { + id: "qwen/qwen3-30b-a3b-instruct-2507", + name: "Qwen: Qwen3 30B A3B Instruct 2507", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.04815, + output: 0.19305, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32000, + } satisfies Model<"openai-completions">, + "qwen/qwen3-30b-a3b-thinking-2507": { + id: "qwen/qwen3-30b-a3b-thinking-2507", + name: "Qwen: Qwen3 30B A3B Thinking 2507", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.08, + output: 0.39999999999999997, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "qwen/qwen3-32b": { + id: "qwen/qwen3-32b", + name: "Qwen: Qwen3 32B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.08, + output: 0.28, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "qwen/qwen3-8b": { + id: "qwen/qwen3-8b", + name: "Qwen: Qwen3 8B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.049999999999999996, + output: 0.39999999999999997, + cacheRead: 0.049999999999999996, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 8192, + } satisfies Model<"openai-completions">, + "qwen/qwen3-coder": { + id: "qwen/qwen3-coder", + name: "Qwen: Qwen3 Coder 480B A35B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.22, + output: 1.7999999999999998, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3-coder-30b-a3b-instruct": { + id: "qwen/qwen3-coder-30b-a3b-instruct", + name: "Qwen: Qwen3 Coder 30B A3B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.07, + output: 0.27, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 160000, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3-coder-flash": { + id: "qwen/qwen3-coder-flash", + name: "Qwen: Qwen3 Coder Flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.195, + output: 0.975, + cacheRead: 0.039, + cacheWrite: 0.24375, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3-coder-next": { + id: "qwen/qwen3-coder-next", + name: "Qwen: Qwen3 Coder Next", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.11, + output: 0.7999999999999999, + cacheRead: 0.07, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "qwen/qwen3-coder-plus": { + id: "qwen/qwen3-coder-plus", + name: "Qwen: Qwen3 Coder Plus", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.65, + output: 3.25, + cacheRead: 0.13, + cacheWrite: 0.8125, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3-coder:free": { + id: "qwen/qwen3-coder:free", + name: "Qwen: Qwen3 Coder 480B A35B (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 262000, + } satisfies Model<"openai-completions">, + "qwen/qwen3-max": { + id: "qwen/qwen3-max", + name: "Qwen: Qwen3 Max", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.78, + output: 3.9, + cacheRead: 0.156, + cacheWrite: 0.975, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3-max-thinking": { + id: "qwen/qwen3-max-thinking", + name: "Qwen: Qwen3 Max Thinking", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.78, + output: 3.9, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3-next-80b-a3b-instruct": { + id: "qwen/qwen3-next-80b-a3b-instruct", + name: "Qwen: Qwen3 Next 80B A3B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.09, + output: 1.1, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "qwen/qwen3-next-80b-a3b-instruct:free": { + id: "qwen/qwen3-next-80b-a3b-instruct:free", + name: "Qwen: Qwen3 Next 80B A3B Instruct (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "qwen/qwen3-next-80b-a3b-thinking": { + id: "qwen/qwen3-next-80b-a3b-thinking", + name: "Qwen: Qwen3 Next 80B A3B Thinking", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.0975, + output: 0.78, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3-vl-235b-a22b-instruct": { + id: "qwen/qwen3-vl-235b-a22b-instruct", + name: "Qwen: Qwen3 VL 235B A22B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.19999999999999998, + output: 0.88, + cacheRead: 0.11, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "qwen/qwen3-vl-235b-a22b-thinking": { + id: "qwen/qwen3-vl-235b-a22b-thinking", + name: "Qwen: Qwen3 VL 235B A22B Thinking", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.26, + output: 2.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3-vl-30b-a3b-instruct": { + id: "qwen/qwen3-vl-30b-a3b-instruct", + name: "Qwen: Qwen3 VL 30B A3B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.13, + output: 0.52, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3-vl-30b-a3b-thinking": { + id: "qwen/qwen3-vl-30b-a3b-thinking", + name: "Qwen: Qwen3 VL 30B A3B Thinking", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.13, + output: 1.56, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3-vl-32b-instruct": { + id: "qwen/qwen3-vl-32b-instruct", + name: "Qwen: Qwen3 VL 32B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.10400000000000001, + output: 0.41600000000000004, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3-vl-8b-instruct": { + id: "qwen/qwen3-vl-8b-instruct", + name: "Qwen: Qwen3 VL 8B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.08, + output: 0.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3-vl-8b-thinking": { + id: "qwen/qwen3-vl-8b-thinking", + name: "Qwen: Qwen3 VL 8B Thinking", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.117, + output: 1.365, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3.5-122b-a10b": { + id: "qwen/qwen3.5-122b-a10b", + name: "Qwen: Qwen3.5-122B-A10B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.26, + output: 2.08, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "qwen/qwen3.5-27b": { + id: "qwen/qwen3.5-27b", + name: "Qwen: Qwen3.5-27B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.195, + output: 1.56, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3.5-35b-a3b": { + id: "qwen/qwen3.5-35b-a3b", + name: "Qwen: Qwen3.5-35B-A3B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.14, + output: 1, + cacheRead: 0.049999999999999996, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "qwen/qwen3.5-397b-a17b": { + id: "qwen/qwen3.5-397b-a17b", + name: "Qwen: Qwen3.5 397B A17B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.39, + output: 2.34, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3.5-9b": { + id: "qwen/qwen3.5-9b", + name: "Qwen: Qwen3.5-9B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.09999999999999999, + output: 0.15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "qwen/qwen3.5-flash-02-23": { + id: "qwen/qwen3.5-flash-02-23", + name: "Qwen: Qwen3.5-Flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.065, + output: 0.26, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3.5-plus-02-15": { + id: "qwen/qwen3.5-plus-02-15", + name: "Qwen: Qwen3.5 Plus 2026-02-15", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.26, + output: 1.56, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3.5-plus-20260420": { + id: "qwen/qwen3.5-plus-20260420", + name: "Qwen: Qwen3.5 Plus 2026-04-20", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 1.7999999999999998, + cacheRead: 0, + cacheWrite: 0.375, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3.6-27b": { + id: "qwen/qwen3.6-27b", + name: "Qwen: Qwen3.6 27B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.28900000000000003, + output: 2.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "qwen/qwen3.6-35b-a3b": { + id: "qwen/qwen3.6-35b-a3b", + name: "Qwen: Qwen3.6 35B A3B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.14, + output: 1, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262140, + } satisfies Model<"openai-completions">, + "qwen/qwen3.6-flash": { + id: "qwen/qwen3.6-flash", + name: "Qwen: Qwen3.6 Flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.1875, + output: 1.125, + cacheRead: 0, + cacheWrite: 0.234375, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3.6-max-preview": { + id: "qwen/qwen3.6-max-preview", + name: "Qwen: Qwen3.6 Max Preview", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 1.04, + output: 6.24, + cacheRead: 0, + cacheWrite: 1.3, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3.6-plus": { + id: "qwen/qwen3.6-plus", + name: "Qwen: Qwen3.6 Plus", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.325, + output: 1.95, + cacheRead: 0, + cacheWrite: 0.40625, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3.7-max": { + id: "qwen/qwen3.7-max", + name: "Qwen: Qwen3.7 Max", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 1.25, + output: 3.75, + cacheRead: 0.25, + cacheWrite: 1.5625, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3.7-plus": { + id: "qwen/qwen3.7-plus", + name: "Qwen: Qwen3.7 Plus", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.39999999999999997, + output: 1.5999999999999999, + cacheRead: 0.08, + cacheWrite: 0.5, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "rekaai/reka-edge": { + id: "rekaai/reka-edge", + name: "Reka Edge", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.09999999999999999, + output: 0.09999999999999999, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 16384, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "relace/relace-search": { + id: "relace/relace-search", + name: "Relace: Relace Search", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "sao10k/l3.1-euryale-70b": { + id: "sao10k/l3.1-euryale-70b", + name: "Sao10K: Llama 3.1 Euryale 70B v2.2", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.85, + output: 0.85, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "stepfun/step-3.5-flash": { + id: "stepfun/step-3.5-flash", + name: "StepFun: Step 3.5 Flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.09, + output: 0.3, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "stepfun/step-3.7-flash": { + id: "stepfun/step-3.7-flash", + name: "StepFun: Step 3.7 Flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.19999999999999998, + output: 1.15, + cacheRead: 0.04, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"openai-completions">, + "tencent/hy3-preview": { + id: "tencent/hy3-preview", + name: "Tencent: Hy3 preview", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.063, + output: 0.21, + cacheRead: 0.020999999999999998, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "thedrummer/rocinante-12b": { + id: "thedrummer/rocinante-12b", + name: "TheDrummer: Rocinante 12B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.16999999999999998, + output: 0.43, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 32768, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "thedrummer/unslopnemo-12b": { + id: "thedrummer/unslopnemo-12b", + name: "TheDrummer: UnslopNemo 12B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.39999999999999997, + output: 0.39999999999999997, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 32768, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "upstage/solar-pro-3": { + id: "upstage/solar-pro-3", + name: "Upstage: Solar Pro 3", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.015, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "x-ai/grok-4.20": { + id: "x-ai/grok-4.20", + name: "xAI: Grok 4.20", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.19999999999999998, + cacheWrite: 0, + }, + contextWindow: 2000000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "x-ai/grok-4.3": { + id: "x-ai/grok-4.3", + name: "xAI: Grok 4.3", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.19999999999999998, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "x-ai/grok-build-0.1": { + id: "x-ai/grok-build-0.1", + name: "xAI: Grok Build 0.1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 2, + cacheRead: 0.19999999999999998, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "xiaomi/mimo-v2-flash": { + id: "xiaomi/mimo-v2-flash", + name: "Xiaomi: MiMo-V2-Flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.09999999999999999, + output: 0.3, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "xiaomi/mimo-v2.5": { + id: "xiaomi/mimo-v2.5", + name: "Xiaomi: MiMo-V2.5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.14, + output: 0.28, + cacheRead: 0.0028, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "xiaomi/mimo-v2.5-pro": { + id: "xiaomi/mimo-v2.5-pro", + name: "Xiaomi: MiMo-V2.5-Pro", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.435, + output: 0.87, + cacheRead: 0.0036, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "z-ai/glm-4.5": { + id: "z-ai/glm-4.5", + name: "Z.ai: GLM 4.5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.2, + cacheRead: 0.11, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 98304, + } satisfies Model<"openai-completions">, + "z-ai/glm-4.5-air": { + id: "z-ai/glm-4.5-air", + name: "Z.ai: GLM 4.5 Air", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.125, + output: 0.85, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131070, + } satisfies Model<"openai-completions">, + "z-ai/glm-4.5v": { + id: "z-ai/glm-4.5v", + name: "Z.ai: GLM 4.5V", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 1.7999999999999998, + cacheRead: 0.11, + cacheWrite: 0, + }, + contextWindow: 65536, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "z-ai/glm-4.6": { + id: "z-ai/glm-4.6", + name: "Z.ai: GLM 4.6", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.43, + output: 1.74, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "z-ai/glm-4.6v": { + id: "z-ai/glm-4.6v", + name: "Z.ai: GLM 4.6V", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 0.8999999999999999, + cacheRead: 0.055, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "z-ai/glm-4.7": { + id: "z-ai/glm-4.7", + name: "Z.ai: GLM 4.7", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.39999999999999997, + output: 1.75, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "z-ai/glm-4.7-flash": { + id: "z-ai/glm-4.7-flash", + name: "Z.ai: GLM 4.7 Flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.06, + output: 0.39999999999999997, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "z-ai/glm-5": { + id: "z-ai/glm-5", + name: "Z.ai: GLM 5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 1.9, + cacheRead: 0.119, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "z-ai/glm-5-turbo": { + id: "z-ai/glm-5-turbo", + name: "Z.ai: GLM 5 Turbo", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 1.2, + output: 4, + cacheRead: 0.24, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "z-ai/glm-5.1": { + id: "z-ai/glm-5.1", + name: "Z.ai: GLM 5.1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.98, + output: 3.08, + cacheRead: 0.182, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "~anthropic/claude-fable-latest": { + id: "~anthropic/claude-fable-latest", + name: "Anthropic: Claude Fable Latest", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 1, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "~anthropic/claude-haiku-latest": { + id: "~anthropic/claude-haiku-latest", + name: "Anthropic Claude Haiku Latest", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.09999999999999999, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "~anthropic/claude-opus-latest": { + id: "~anthropic/claude-opus-latest", + name: "Anthropic: Claude Opus Latest", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "~anthropic/claude-sonnet-latest": { + id: "~anthropic/claude-sonnet-latest", + name: "Anthropic Claude Sonnet Latest", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "~google/gemini-flash-latest": { + id: "~google/gemini-flash-latest", + name: "Google Gemini Flash Latest", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.5, + output: 9, + cacheRead: 0.15, + cacheWrite: 0.08333333333333334, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "~google/gemini-pro-latest": { + id: "~google/gemini-pro-latest", + name: "Google Gemini Pro Latest", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.19999999999999998, + cacheWrite: 0.375, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "~moonshotai/kimi-latest": { + id: "~moonshotai/kimi-latest", + name: "MoonshotAI Kimi Latest", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6799999999999999, + output: 3.41, + cacheRead: 0.33999999999999997, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262142, + } satisfies Model<"openai-completions">, + "~openai/gpt-latest": { + id: "~openai/gpt-latest", + name: "OpenAI GPT Latest", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "~openai/gpt-mini-latest": { + id: "~openai/gpt-mini-latest", + name: "OpenAI GPT Mini Latest", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.75, + output: 4.5, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/openrouter.ts b/packages/ai/src/providers/openrouter.ts new file mode 100644 index 00000000..8c3f254d --- /dev/null +++ b/packages/ai/src/providers/openrouter.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { OPENROUTER_MODELS } from "./openrouter.models.ts"; + +export function openrouterProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "openrouter", + name: "OpenRouter", + baseUrl: "https://openrouter.ai/api/v1", + auth: { apiKey: envApiKeyAuth("OpenRouter API key", ["OPENROUTER_API_KEY"]) }, + models: Object.values(OPENROUTER_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/together.models.ts b/packages/ai/src/providers/together.models.ts new file mode 100644 index 00000000..350f87f4 --- /dev/null +++ b/packages/ai/src/providers/together.models.ts @@ -0,0 +1,365 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const TOGETHER_MODELS = { + "MiniMaxAI/MiniMax-M2.5": { + id: "MiniMaxAI/MiniMax-M2.5", + name: "MiniMax-M2.5", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "MiniMaxAI/MiniMax-M2.7": { + id: "MiniMaxAI/MiniMax-M2.7", + name: "MiniMax-M2.7", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null}, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3-235B-A22B-Instruct-2507-tput": { + id: "Qwen/Qwen3-235B-A22B-Instruct-2507-tput", + name: "Qwen3 235B A22B Instruct 2507 FP8", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text"], + cost: { + input: 0.2, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8": { + id: "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8", + name: "Qwen3 Coder 480B A35B Instruct", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, + input: ["text"], + cost: { + input: 2, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3-Coder-Next-FP8": { + id: "Qwen/Qwen3-Coder-Next-FP8", + name: "Qwen3 Coder Next FP8", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text"], + cost: { + input: 0.5, + output: 1.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3.5-397B-A17B": { + id: "Qwen/Qwen3.5-397B-A17B", + name: "Qwen3.5 397B A17B", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text", "image"], + cost: { + input: 0.6, + output: 3.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 130000, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3.6-Plus": { + id: "Qwen/Qwen3.6-Plus", + name: "Qwen3.6 Plus", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 500000, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3.7-Max": { + id: "Qwen/Qwen3.7-Max", + name: "Qwen3.7 Max", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text"], + cost: { + input: 2.5, + output: 7.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 500000, + } satisfies Model<"openai-completions">, + "deepseek-ai/DeepSeek-V3": { + id: "deepseek-ai/DeepSeek-V3", + name: "DeepSeek-V3", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text"], + cost: { + input: 1.25, + output: 1.25, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "deepseek-ai/DeepSeek-V3-1": { + id: "deepseek-ai/DeepSeek-V3-1", + name: "DeepSeek V3.1", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text"], + cost: { + input: 0.6, + output: 1.7, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "deepseek-ai/DeepSeek-V4-Pro": { + id: "deepseek-ai/DeepSeek-V4-Pro", + name: "DeepSeek V4 Pro", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":null}, + input: ["text"], + cost: { + input: 2.1, + output: 4.4, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 512000, + maxTokens: 384000, + } satisfies Model<"openai-completions">, + "essentialai/Rnj-1-Instruct": { + id: "essentialai/Rnj-1-Instruct", + name: "Rnj-1 Instruct", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0.15, + output: 0.15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 32768, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "google/gemma-4-31B-it": { + id: "google/gemma-4-31B-it", + name: "Gemma 4 31B Instruct", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text", "image"], + cost: { + input: 0.2, + output: 0.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "meta-llama/Llama-3.3-70B-Instruct-Turbo": { + id: "meta-llama/Llama-3.3-70B-Instruct-Turbo", + name: "Llama 3.3 70B", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0.88, + output: 0.88, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "moonshotai/Kimi-K2.5": { + id: "moonshotai/Kimi-K2.5", + name: "Kimi K2.5", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text", "image"], + cost: { + input: 0.5, + output: 2.8, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "moonshotai/Kimi-K2.6": { + id: "moonshotai/Kimi-K2.6", + name: "Kimi K2.6", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text", "image"], + cost: { + input: 1.2, + output: 4.5, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131000, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-ultra-550b-a55b": { + id: "nvidia/nemotron-3-ultra-550b-a55b", + name: "Nemotron 3 Ultra 550B A55B", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text"], + cost: { + input: 0.6, + output: 3.6, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 512300, + maxTokens: 512300, + } satisfies Model<"openai-completions">, + "openai/gpt-oss-120b": { + id: "openai/gpt-oss-120b", + name: "GPT OSS 120B", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"openai"}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null}, + input: ["text"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "zai-org/GLM-5.1": { + id: "zai-org/GLM-5.1", + name: "GLM-5.1", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 131072, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/together.ts b/packages/ai/src/providers/together.ts new file mode 100644 index 00000000..36631d11 --- /dev/null +++ b/packages/ai/src/providers/together.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { TOGETHER_MODELS } from "./together.models.ts"; + +export function togetherProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "together", + name: "Together", + baseUrl: "https://api.together.ai/v1", + auth: { apiKey: envApiKeyAuth("Together API key", ["TOGETHER_API_KEY"]) }, + models: Object.values(TOGETHER_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/vercel-ai-gateway.models.ts b/packages/ai/src/providers/vercel-ai-gateway.models.ts new file mode 100644 index 00000000..1eb32f12 --- /dev/null +++ b/packages/ai/src/providers/vercel-ai-gateway.models.ts @@ -0,0 +1,2884 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const VERCEL_AI_GATEWAY_MODELS = { + "alibaba/qwen-3-14b": { + id: "alibaba/qwen-3-14b", + name: "Qwen3-14B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.12, + output: 0.24, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 40960, + maxTokens: 16384, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen-3-235b": { + id: "alibaba/qwen-3-235b", + name: "Qwen3 235B A22B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.22, + output: 0.88, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 16384, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen-3-30b": { + id: "alibaba/qwen-3-30b", + name: "Qwen3-30B-A3B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.12, + output: 0.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 40960, + maxTokens: 16384, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen-3-32b": { + id: "alibaba/qwen-3-32b", + name: "Qwen 3 32B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.16, + output: 0.64, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen-3.6-max-preview": { + id: "alibaba/qwen-3.6-max-preview", + name: "Qwen 3.6 Max Preview", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1.3, + output: 7.8, + cacheRead: 0.26, + cacheWrite: 1.625, + }, + contextWindow: 240000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3-235b-a22b-thinking": { + id: "alibaba/qwen3-235b-a22b-thinking", + name: "Qwen3 VL 235B A22B Thinking", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.39999999999999997, + output: 4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3-coder": { + id: "alibaba/qwen3-coder", + name: "Qwen3 Coder 480B A35B Instruct", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1.5, + output: 7.5, + cacheRead: 0.3, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3-coder-30b-a3b": { + id: "alibaba/qwen3-coder-30b-a3b", + name: "Qwen 3 Coder 30B A3B Instruct", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3-coder-next": { + id: "alibaba/qwen3-coder-next", + name: "Qwen3 Coder Next", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.5, + output: 1.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3-coder-plus": { + id: "alibaba/qwen3-coder-plus", + name: "Qwen3 Coder Plus", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 1, + output: 5, + cacheRead: 0.19999999999999998, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3-max": { + id: "alibaba/qwen3-max", + name: "Qwen3 Max", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 1.2, + output: 6, + cacheRead: 0.24, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3-max-preview": { + id: "alibaba/qwen3-max-preview", + name: "Qwen3 Max Preview", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 1.2, + output: 6, + cacheRead: 0.24, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3-max-thinking": { + id: "alibaba/qwen3-max-thinking", + name: "Qwen 3 Max Thinking", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1.2, + output: 6, + cacheRead: 0.24, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3-next-80b-a3b-instruct": { + id: "alibaba/qwen3-next-80b-a3b-instruct", + name: "Qwen3 Next 80B A3B Instruct", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.15, + output: 1.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3-next-80b-a3b-thinking": { + id: "alibaba/qwen3-next-80b-a3b-thinking", + name: "Qwen3 Next 80B A3B Thinking", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.15, + output: 1.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3-vl-thinking": { + id: "alibaba/qwen3-vl-thinking", + name: "Qwen3 VL 235B A22B Thinking", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.39999999999999997, + output: 4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3.5-flash": { + id: "alibaba/qwen3.5-flash", + name: "Qwen 3.5 Flash", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.09999999999999999, + output: 0.39999999999999997, + cacheRead: 0.001, + cacheWrite: 0.125, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3.5-plus": { + id: "alibaba/qwen3.5-plus", + name: "Qwen 3.5 Plus", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.39999999999999997, + output: 2.4, + cacheRead: 0.04, + cacheWrite: 0.5, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3.6-27b": { + id: "alibaba/qwen3.6-27b", + name: "Qwen 3.6 27B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 3.5999999999999996, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3.6-plus": { + id: "alibaba/qwen3.6-plus", + name: "Qwen 3.6 Plus", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0.09999999999999999, + cacheWrite: 0.625, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3.7-max": { + id: "alibaba/qwen3.7-max", + name: "Qwen 3.7 Max", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1.25, + output: 3.75, + cacheRead: 0.25, + cacheWrite: 1.5625, + }, + contextWindow: 991000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3.7-plus": { + id: "alibaba/qwen3.7-plus", + name: "Qwen 3.7 Plus", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.39999999999999997, + output: 1.5999999999999999, + cacheRead: 0.08, + cacheWrite: 0.5, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-3-haiku": { + id: "anthropic/claude-3-haiku", + name: "Claude 3 Haiku", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.25, + cacheRead: 0.03, + cacheWrite: 0.3, + }, + contextWindow: 200000, + maxTokens: 4096, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-3.5-haiku": { + id: "anthropic/claude-3.5-haiku", + name: "Claude 3.5 Haiku", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.7999999999999999, + output: 4, + cacheRead: 0.08, + cacheWrite: 1, + }, + contextWindow: 200000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-fable-5": { + id: "anthropic/claude-fable-5", + name: "Claude Fable 5", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + compat: {"forceAdaptiveThinking":true}, + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 1, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-haiku-4.5": { + id: "anthropic/claude-haiku-4.5", + name: "Claude Haiku 4.5", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.09999999999999999, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-opus-4": { + id: "anthropic/claude-opus-4", + name: "Claude Opus 4", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-opus-4.1": { + id: "anthropic/claude-opus-4.1", + name: "Claude Opus 4.1", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-opus-4.5": { + id: "anthropic/claude-opus-4.5", + name: "Claude Opus 4.5", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-opus-4.6": { + id: "anthropic/claude-opus-4.6", + name: "Claude Opus 4.6", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + compat: {"forceAdaptiveThinking":true}, + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-opus-4.7": { + id: "anthropic/claude-opus-4.7", + name: "Claude Opus 4.7", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-opus-4.8": { + id: "anthropic/claude-opus-4.8", + name: "Claude Opus 4.8", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-sonnet-4": { + id: "anthropic/claude-sonnet-4", + name: "Claude Sonnet 4", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-sonnet-4.5": { + id: "anthropic/claude-sonnet-4.5", + name: "Claude Sonnet 4.5", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-sonnet-4.6": { + id: "anthropic/claude-sonnet-4.6", + name: "Claude Sonnet 4.6", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + compat: {"forceAdaptiveThinking":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "arcee-ai/trinity-large-preview": { + id: "arcee-ai/trinity-large-preview", + name: "Trinity Large Preview", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.25, + output: 1, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131000, + maxTokens: 131000, + } satisfies Model<"anthropic-messages">, + "arcee-ai/trinity-large-thinking": { + id: "arcee-ai/trinity-large-thinking", + name: "Trinity Large Thinking", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.25, + output: 0.8999999999999999, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262100, + maxTokens: 80000, + } satisfies Model<"anthropic-messages">, + "bytedance/seed-1.6": { + id: "bytedance/seed-1.6", + name: "Seed 1.6", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.049999999999999996, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "cohere/command-a": { + id: "cohere/command-a", + name: "Command A", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 2.5, + output: 10, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 8000, + } satisfies Model<"anthropic-messages">, + "deepseek/deepseek-r1": { + id: "deepseek/deepseek-r1", + name: "DeepSeek-R1", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1.35, + output: 5.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "deepseek/deepseek-v3": { + id: "deepseek/deepseek-v3", + name: "DeepSeek V3 0324", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.27, + output: 1.12, + cacheRead: 0.135, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 163840, + } satisfies Model<"anthropic-messages">, + "deepseek/deepseek-v3.1": { + id: "deepseek/deepseek-v3.1", + name: "DeepSeek V3.1", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.56, + output: 1.68, + cacheRead: 0.28, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "deepseek/deepseek-v3.1-terminus": { + id: "deepseek/deepseek-v3.1-terminus", + name: "DeepSeek V3.1 Terminus", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.27, + output: 1, + cacheRead: 0.135, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, + "deepseek/deepseek-v3.2": { + id: "deepseek/deepseek-v3.2", + name: "DeepSeek V3.2", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.28, + output: 0.42, + cacheRead: 0.028, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8000, + } satisfies Model<"anthropic-messages">, + "deepseek/deepseek-v3.2-thinking": { + id: "deepseek/deepseek-v3.2-thinking", + name: "DeepSeek V3.2 Thinking", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.62, + output: 1.85, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8000, + } satisfies Model<"anthropic-messages">, + "deepseek/deepseek-v4-flash": { + id: "deepseek/deepseek-v4-flash", + name: "DeepSeek V4 Flash", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.14, + output: 0.28, + cacheRead: 0.0028, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"anthropic-messages">, + "deepseek/deepseek-v4-pro": { + id: "deepseek/deepseek-v4-pro", + name: "DeepSeek V4 Pro", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.435, + output: 0.87, + cacheRead: 0.0036, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"anthropic-messages">, + "google/gemini-2.5-flash": { + id: "google/gemini-2.5-flash", + name: "Gemini 2.5 Flash", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 2.5, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, + "google/gemini-2.5-flash-lite": { + id: "google/gemini-2.5-flash-lite", + name: "Gemini 2.5 Flash Lite", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.09999999999999999, + output: 0.39999999999999997, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, + "google/gemini-2.5-pro": { + id: "google/gemini-2.5-pro", + name: "Gemini 2.5 Pro", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, + "google/gemini-3-flash": { + id: "google/gemini-3-flash", + name: "Gemini 3 Flash", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0.049999999999999996, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65000, + } satisfies Model<"anthropic-messages">, + "google/gemini-3-pro-preview": { + id: "google/gemini-3-pro-preview", + name: "Gemini 3 Pro Preview", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.19999999999999998, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "google/gemini-3.1-flash-lite": { + id: "google/gemini-3.1-flash-lite", + name: "Gemini 3.1 Flash Lite", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.5, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65000, + } satisfies Model<"anthropic-messages">, + "google/gemini-3.1-flash-lite-preview": { + id: "google/gemini-3.1-flash-lite-preview", + name: "Gemini 3.1 Flash Lite Preview", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.5, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65000, + } satisfies Model<"anthropic-messages">, + "google/gemini-3.1-pro-preview": { + id: "google/gemini-3.1-pro-preview", + name: "Gemini 3.1 Pro Preview", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.19999999999999998, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "google/gemini-3.5-flash": { + id: "google/gemini-3.5-flash", + name: "Gemini 3.5 Flash", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.5, + output: 9, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "google/gemma-4-26b-a4b-it": { + id: "google/gemma-4-26b-a4b-it", + name: "Gemma 4 26B A4B IT", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.015, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "google/gemma-4-31b-it": { + id: "google/gemma-4-31b-it", + name: "Gemma 4 31B IT", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.14, + output: 0.39999999999999997, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "inception/mercury-2": { + id: "inception/mercury-2", + name: "Mercury 2", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.25, + output: 0.75, + cacheRead: 0.024999999999999998, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "inception/mercury-coder-small": { + id: "inception/mercury-coder-small", + name: "Mercury Coder Small Beta", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.25, + output: 1, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 32000, + maxTokens: 16384, + } satisfies Model<"anthropic-messages">, + "kwaipilot/kat-coder-pro-v2": { + id: "kwaipilot/kat-coder-pro-v2", + name: "Kat Coder Pro V2", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"anthropic-messages">, + "meituan/longcat-flash-chat": { + id: "meituan/longcat-flash-chat", + name: "LongCat Flash Chat", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 100000, + } satisfies Model<"anthropic-messages">, + "meta/llama-3.1-70b": { + id: "meta/llama-3.1-70b", + name: "Llama 3.1 70B Instruct", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.72, + output: 0.72, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "meta/llama-3.1-8b": { + id: "meta/llama-3.1-8b", + name: "Llama 3.1 8B Instruct", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.22, + output: 0.22, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "meta/llama-3.2-11b": { + id: "meta/llama-3.2-11b", + name: "Llama 3.2 11B Vision Instruct", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.16, + output: 0.16, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "meta/llama-3.2-90b": { + id: "meta/llama-3.2-90b", + name: "Llama 3.2 90B Vision Instruct", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.72, + output: 0.72, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "meta/llama-3.3-70b": { + id: "meta/llama-3.3-70b", + name: "Llama 3.3 70B Instruct", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.72, + output: 0.72, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "meta/llama-4-maverick": { + id: "meta/llama-4-maverick", + name: "Llama 4 Maverick 17B Instruct", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.24, + output: 0.9700000000000001, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "meta/llama-4-scout": { + id: "meta/llama-4-scout", + name: "Llama 4 Scout 17B Instruct", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.16999999999999998, + output: 0.66, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "minimax/minimax-m2": { + id: "minimax/minimax-m2", + name: "MiniMax M2", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.03, + cacheWrite: 0.375, + }, + contextWindow: 205000, + maxTokens: 205000, + } satisfies Model<"anthropic-messages">, + "minimax/minimax-m2.1": { + id: "minimax/minimax-m2.1", + name: "MiniMax M2.1", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.03, + cacheWrite: 0.375, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "minimax/minimax-m2.1-lightning": { + id: "minimax/minimax-m2.1-lightning", + name: "MiniMax M2.1 Lightning", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 2.4, + cacheRead: 0.03, + cacheWrite: 0.375, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "minimax/minimax-m2.5": { + id: "minimax/minimax-m2.5", + name: "MiniMax M2.5", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.03, + cacheWrite: 0.375, + }, + contextWindow: 204800, + maxTokens: 131000, + } satisfies Model<"anthropic-messages">, + "minimax/minimax-m2.5-highspeed": { + id: "minimax/minimax-m2.5-highspeed", + name: "MiniMax M2.5 High Speed", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.4, + cacheRead: 0.03, + cacheWrite: 0.375, + }, + contextWindow: 204800, + maxTokens: 131000, + } satisfies Model<"anthropic-messages">, + "minimax/minimax-m2.7": { + id: "minimax/minimax-m2.7", + name: "MiniMax M2.7", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0.375, + }, + contextWindow: 204800, + maxTokens: 131000, + } satisfies Model<"anthropic-messages">, + "minimax/minimax-m2.7-highspeed": { + id: "minimax/minimax-m2.7-highspeed", + name: "MiniMax M2.7 High Speed", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.4, + cacheRead: 0.06, + cacheWrite: 0.375, + }, + contextWindow: 204800, + maxTokens: 131100, + } satisfies Model<"anthropic-messages">, + "minimax/minimax-m3": { + id: "minimax/minimax-m3", + name: "MiniMax M3", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 1000000, + } satisfies Model<"anthropic-messages">, + "mistral/codestral": { + id: "mistral/codestral", + name: "Mistral Codestral", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.3, + output: 0.8999999999999999, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4000, + } satisfies Model<"anthropic-messages">, + "mistral/devstral-2": { + id: "mistral/devstral-2", + name: "Devstral 2", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.39999999999999997, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"anthropic-messages">, + "mistral/devstral-small": { + id: "mistral/devstral-small", + name: "Devstral Small 1.1", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.09999999999999999, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "mistral/devstral-small-2": { + id: "mistral/devstral-small-2", + name: "Devstral Small 2", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.09999999999999999, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"anthropic-messages">, + "mistral/ministral-3b": { + id: "mistral/ministral-3b", + name: "Ministral 3B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.09999999999999999, + output: 0.09999999999999999, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4000, + } satisfies Model<"anthropic-messages">, + "mistral/ministral-8b": { + id: "mistral/ministral-8b", + name: "Ministral 8B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.15, + output: 0.15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4000, + } satisfies Model<"anthropic-messages">, + "mistral/mistral-medium": { + id: "mistral/mistral-medium", + name: "Mistral Medium 3.1", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.39999999999999997, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "mistral/mistral-medium-3.5": { + id: "mistral/mistral-medium-3.5", + name: "Mistral Medium Latest", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1.5, + output: 7.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"anthropic-messages">, + "mistral/mistral-nemo": { + id: "mistral/mistral-nemo", + name: "Mistral Nemo 12B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.02, + output: 0.04, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "mistral/mistral-small": { + id: "mistral/mistral-small", + name: "Mistral Small", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.09999999999999999, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 32000, + maxTokens: 4000, + } satisfies Model<"anthropic-messages">, + "mistral/pixtral-12b": { + id: "mistral/pixtral-12b", + name: "Pixtral 12B 2409", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4000, + } satisfies Model<"anthropic-messages">, + "mistral/pixtral-large": { + id: "mistral/pixtral-large", + name: "Pixtral Large", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2, + output: 6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4000, + } satisfies Model<"anthropic-messages">, + "moonshotai/kimi-k2": { + id: "moonshotai/kimi-k2", + name: "Kimi K2 Instruct", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.5700000000000001, + output: 2.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "moonshotai/kimi-k2-thinking": { + id: "moonshotai/kimi-k2-thinking", + name: "Kimi K2 Thinking", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.5, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 262114, + maxTokens: 262114, + } satisfies Model<"anthropic-messages">, + "moonshotai/kimi-k2-thinking-turbo": { + id: "moonshotai/kimi-k2-thinking-turbo", + name: "Kimi K2 Thinking Turbo", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1.15, + output: 8, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 262114, + maxTokens: 262114, + } satisfies Model<"anthropic-messages">, + "moonshotai/kimi-k2-turbo": { + id: "moonshotai/kimi-k2-turbo", + name: "Kimi K2 Turbo", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 1.15, + output: 8, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 16384, + } satisfies Model<"anthropic-messages">, + "moonshotai/kimi-k2.5": { + id: "moonshotai/kimi-k2.5", + name: "Kimi K2.5", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 3, + cacheRead: 0.09999999999999999, + cacheWrite: 0, + }, + contextWindow: 262114, + maxTokens: 262114, + } satisfies Model<"anthropic-messages">, + "moonshotai/kimi-k2.6": { + id: "moonshotai/kimi-k2.6", + name: "Kimi K2.6", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.16, + cacheWrite: 0, + }, + contextWindow: 262000, + maxTokens: 262000, + } satisfies Model<"anthropic-messages">, + "nvidia/nemotron-3-super-120b-a12b": { + id: "nvidia/nemotron-3-super-120b-a12b", + name: "NVIDIA Nemotron 3 Super 120B A12B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.15, + output: 0.65, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "nvidia/nemotron-3-ultra-550b-a55b": { + id: "nvidia/nemotron-3-ultra-550b-a55b", + name: "Nemotron 3 Ultra", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.4, + cacheRead: 0.12, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65000, + } satisfies Model<"anthropic-messages">, + "nvidia/nemotron-nano-12b-v2-vl": { + id: "nvidia/nemotron-nano-12b-v2-vl", + name: "Nvidia Nemotron Nano 12B V2 VL", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.19999999999999998, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "nvidia/nemotron-nano-9b-v2": { + id: "nvidia/nemotron-nano-9b-v2", + name: "Nvidia Nemotron Nano 9B V2", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.06, + output: 0.22999999999999998, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "openai/gpt-4-turbo": { + id: "openai/gpt-4-turbo", + name: "GPT-4 Turbo", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 10, + output: 30, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"anthropic-messages">, + "openai/gpt-4.1": { + id: "openai/gpt-4.1", + name: "GPT-4.1", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "openai/gpt-4.1-mini": { + id: "openai/gpt-4.1-mini", + name: "GPT-4.1 mini", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.39999999999999997, + output: 1.5999999999999999, + cacheRead: 0.09999999999999999, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "openai/gpt-4.1-nano": { + id: "openai/gpt-4.1-nano", + name: "GPT-4.1 nano", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.09999999999999999, + output: 0.39999999999999997, + cacheRead: 0.024999999999999998, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "openai/gpt-4o": { + id: "openai/gpt-4o", + name: "GPT-4o", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 10, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"anthropic-messages">, + "openai/gpt-4o-mini": { + id: "openai/gpt-4o-mini", + name: "GPT-4o mini", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5": { + id: "openai/gpt-5", + name: "GPT-5", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5-chat": { + id: "openai/gpt-5-chat", + name: "GPT 5 Chat", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5-codex": { + id: "openai/gpt-5-codex", + name: "GPT-5-Codex", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5-mini": { + id: "openai/gpt-5-mini", + name: "GPT-5 mini", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.024999999999999998, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5-nano": { + id: "openai/gpt-5-nano", + name: "GPT-5 nano", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.049999999999999996, + output: 0.39999999999999997, + cacheRead: 0.005, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5-pro": { + id: "openai/gpt-5-pro", + name: "GPT-5 pro", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 120, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 272000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.1-codex": { + id: "openai/gpt-5.1-codex", + name: "GPT-5.1-Codex", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.1-codex-max": { + id: "openai/gpt-5.1-codex-max", + name: "GPT 5.1 Codex Max", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.1-codex-mini": { + id: "openai/gpt-5.1-codex-mini", + name: "GPT 5.1 Codex Mini", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.024999999999999998, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.1-instant": { + id: "openai/gpt-5.1-instant", + name: "GPT-5.1 Instant", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.1-thinking": { + id: "openai/gpt-5.1-thinking", + name: "GPT 5.1 Thinking", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.2": { + id: "openai/gpt-5.2", + name: "GPT 5.2", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.2-chat": { + id: "openai/gpt-5.2-chat", + name: "GPT 5.2 Chat", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.2-codex": { + id: "openai/gpt-5.2-codex", + name: "GPT 5.2 Codex", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.2-pro": { + id: "openai/gpt-5.2-pro", + name: "GPT 5.2 ", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 21, + output: 168, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.3-chat": { + id: "openai/gpt-5.3-chat", + name: "GPT-5.3 Chat", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.3-codex": { + id: "openai/gpt-5.3-codex", + name: "GPT 5.3 Codex", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.4": { + id: "openai/gpt-5.4", + name: "GPT 5.4", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 2.5, + output: 15, + cacheRead: 0.25, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.4-mini": { + id: "openai/gpt-5.4-mini", + name: "GPT 5.4 Mini", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.75, + output: 4.5, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.4-nano": { + id: "openai/gpt-5.4-nano", + name: "GPT 5.4 Nano", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.19999999999999998, + output: 1.25, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.4-pro": { + id: "openai/gpt-5.4-pro", + name: "GPT 5.4 Pro", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 30, + output: 180, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.5": { + id: "openai/gpt-5.5", + name: "GPT 5.5", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.5-pro": { + id: "openai/gpt-5.5-pro", + name: "GPT 5.5 Pro", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","off":null,"minimal":null,"low":null}, + input: ["text", "image"], + cost: { + input: 30, + output: 180, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-oss-120b": { + id: "openai/gpt-oss-120b", + name: "GPT OSS 120B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.35, + output: 0.75, + cacheRead: 0.25, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-oss-20b": { + id: "openai/gpt-oss-20b", + name: "GPT OSS 20B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.049999999999999996, + output: 0.19999999999999998, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "openai/gpt-oss-safeguard-20b": { + id: "openai/gpt-oss-safeguard-20b", + name: "GPT OSS Safeguard 20B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.075, + output: 0.3, + cacheRead: 0.037, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, + "openai/o1": { + id: "openai/o1", + name: "o1", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 60, + cacheRead: 7.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"anthropic-messages">, + "openai/o3": { + id: "openai/o3", + name: "o3", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"anthropic-messages">, + "openai/o3-deep-research": { + id: "openai/o3-deep-research", + name: "o3-deep-research", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 10, + output: 40, + cacheRead: 2.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"anthropic-messages">, + "openai/o3-mini": { + id: "openai/o3-mini", + name: "o3-mini", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.55, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"anthropic-messages">, + "openai/o3-pro": { + id: "openai/o3-pro", + name: "o3 Pro", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 20, + output: 80, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"anthropic-messages">, + "openai/o4-mini": { + id: "openai/o4-mini", + name: "o4-mini", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.275, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"anthropic-messages">, + "perplexity/sonar": { + id: "perplexity/sonar", + name: "Sonar", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 127000, + maxTokens: 8000, + } satisfies Model<"anthropic-messages">, + "perplexity/sonar-pro": { + id: "perplexity/sonar-pro", + name: "Sonar Pro", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 8000, + } satisfies Model<"anthropic-messages">, + "stepfun/step-3.5-flash": { + id: "stepfun/step-3.5-flash", + name: "StepFun 3.5 Flash", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.09, + output: 0.3, + cacheRead: 0, + cacheWrite: 0.02, + }, + contextWindow: 262114, + maxTokens: 262114, + } satisfies Model<"anthropic-messages">, + "stepfun/step-3.7-flash": { + id: "stepfun/step-3.7-flash", + name: "Step 3.7 Flash", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.19999999999999998, + output: 1.15, + cacheRead: 0.04, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"anthropic-messages">, + "xai/grok-4.1-fast-non-reasoning": { + id: "xai/grok-4.1-fast-non-reasoning", + name: "Grok 4.1 Fast Non-Reasoning", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.19999999999999998, + output: 0.5, + cacheRead: 0.049999999999999996, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 1000000, + } satisfies Model<"anthropic-messages">, + "xai/grok-4.1-fast-reasoning": { + id: "xai/grok-4.1-fast-reasoning", + name: "Grok 4.1 Fast Reasoning", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.19999999999999998, + output: 0.5, + cacheRead: 0.049999999999999996, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 1000000, + } satisfies Model<"anthropic-messages">, + "xai/grok-4.20-multi-agent": { + id: "xai/grok-4.20-multi-agent", + name: "Grok 4.20 Multi-Agent", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.19999999999999998, + cacheWrite: 0, + }, + contextWindow: 2000000, + maxTokens: 2000000, + } satisfies Model<"anthropic-messages">, + "xai/grok-4.20-multi-agent-beta": { + id: "xai/grok-4.20-multi-agent-beta", + name: "Grok 4.20 Multi Agent Beta", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.19999999999999998, + cacheWrite: 0, + }, + contextWindow: 2000000, + maxTokens: 2000000, + } satisfies Model<"anthropic-messages">, + "xai/grok-4.20-non-reasoning": { + id: "xai/grok-4.20-non-reasoning", + name: "Grok 4.20 Non-Reasoning", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.19999999999999998, + cacheWrite: 0, + }, + contextWindow: 2000000, + maxTokens: 2000000, + } satisfies Model<"anthropic-messages">, + "xai/grok-4.20-non-reasoning-beta": { + id: "xai/grok-4.20-non-reasoning-beta", + name: "Grok 4.20 Beta Non-Reasoning", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.19999999999999998, + cacheWrite: 0, + }, + contextWindow: 2000000, + maxTokens: 2000000, + } satisfies Model<"anthropic-messages">, + "xai/grok-4.20-reasoning": { + id: "xai/grok-4.20-reasoning", + name: "Grok 4.20 Reasoning", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.19999999999999998, + cacheWrite: 0, + }, + contextWindow: 2000000, + maxTokens: 2000000, + } satisfies Model<"anthropic-messages">, + "xai/grok-4.20-reasoning-beta": { + id: "xai/grok-4.20-reasoning-beta", + name: "Grok 4.20 Beta Reasoning", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.19999999999999998, + cacheWrite: 0, + }, + contextWindow: 2000000, + maxTokens: 2000000, + } satisfies Model<"anthropic-messages">, + "xai/grok-4.3": { + id: "xai/grok-4.3", + name: "Grok 4.3", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.19999999999999998, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 1000000, + } satisfies Model<"anthropic-messages">, + "xai/grok-build-0.1": { + id: "xai/grok-build-0.1", + name: "Grok Build 0.1", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 2, + cacheRead: 0.19999999999999998, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"anthropic-messages">, + "xiaomi/mimo-v2-flash": { + id: "xiaomi/mimo-v2-flash", + name: "MiMo V2 Flash", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.09999999999999999, + output: 0.3, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "xiaomi/mimo-v2-pro": { + id: "xiaomi/mimo-v2-pro", + name: "MiMo V2 Pro", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0.19999999999999998, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "xiaomi/mimo-v2.5": { + id: "xiaomi/mimo-v2.5", + name: "MiMo M2.5", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.14, + output: 0.28, + cacheRead: 0.0028, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 131100, + } satisfies Model<"anthropic-messages">, + "xiaomi/mimo-v2.5-pro": { + id: "xiaomi/mimo-v2.5-pro", + name: "MiMo V2.5 Pro", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.435, + output: 0.87, + cacheRead: 0.0036, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 131000, + } satisfies Model<"anthropic-messages">, + "zai/glm-4.5": { + id: "zai/glm-4.5", + name: "GLM-4.5", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.2, + cacheRead: 0.11, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 96000, + } satisfies Model<"anthropic-messages">, + "zai/glm-4.5-air": { + id: "zai/glm-4.5-air", + name: "GLM 4.5 Air", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.19999999999999998, + output: 1.1, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 96000, + } satisfies Model<"anthropic-messages">, + "zai/glm-4.5v": { + id: "zai/glm-4.5v", + name: "GLM 4.5V", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 1.7999999999999998, + cacheRead: 0.11, + cacheWrite: 0, + }, + contextWindow: 66000, + maxTokens: 16000, + } satisfies Model<"anthropic-messages">, + "zai/glm-4.6": { + id: "zai/glm-4.6", + name: "GLM 4.6", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.2, + cacheRead: 0.11, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 96000, + } satisfies Model<"anthropic-messages">, + "zai/glm-4.6v": { + id: "zai/glm-4.6v", + name: "GLM-4.6V", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 0.8999999999999999, + cacheRead: 0.049999999999999996, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 24000, + } satisfies Model<"anthropic-messages">, + "zai/glm-4.6v-flash": { + id: "zai/glm-4.6v-flash", + name: "GLM-4.6V-Flash", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 24000, + } satisfies Model<"anthropic-messages">, + "zai/glm-4.7": { + id: "zai/glm-4.7", + name: "GLM 4.7", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 2.25, + output: 2.75, + cacheRead: 2.25, + cacheWrite: 0, + }, + contextWindow: 131000, + maxTokens: 40000, + } satisfies Model<"anthropic-messages">, + "zai/glm-4.7-flash": { + id: "zai/glm-4.7-flash", + name: "GLM 4.7 Flash", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.07, + output: 0.39999999999999997, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 131000, + } satisfies Model<"anthropic-messages">, + "zai/glm-4.7-flashx": { + id: "zai/glm-4.7-flashx", + name: "GLM 4.7 FlashX", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.06, + output: 0.39999999999999997, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "zai/glm-5": { + id: "zai/glm-5", + name: "GLM 5", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3.1999999999999997, + cacheRead: 0.19999999999999998, + cacheWrite: 0, + }, + contextWindow: 202800, + maxTokens: 131100, + } satisfies Model<"anthropic-messages">, + "zai/glm-5-turbo": { + id: "zai/glm-5-turbo", + name: "GLM 5 Turbo", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1.2, + output: 4, + cacheRead: 0.24, + cacheWrite: 0, + }, + contextWindow: 202800, + maxTokens: 131100, + } satisfies Model<"anthropic-messages">, + "zai/glm-5.1": { + id: "zai/glm-5.1", + name: "GLM 5.1", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0.26, + cacheWrite: 0, + }, + contextWindow: 202800, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "zai/glm-5v-turbo": { + id: "zai/glm-5v-turbo", + name: "GLM 5V Turbo", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.2, + output: 4, + cacheRead: 0.24, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, +} as const; diff --git a/packages/ai/src/providers/vercel-ai-gateway.ts b/packages/ai/src/providers/vercel-ai-gateway.ts new file mode 100644 index 00000000..3aca0328 --- /dev/null +++ b/packages/ai/src/providers/vercel-ai-gateway.ts @@ -0,0 +1,15 @@ +import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { VERCEL_AI_GATEWAY_MODELS } from "./vercel-ai-gateway.models.ts"; + +export function vercelAIGatewayProvider(): Provider<"anthropic-messages"> { + return createProvider({ + id: "vercel-ai-gateway", + name: "Vercel AI Gateway", + baseUrl: "https://ai-gateway.vercel.sh", + auth: { apiKey: envApiKeyAuth("Vercel AI Gateway API key", ["AI_GATEWAY_API_KEY"]) }, + models: Object.values(VERCEL_AI_GATEWAY_MODELS), + api: anthropicMessagesApi(), + }); +} diff --git a/packages/ai/src/providers/xai.models.ts b/packages/ai/src/providers/xai.models.ts new file mode 100644 index 00000000..878193aa --- /dev/null +++ b/packages/ai/src/providers/xai.models.ts @@ -0,0 +1,126 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const XAI_MODELS = { + "grok-3": { + id: "grok-3", + name: "Grok 3", + api: "openai-completions", + provider: "xai", + baseUrl: "https://api.x.ai/v1", + reasoning: false, + input: ["text"], + cost: { + input: 3, + output: 15, + cacheRead: 0.75, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 8192, + } satisfies Model<"openai-completions">, + "grok-3-fast": { + id: "grok-3-fast", + name: "Grok 3 Fast", + api: "openai-completions", + provider: "xai", + baseUrl: "https://api.x.ai/v1", + reasoning: false, + input: ["text"], + cost: { + input: 5, + output: 25, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 8192, + } satisfies Model<"openai-completions">, + "grok-4.20-0309-non-reasoning": { + id: "grok-4.20-0309-non-reasoning", + name: "Grok 4.20 (Non-Reasoning)", + api: "openai-completions", + provider: "xai", + baseUrl: "https://api.x.ai/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 30000, + } satisfies Model<"openai-completions">, + "grok-4.20-0309-reasoning": { + id: "grok-4.20-0309-reasoning", + name: "Grok 4.20 (Reasoning)", + api: "openai-completions", + provider: "xai", + baseUrl: "https://api.x.ai/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 30000, + } satisfies Model<"openai-completions">, + "grok-4.3": { + id: "grok-4.3", + name: "Grok 4.3", + api: "openai-completions", + provider: "xai", + baseUrl: "https://api.x.ai/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 30000, + } satisfies Model<"openai-completions">, + "grok-build-0.1": { + id: "grok-build-0.1", + name: "Grok Build 0.1", + api: "openai-completions", + provider: "xai", + baseUrl: "https://api.x.ai/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 2, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"openai-completions">, + "grok-code-fast-1": { + id: "grok-code-fast-1", + name: "Grok Code Fast 1", + api: "openai-completions", + provider: "xai", + baseUrl: "https://api.x.ai/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.2, + output: 1.5, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 32768, + maxTokens: 8192, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/xai.ts b/packages/ai/src/providers/xai.ts new file mode 100644 index 00000000..3373fbf5 --- /dev/null +++ b/packages/ai/src/providers/xai.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { XAI_MODELS } from "./xai.models.ts"; + +export function xaiProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "xai", + name: "xAI", + baseUrl: "https://api.x.ai/v1", + auth: { apiKey: envApiKeyAuth("xAI API key", ["XAI_API_KEY"]) }, + models: Object.values(XAI_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/xiaomi-token-plan-ams.models.ts b/packages/ai/src/providers/xiaomi-token-plan-ams.models.ts new file mode 100644 index 00000000..fec90428 --- /dev/null +++ b/packages/ai/src/providers/xiaomi-token-plan-ams.models.ts @@ -0,0 +1,97 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const XIAOMI_TOKEN_PLAN_AMS_MODELS = { + "mimo-v2-omni": { + id: "mimo-v2-omni", + name: "MiMo-V2-Omni", + api: "openai-completions", + provider: "xiaomi-token-plan-ams", + baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2-pro": { + id: "mimo-v2-pro", + name: "MiMo-V2-Pro", + api: "openai-completions", + provider: "xiaomi-token-plan-ams", + baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5": { + id: "mimo-v2.5", + name: "MiMo-V2.5", + api: "openai-completions", + provider: "xiaomi-token-plan-ams", + baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5-pro": { + id: "mimo-v2.5-pro", + name: "MiMo-V2.5-Pro", + api: "openai-completions", + provider: "xiaomi-token-plan-ams", + baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5-pro-ultraspeed": { + id: "mimo-v2.5-pro-ultraspeed", + name: "MiMo-V2.5-Pro-UltraSpeed", + api: "openai-completions", + provider: "xiaomi-token-plan-ams", + baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.305, + output: 2.61, + cacheRead: 0.0108, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/xiaomi-token-plan-ams.ts b/packages/ai/src/providers/xiaomi-token-plan-ams.ts new file mode 100644 index 00000000..017aa671 --- /dev/null +++ b/packages/ai/src/providers/xiaomi-token-plan-ams.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { XIAOMI_TOKEN_PLAN_AMS_MODELS } from "./xiaomi-token-plan-ams.models.ts"; + +export function xiaomiTokenPlanAmsProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "xiaomi-token-plan-ams", + name: "Xiaomi Token Plan AMS", + baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", + auth: { apiKey: envApiKeyAuth("Xiaomi Token Plan AMS API key", ["XIAOMI_TOKEN_PLAN_AMS_API_KEY"]) }, + models: Object.values(XIAOMI_TOKEN_PLAN_AMS_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/xiaomi-token-plan-cn.models.ts b/packages/ai/src/providers/xiaomi-token-plan-cn.models.ts new file mode 100644 index 00000000..9932fefa --- /dev/null +++ b/packages/ai/src/providers/xiaomi-token-plan-cn.models.ts @@ -0,0 +1,97 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const XIAOMI_TOKEN_PLAN_CN_MODELS = { + "mimo-v2-omni": { + id: "mimo-v2-omni", + name: "MiMo-V2-Omni", + api: "openai-completions", + provider: "xiaomi-token-plan-cn", + baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2-pro": { + id: "mimo-v2-pro", + name: "MiMo-V2-Pro", + api: "openai-completions", + provider: "xiaomi-token-plan-cn", + baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5": { + id: "mimo-v2.5", + name: "MiMo-V2.5", + api: "openai-completions", + provider: "xiaomi-token-plan-cn", + baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5-pro": { + id: "mimo-v2.5-pro", + name: "MiMo-V2.5-Pro", + api: "openai-completions", + provider: "xiaomi-token-plan-cn", + baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5-pro-ultraspeed": { + id: "mimo-v2.5-pro-ultraspeed", + name: "MiMo-V2.5-Pro-UltraSpeed", + api: "openai-completions", + provider: "xiaomi-token-plan-cn", + baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.305, + output: 2.61, + cacheRead: 0.0108, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/xiaomi-token-plan-cn.ts b/packages/ai/src/providers/xiaomi-token-plan-cn.ts new file mode 100644 index 00000000..f7ab14fa --- /dev/null +++ b/packages/ai/src/providers/xiaomi-token-plan-cn.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { XIAOMI_TOKEN_PLAN_CN_MODELS } from "./xiaomi-token-plan-cn.models.ts"; + +export function xiaomiTokenPlanCnProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "xiaomi-token-plan-cn", + name: "Xiaomi Token Plan CN", + baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", + auth: { apiKey: envApiKeyAuth("Xiaomi Token Plan CN API key", ["XIAOMI_TOKEN_PLAN_CN_API_KEY"]) }, + models: Object.values(XIAOMI_TOKEN_PLAN_CN_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/xiaomi-token-plan-sgp.models.ts b/packages/ai/src/providers/xiaomi-token-plan-sgp.models.ts new file mode 100644 index 00000000..dd248921 --- /dev/null +++ b/packages/ai/src/providers/xiaomi-token-plan-sgp.models.ts @@ -0,0 +1,97 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const XIAOMI_TOKEN_PLAN_SGP_MODELS = { + "mimo-v2-omni": { + id: "mimo-v2-omni", + name: "MiMo-V2-Omni", + api: "openai-completions", + provider: "xiaomi-token-plan-sgp", + baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2-pro": { + id: "mimo-v2-pro", + name: "MiMo-V2-Pro", + api: "openai-completions", + provider: "xiaomi-token-plan-sgp", + baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5": { + id: "mimo-v2.5", + name: "MiMo-V2.5", + api: "openai-completions", + provider: "xiaomi-token-plan-sgp", + baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5-pro": { + id: "mimo-v2.5-pro", + name: "MiMo-V2.5-Pro", + api: "openai-completions", + provider: "xiaomi-token-plan-sgp", + baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5-pro-ultraspeed": { + id: "mimo-v2.5-pro-ultraspeed", + name: "MiMo-V2.5-Pro-UltraSpeed", + api: "openai-completions", + provider: "xiaomi-token-plan-sgp", + baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.305, + output: 2.61, + cacheRead: 0.0108, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/xiaomi-token-plan-sgp.ts b/packages/ai/src/providers/xiaomi-token-plan-sgp.ts new file mode 100644 index 00000000..e3762057 --- /dev/null +++ b/packages/ai/src/providers/xiaomi-token-plan-sgp.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { XIAOMI_TOKEN_PLAN_SGP_MODELS } from "./xiaomi-token-plan-sgp.models.ts"; + +export function xiaomiTokenPlanSgpProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "xiaomi-token-plan-sgp", + name: "Xiaomi Token Plan SGP", + baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", + auth: { apiKey: envApiKeyAuth("Xiaomi Token Plan SGP API key", ["XIAOMI_TOKEN_PLAN_SGP_API_KEY"]) }, + models: Object.values(XIAOMI_TOKEN_PLAN_SGP_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/xiaomi.models.ts b/packages/ai/src/providers/xiaomi.models.ts new file mode 100644 index 00000000..23ec9d55 --- /dev/null +++ b/packages/ai/src/providers/xiaomi.models.ts @@ -0,0 +1,115 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const XIAOMI_MODELS = { + "mimo-v2-flash": { + id: "mimo-v2-flash", + name: "MiMo-V2-Flash", + api: "openai-completions", + provider: "xiaomi", + baseUrl: "https://api.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.1, + output: 0.3, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "mimo-v2-omni": { + id: "mimo-v2-omni", + name: "MiMo-V2-Omni", + api: "openai-completions", + provider: "xiaomi", + baseUrl: "https://api.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2-pro": { + id: "mimo-v2-pro", + name: "MiMo-V2-Pro", + api: "openai-completions", + provider: "xiaomi", + baseUrl: "https://api.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5": { + id: "mimo-v2.5", + name: "MiMo-V2.5", + api: "openai-completions", + provider: "xiaomi", + baseUrl: "https://api.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5-pro": { + id: "mimo-v2.5-pro", + name: "MiMo-V2.5-Pro", + api: "openai-completions", + provider: "xiaomi", + baseUrl: "https://api.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5-pro-ultraspeed": { + id: "mimo-v2.5-pro-ultraspeed", + name: "MiMo-V2.5-Pro-UltraSpeed", + api: "openai-completions", + provider: "xiaomi", + baseUrl: "https://api.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.305, + output: 2.61, + cacheRead: 0.0108, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/xiaomi.ts b/packages/ai/src/providers/xiaomi.ts new file mode 100644 index 00000000..5abf5169 --- /dev/null +++ b/packages/ai/src/providers/xiaomi.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { XIAOMI_MODELS } from "./xiaomi.models.ts"; + +export function xiaomiProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "xiaomi", + name: "Xiaomi", + baseUrl: "https://api.xiaomimimo.com/v1", + auth: { apiKey: envApiKeyAuth("Xiaomi API key", ["XIAOMI_API_KEY"]) }, + models: Object.values(XIAOMI_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/zai-coding-cn.models.ts b/packages/ai/src/providers/zai-coding-cn.models.ts new file mode 100644 index 00000000..3f6cc35f --- /dev/null +++ b/packages/ai/src/providers/zai-coding-cn.models.ts @@ -0,0 +1,97 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const ZAI_CODING_CN_MODELS = { + "glm-4.5-air": { + id: "glm-4.5-air", + name: "GLM-4.5-Air", + api: "openai-completions", + provider: "zai-coding-cn", + baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 98304, + } satisfies Model<"openai-completions">, + "glm-4.7": { + id: "glm-4.7", + name: "GLM-4.7", + api: "openai-completions", + provider: "zai-coding-cn", + baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "glm-5-turbo": { + id: "glm-5-turbo", + name: "GLM-5-Turbo", + api: "openai-completions", + provider: "zai-coding-cn", + baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "glm-5.1": { + id: "glm-5.1", + name: "GLM-5.1", + api: "openai-completions", + provider: "zai-coding-cn", + baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "glm-5v-turbo": { + id: "glm-5v-turbo", + name: "GLM-5V-Turbo", + api: "openai-completions", + provider: "zai-coding-cn", + baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/zai-coding-cn.ts b/packages/ai/src/providers/zai-coding-cn.ts new file mode 100644 index 00000000..2f15a6ca --- /dev/null +++ b/packages/ai/src/providers/zai-coding-cn.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { ZAI_CODING_CN_MODELS } from "./zai-coding-cn.models.ts"; + +export function zaiCodingCnProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "zai-coding-cn", + name: "Z.AI Coding CN", + baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", + auth: { apiKey: envApiKeyAuth("Z.AI Coding CN API key", ["ZAI_CODING_CN_API_KEY"]) }, + models: Object.values(ZAI_CODING_CN_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/zai.models.ts b/packages/ai/src/providers/zai.models.ts new file mode 100644 index 00000000..b1da13f6 --- /dev/null +++ b/packages/ai/src/providers/zai.models.ts @@ -0,0 +1,97 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import type { Model } from "../types.ts"; + +export const ZAI_MODELS = { + "glm-4.5-air": { + id: "glm-4.5-air", + name: "GLM-4.5-Air", + api: "openai-completions", + provider: "zai", + baseUrl: "https://api.z.ai/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 98304, + } satisfies Model<"openai-completions">, + "glm-4.7": { + id: "glm-4.7", + name: "GLM-4.7", + api: "openai-completions", + provider: "zai", + baseUrl: "https://api.z.ai/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "glm-5-turbo": { + id: "glm-5-turbo", + name: "GLM-5-Turbo", + api: "openai-completions", + provider: "zai", + baseUrl: "https://api.z.ai/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "glm-5.1": { + id: "glm-5.1", + name: "GLM-5.1", + api: "openai-completions", + provider: "zai", + baseUrl: "https://api.z.ai/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "glm-5v-turbo": { + id: "glm-5v-turbo", + name: "GLM-5V-Turbo", + api: "openai-completions", + provider: "zai", + baseUrl: "https://api.z.ai/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/zai.ts b/packages/ai/src/providers/zai.ts new file mode 100644 index 00000000..85401066 --- /dev/null +++ b/packages/ai/src/providers/zai.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { ZAI_MODELS } from "./zai.models.ts"; + +export function zaiProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "zai", + name: "Z.AI", + baseUrl: "https://api.z.ai/api/coding/paas/v4", + auth: { apiKey: envApiKeyAuth("Z.AI API key", ["ZAI_API_KEY"]) }, + models: Object.values(ZAI_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/utils/oauth/load.ts b/packages/ai/src/utils/oauth/load.ts new file mode 100644 index 00000000..11198853 --- /dev/null +++ b/packages/ai/src/utils/oauth/load.ts @@ -0,0 +1,21 @@ +import type { OAuthAuth } from "../../auth/types.ts"; + +/** + * Loads an OAuth flow module through a variable specifier so bundlers cannot + * follow the import into Node-only flow code (`node:http` callback servers, + * `node:crypto` PKCE). The `.ts`/`.js` rewrite keeps the trick working from + * both source and built output. + */ +const importOAuthModule = (specifier: string): Promise => { + const runtimeSpecifier = import.meta.url.endsWith(".js") ? specifier.replace(/\.ts$/, ".js") : specifier; + return import(runtimeSpecifier); +}; + +export const loadAnthropicOAuth = async (): Promise => + ((await importOAuthModule("./anthropic.ts")) as { anthropicOAuth: OAuthAuth }).anthropicOAuth; + +export const loadOpenAICodexOAuth = async (): Promise => + ((await importOAuthModule("./openai-codex.ts")) as { openaiCodexOAuth: OAuthAuth }).openaiCodexOAuth; + +export const loadGitHubCopilotOAuth = async (): Promise => + ((await importOAuthModule("./github-copilot.ts")) as { githubCopilotOAuth: OAuthAuth }).githubCopilotOAuth; diff --git a/packages/ai/test/lazy-module-load.test.ts b/packages/ai/test/lazy-module-load.test.ts index bfb4005e..aa25d9a1 100644 --- a/packages/ai/test/lazy-module-load.test.ts +++ b/packages/ai/test/lazy-module-load.test.ts @@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest"; const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const aiEntryUrl = new URL("../src/index.ts", import.meta.url).href; +const providersAllUrl = new URL("../src/providers/all.ts", import.meta.url).href; const SDK_SPECIFIERS = [ "@anthropic-ai/sdk", @@ -66,6 +67,15 @@ describe("lazy provider module loading", () => { expect(result.loadedSpecifiers).toEqual([]); }); + it("does not load provider SDKs when building all builtin providers", () => { + const result = runProbe(` + const all = await import(${JSON.stringify(providersAllUrl)}); + const models = all.builtinModels(); + await models.getModels(); + `); + expect(result.loadedSpecifiers).toEqual([]); + }); + it("loads only the Anthropic SDK when streaming through the lazy API wrapper", () => { const result = runProbe(` const model = { diff --git a/packages/ai/test/providers.test.ts b/packages/ai/test/providers.test.ts new file mode 100644 index 00000000..2b41ba1c --- /dev/null +++ b/packages/ai/test/providers.test.ts @@ -0,0 +1,208 @@ +import { describe, expect, it } from "vitest"; +import { envApiKeyAuth } from "../src/auth/helpers.ts"; +import type { AuthContext } from "../src/auth/types.ts"; +import { createModels, createProvider } from "../src/models.ts"; +import { builtinModels, builtinProviders } from "../src/providers/all.ts"; +import { amazonBedrockProvider } from "../src/providers/amazon-bedrock.ts"; +import { anthropicProvider } from "../src/providers/anthropic.ts"; +import { fauxAssistantMessage, fauxProvider } from "../src/providers/faux.ts"; +import { googleVertexProvider } from "../src/providers/google-vertex.ts"; +import type { Api, Context, Model, ProviderStreams } from "../src/types.ts"; +import { AssistantMessageEventStream } from "../src/utils/event-stream.ts"; + +function fakeAuthContext(env: Record, files: string[] = []): AuthContext { + return { + env: async (name) => env[name], + fileExists: async (path) => files.includes(path), + }; +} + +const context: Context = { messages: [{ role: "user", content: "hi", timestamp: Date.now() }] }; + +describe("builtin providers", () => { + it("builtinModels registers every builtin provider with models", async () => { + const models = builtinModels(); + const providers = models.getProviders(); + expect(providers.length).toBe(builtinProviders().length); + expect(providers.map((p) => p.id)).toContain("anthropic"); + + const anthropic = await models.getModel("anthropic", "claude-haiku-4-5"); + expect(anthropic?.api).toBe("anthropic-messages"); + + const all = await models.getModels(); + expect(all.length).toBeGreaterThan(500); + + // every provider lists at least one model and owns its models + for (const provider of providers) { + const list = await models.getModels(provider.id); + expect(list.length).toBeGreaterThan(0); + expect(list.every((m) => m.provider === provider.id)).toBe(true); + } + }); + + it("resolves anthropic auth from env with OAuth token precedence", async () => { + const models = createModels({ + authContext: fakeAuthContext({ ANTHROPIC_API_KEY: "key", ANTHROPIC_OAUTH_TOKEN: "oauth-token" }), + }); + models.setProvider(anthropicProvider()); + const model = (await models.getModel("anthropic", "claude-haiku-4-5"))!; + + const result = await models.getAuth(model); + expect(result?.auth.apiKey).toBe("oauth-token"); + expect(result?.source).toBe("ANTHROPIC_OAUTH_TOKEN"); + }); + + it("reports bedrock as configured from ambient AWS credentials without an api key", async () => { + const models = createModels({ authContext: fakeAuthContext({ AWS_PROFILE: "dev" }) }); + models.setProvider(amazonBedrockProvider()); + const model = (await models.getModels("amazon-bedrock"))[0]; + + const result = await models.getAuth(model); + expect(result?.auth).toEqual({}); + expect(result?.source).toBe("AWS_PROFILE"); + + const unconfigured = createModels({ authContext: fakeAuthContext({}) }); + unconfigured.setProvider(amazonBedrockProvider()); + expect(await unconfigured.getAuth(model)).toBeUndefined(); + }); + + it("resolves vertex via ADC file plus project and location", async () => { + const adc = "~/.config/gcloud/application_default_credentials.json"; + const configured = createModels({ + authContext: fakeAuthContext({ GOOGLE_CLOUD_PROJECT: "proj", GOOGLE_CLOUD_LOCATION: "us-central1" }, [adc]), + }); + configured.setProvider(googleVertexProvider()); + const model = (await configured.getModels("google-vertex"))[0]; + + const result = await configured.getAuth(model); + expect(result?.auth).toEqual({}); + expect(result?.source).toContain("application default"); + + // ADC without project/location is not configured + const partial = createModels({ authContext: fakeAuthContext({ GOOGLE_CLOUD_PROJECT: "proj" }, [adc]) }); + partial.setProvider(googleVertexProvider()); + expect(await partial.getAuth(model)).toBeUndefined(); + + // explicit key wins over ADC + const keyed = createModels({ authContext: fakeAuthContext({ GOOGLE_CLOUD_API_KEY: "vertex-key" }) }); + keyed.setProvider(googleVertexProvider()); + expect((await keyed.getAuth(model))?.auth.apiKey).toBe("vertex-key"); + }); +}); + +describe("envApiKeyAuth", () => { + it("prefers the stored credential key and falls back through env vars in order", async () => { + const auth = envApiKeyAuth("Test key", ["FIRST_KEY", "SECOND_KEY"]); + const model = { provider: "p1" } as Model; + + const stored = await auth.resolve({ + model, + ctx: fakeAuthContext({ FIRST_KEY: "env" }), + credential: { type: "api-key", key: "stored" }, + }); + expect(stored?.auth.apiKey).toBe("stored"); + expect(stored?.source).toBe("stored credential"); + + const second = await auth.resolve({ model, ctx: fakeAuthContext({ SECOND_KEY: "second" }) }); + expect(second?.auth.apiKey).toBe("second"); + expect(second?.source).toBe("SECOND_KEY"); + + expect(await auth.resolve({ model, ctx: fakeAuthContext({}) })).toBeUndefined(); + }); + + it("login prompts for a secret and returns an api-key credential", async () => { + const auth = envApiKeyAuth("Test key", ["TEST_KEY"]); + const credential = await auth.login?.({ + prompt: async (prompt) => { + expect(prompt.type).toBe("secret"); + return "entered-key"; + }, + notify: () => {}, + }); + expect(credential).toEqual({ type: "api-key", key: "entered-key" }); + }); +}); + +describe("createProvider", () => { + function recordingStreams(label: string, calls: string[]): ProviderStreams { + const respond = (model: Model) => { + calls.push(`${label}:${model.id}`); + const stream = new AssistantMessageEventStream(); + const message = fauxAssistantMessage("ok"); + stream.push({ type: "start", partial: message }); + stream.push({ type: "done", reason: "stop", message }); + stream.end(message); + return stream; + }; + return { stream: respond, streamSimple: respond }; + } + + function testModel(api: string, id: string): Model { + return { + id, + name: id, + api, + provider: "mixed", + baseUrl: "https://example.test/v1", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 10000, + maxTokens: 1000, + }; + } + + it("dispatches on model.api for mixed-API providers", async () => { + const calls: string[] = []; + const provider = createProvider({ + id: "mixed", + auth: { apiKey: { name: "Test", resolve: async () => ({ auth: {} }) } }, + models: [testModel("api-a", "model-a"), testModel("api-b", "model-b")], + api: { "api-a": recordingStreams("a", calls), "api-b": recordingStreams("b", calls) }, + }); + const models = createModels(); + models.setProvider(provider); + + await models.completeSimple(testModel("api-a", "model-a"), context); + await models.completeSimple(testModel("api-b", "model-b"), context); + expect(calls).toEqual(["a:model-a", "b:model-b"]); + }); + + it("produces a stream error for a model whose api has no implementation", async () => { + const provider = createProvider({ + id: "mixed", + auth: { apiKey: { name: "Test", resolve: async () => ({ auth: {} }) } }, + models: [testModel("api-a", "model-a")], + api: { "api-a": recordingStreams("a", []) }, + }); + const result = await provider.streamSimple(testModel("api-ghost", "model-x"), context).result(); + expect(result.stopReason).toBe("error"); + expect(result.errorMessage).toContain("no API implementation"); + }); + + it("supports async model listers", async () => { + const provider = createProvider({ + id: "dynamic", + auth: { apiKey: { name: "Test", resolve: async () => ({ auth: {} }) } }, + models: async () => [testModel("api-a", "listed")], + api: recordingStreams("a", []), + }); + const models = await provider.getModels(); + expect(models.map((m) => m.id)).toEqual(["listed"]); + }); +}); + +describe("fauxProvider", () => { + it("streams queued responses through a Models collection", async () => { + const faux = fauxProvider(); + const models = createModels(); + models.setProvider(faux.provider); + faux.setResponses([fauxAssistantMessage("hello from faux")]); + + const model = (await models.getModels(faux.provider.id))[0]; + const result = await models.completeSimple(model, context); + expect(result.stopReason).toBe("stop"); + expect(result.content).toEqual([{ type: "text", text: "hello from faux" }]); + expect(faux.state.callCount).toBe(1); + }); +}); diff --git a/packages/ai/test/scratch.ts b/packages/ai/test/scratch.ts index e41af118..5e470cbc 100644 --- a/packages/ai/test/scratch.ts +++ b/packages/ai/test/scratch.ts @@ -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, { From 4d5c015820595a3a60a46d468c7814436283dd62 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 10 Jun 2026 20:41:29 +0200 Subject: [PATCH 010/122] feat(ai): adapt OAuth flows to OAuthAuth (phase 4) anthropic, openai-codex, and github-copilot flow modules gain OAuthAuth exports (login/refresh/toAuth) wired to the prompt()/notify() login callbacks, making the lazyOAuth attachments on the provider factories functional. Copilot's modifyModels baseUrl rewriting becomes toAuth() returning ModelAuth.baseUrl derived from the token proxy endpoint. Callback-server flows race a manual_code prompt and abort it through AuthPrompt.signal once the flow settles; OAuthAuth has no usesCallbackServer flag. The old OAuthProviderInterface exports stay unchanged until the coding-agent migration. --- packages/agent/docs/models.md | 4 +- packages/ai/src/utils/oauth/anthropic.ts | 37 ++++ packages/ai/src/utils/oauth/github-copilot.ts | 37 ++++ packages/ai/src/utils/oauth/openai-codex.ts | 57 +++++++ packages/ai/test/oauth-auth.test.ts | 160 ++++++++++++++++++ 5 files changed, 293 insertions(+), 2 deletions(-) create mode 100644 packages/ai/test/oauth-auth.test.ts diff --git a/packages/agent/docs/models.md b/packages/agent/docs/models.md index de3c3027..0925072e 100644 --- a/packages/agent/docs/models.md +++ b/packages/agent/docs/models.md @@ -807,8 +807,8 @@ Check items off as they land. Keep this list current; it is the working state fo ### Phase 4 — OAuth adaptation -- [ ] Adapt `utils/oauth/anthropic.ts`, `openai-codex.ts`, `github-copilot.ts` to `OAuthAuth` (`login`/`refresh`/`toAuth`) + `prompt()/notify()`; `modifyModels` baseUrl rewriting becomes `toAuth().baseUrl`. -- [ ] Remove `usesCallbackServer`; callback-server flows race a `manual_code` prompt instead. +- [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. ### Phase 5 — packaging diff --git a/packages/ai/src/utils/oauth/anthropic.ts b/packages/ai/src/utils/oauth/anthropic.ts index feeb34fa..c8a9226a 100644 --- a/packages/ai/src/utils/oauth/anthropic.ts +++ b/packages/ai/src/utils/oauth/anthropic.ts @@ -6,6 +6,7 @@ */ import type { Server } from "node:http"; +import type { OAuthAuth } from "../../auth/types.ts"; import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.ts"; import { generatePKCE } from "./pkce.ts"; import type { OAuthCredentials, OAuthLoginCallbacks, OAuthPrompt, OAuthProviderInterface } from "./types.ts"; @@ -378,6 +379,42 @@ export async function refreshAnthropicToken(refreshToken: string): Promise callbacks.notify({ type: "auth_url", url: info.url, instructions: info.instructions }), + onProgress: (message) => callbacks.notify({ type: "progress", message }), + onPrompt: (prompt) => + callbacks.prompt({ type: "text", message: prompt.message, placeholder: prompt.placeholder }), + onManualCodeInput: () => + callbacks.prompt({ + type: "manual_code", + message: "Complete login in your browser, or paste the authorization code / redirect URL here:", + placeholder: REDIRECT_URI, + signal: manualAbort.signal, + }), + }); + return { ...credentials, type: "oauth" }; + } finally { + manualAbort.abort(); + } + }, + + async refresh(credential) { + return { ...(await refreshAnthropicToken(credential.refresh)), type: "oauth" }; + }, + + async toAuth(credential) { + return { apiKey: credential.access }; + }, +}; + export const anthropicOAuthProvider: OAuthProviderInterface = { id: "anthropic", name: "Anthropic (Claude Pro/Max)", diff --git a/packages/ai/src/utils/oauth/github-copilot.ts b/packages/ai/src/utils/oauth/github-copilot.ts index 6a27b9d1..cf7e293b 100644 --- a/packages/ai/src/utils/oauth/github-copilot.ts +++ b/packages/ai/src/utils/oauth/github-copilot.ts @@ -2,6 +2,7 @@ * GitHub Copilot OAuth flow */ +import type { OAuthAuth, OAuthCredential } from "../../auth/types.ts"; import { getModels } from "../../models.ts"; import type { Api, Model } from "../../types.ts"; import { pollOAuthDeviceCodeFlow } from "./device-code.ts"; @@ -330,6 +331,42 @@ export async function loginGitHubCopilot(options: { return credentials; } +function copilotEnterpriseDomain(credential: OAuthCredential): string | undefined { + const enterpriseUrl = credential.enterpriseUrl; + if (typeof enterpriseUrl !== "string" || !enterpriseUrl) return undefined; + return normalizeDomain(enterpriseUrl) ?? undefined; +} + +export const githubCopilotOAuth: OAuthAuth = { + name: "GitHub Copilot", + + async login(callbacks) { + const credentials = await loginGitHubCopilot({ + onDeviceCode: (info) => callbacks.notify({ type: "device_code", ...info }), + onPrompt: (prompt) => + callbacks.prompt({ type: "text", message: prompt.message, placeholder: prompt.placeholder }), + onProgress: (message) => callbacks.notify({ type: "progress", message }), + signal: callbacks.signal, + }); + return { ...credentials, type: "oauth" }; + }, + + async refresh(credential) { + return { + ...(await refreshGitHubCopilotToken(credential.refresh, copilotEnterpriseDomain(credential))), + type: "oauth", + }; + }, + + /** Per-credential baseUrl from the token's proxy endpoint replaces the old `modifyModels` rewriting. */ + async toAuth(credential) { + return { + apiKey: credential.access, + baseUrl: getGitHubCopilotBaseUrl(credential.access, copilotEnterpriseDomain(credential)), + }; + }, +}; + export const githubCopilotOAuthProvider: OAuthProviderInterface = { id: "github-copilot", name: "GitHub Copilot", diff --git a/packages/ai/src/utils/oauth/openai-codex.ts b/packages/ai/src/utils/oauth/openai-codex.ts index 2f769a84..ae9cc9f9 100644 --- a/packages/ai/src/utils/oauth/openai-codex.ts +++ b/packages/ai/src/utils/oauth/openai-codex.ts @@ -17,6 +17,7 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version }); } +import type { OAuthAuth } from "../../auth/types.ts"; import { pollOAuthDeviceCodeFlow } from "./device-code.ts"; import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.ts"; import { generatePKCE } from "./pkce.ts"; @@ -560,6 +561,62 @@ export async function refreshOpenAICodexToken(refreshToken: string): Promise callbacks.notify({ type: "device_code", ...info }), + signal: callbacks.signal, + }); + return { ...credentials, type: "oauth" }; + } + if (method !== OPENAI_CODEX_BROWSER_LOGIN_METHOD) { + throw new Error(`Unknown OpenAI Codex login method: ${method}`); + } + + // The manual_code prompt races the local callback server; abort it once + // the flow settles so the UI can dismiss the pending input. + const manualAbort = new AbortController(); + try { + const credentials = await loginOpenAICodex({ + onAuth: (info) => callbacks.notify({ type: "auth_url", url: info.url, instructions: info.instructions }), + onProgress: (message) => callbacks.notify({ type: "progress", message }), + onPrompt: (prompt) => + callbacks.prompt({ type: "text", message: prompt.message, placeholder: prompt.placeholder }), + onManualCodeInput: () => + callbacks.prompt({ + type: "manual_code", + message: "Complete login in your browser, or paste the authorization code / redirect URL here:", + placeholder: REDIRECT_URI, + signal: manualAbort.signal, + }), + }); + return { ...credentials, type: "oauth" }; + } finally { + manualAbort.abort(); + } + }, + + async refresh(credential) { + return { ...(await refreshOpenAICodexToken(credential.refresh)), type: "oauth" }; + }, + + async toAuth(credential) { + return { apiKey: credential.access }; + }, +}; + export const openaiCodexOAuthProvider: OAuthProviderInterface = { id: "openai-codex", name: "ChatGPT Plus/Pro (Codex Subscription)", diff --git a/packages/ai/test/oauth-auth.test.ts b/packages/ai/test/oauth-auth.test.ts new file mode 100644 index 00000000..689fc2cf --- /dev/null +++ b/packages/ai/test/oauth-auth.test.ts @@ -0,0 +1,160 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { InMemoryCredentialStore } from "../src/auth/credential-store.ts"; +import type { AuthEvent, AuthPrompt } from "../src/auth/types.ts"; +import { createModels } from "../src/models.ts"; +import { anthropicProvider } from "../src/providers/anthropic.ts"; +import { githubCopilotProvider } from "../src/providers/github-copilot.ts"; +import { anthropicOAuth } from "../src/utils/oauth/anthropic.ts"; +import { githubCopilotOAuth } from "../src/utils/oauth/github-copilot.ts"; +import { openaiCodexOAuth } from "../src/utils/oauth/openai-codex.ts"; + +function jsonResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } }); +} + +describe.sequential("OAuthAuth adapters", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("anthropic toAuth derives the api key from the access token", async () => { + const auth = await anthropicOAuth.toAuth({ type: "oauth", access: "token", refresh: "r", expires: 0 }); + expect(auth).toEqual({ apiKey: "token" }); + }); + + it("openai-codex toAuth derives the api key from the access token", async () => { + const auth = await openaiCodexOAuth.toAuth({ type: "oauth", access: "token", refresh: "r", expires: 0 }); + expect(auth).toEqual({ apiKey: "token" }); + }); + + it("github-copilot toAuth derives baseUrl from the token proxy endpoint", async () => { + const access = "tid=abc;exp=123;proxy-ep=proxy.enterprise.example;rest"; + const auth = await githubCopilotOAuth.toAuth({ type: "oauth", access, refresh: "r", expires: 0 }); + expect(auth).toEqual({ apiKey: access, baseUrl: "https://api.enterprise.example" }); + }); + + it("github-copilot toAuth falls back to the enterprise domain, then the individual endpoint", async () => { + const enterprise = await githubCopilotOAuth.toAuth({ + type: "oauth", + access: "no-proxy-ep", + refresh: "r", + expires: 0, + enterpriseUrl: "https://company.ghe.com", + }); + expect(enterprise.baseUrl).toBe("https://copilot-api.company.ghe.com"); + + const individual = await githubCopilotOAuth.toAuth({ + type: "oauth", + access: "no-proxy-ep", + refresh: "r", + expires: 0, + }); + expect(individual.baseUrl).toBe("https://api.individual.githubcopilot.com"); + }); + + it("anthropic refresh exchanges the refresh token and returns a typed credential", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => + jsonResponse({ access_token: "new-access", refresh_token: "new-refresh", expires_in: 3600 }), + ), + ); + + const refreshed = await anthropicOAuth.refresh({ type: "oauth", access: "old", refresh: "old-r", expires: 0 }); + expect(refreshed.type).toBe("oauth"); + expect(refreshed.access).toBe("new-access"); + expect(refreshed.refresh).toBe("new-refresh"); + expect(refreshed.expires).toBeGreaterThan(Date.now()); + }); + + it("github-copilot refresh preserves the enterprise domain", async () => { + const fetchedUrls: string[] = []; + const fetchMock = vi.fn(async (input: unknown) => { + fetchedUrls.push(typeof input === "string" ? input : String(input)); + return jsonResponse({ token: "new-token", expires_at: 9999999999 }); + }); + vi.stubGlobal("fetch", fetchMock); + + const refreshed = await githubCopilotOAuth.refresh({ + type: "oauth", + access: "old", + refresh: "gh-token", + expires: 0, + enterpriseUrl: "company.ghe.com", + }); + expect(refreshed.access).toBe("new-token"); + expect(refreshed.enterpriseUrl).toBe("company.ghe.com"); + expect(fetchedUrls[0]).toContain("api.company.ghe.com"); + }); + + it("anthropic login resolves through the manual_code prompt and aborts it after settling", async () => { + const fetchMock = vi.fn(async (input: unknown) => { + const url = typeof input === "string" ? input : String(input); + if (url.includes("/oauth/token")) { + return jsonResponse({ access_token: "access", refresh_token: "refresh", expires_in: 3600 }); + } + throw new Error(`Unexpected fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + + const events: AuthEvent[] = []; + const prompts: AuthPrompt[] = []; + let manualSignal: AbortSignal | undefined; + + const credential = await anthropicOAuth.login({ + notify: (event) => events.push(event), + prompt: async (prompt) => { + prompts.push(prompt); + if (prompt.type === "manual_code") { + manualSignal = prompt.signal; + return "the-code"; + } + throw new Error(`Unexpected prompt: ${prompt.type}`); + }, + }); + + expect(credential.type).toBe("oauth"); + expect(credential.access).toBe("access"); + expect(events.some((e) => e.type === "auth_url")).toBe(true); + expect(prompts.some((p) => p.type === "manual_code")).toBe(true); + // the prompt's signal is aborted once login settles, so UIs can dismiss it + expect(manualSignal?.aborted).toBe(true); + }); +}); + +describe("OAuth through Models.getAuth (lazy load chain)", () => { + it("resolves stored anthropic oauth credentials via the lazy flow import", async () => { + const credentials = new InMemoryCredentialStore(); + await credentials.modify("anthropic", async () => ({ + type: "oauth", + access: "oauth-access-token", + refresh: "r", + expires: Date.now() + 60_000, + })); + const models = createModels({ credentials }); + models.setProvider(anthropicProvider()); + + const model = (await models.getModels("anthropic"))[0]; + const result = await models.getAuth(model); + expect(result?.auth.apiKey).toBe("oauth-access-token"); + expect(result?.source).toBe("OAuth"); + }); + + it("resolves stored github-copilot oauth credentials including per-credential baseUrl", async () => { + const access = "tid=abc;exp=123;proxy-ep=proxy.business.githubcopilot.com;rest"; + const credentials = new InMemoryCredentialStore(); + await credentials.modify("github-copilot", async () => ({ + type: "oauth", + access, + refresh: "r", + expires: Date.now() + 60_000, + })); + const models = createModels({ credentials }); + models.setProvider(githubCopilotProvider()); + + const model = (await models.getModels("github-copilot"))[0]; + const result = await models.getAuth(model); + expect(result?.auth.apiKey).toBe(access); + expect(result?.auth.baseUrl).toBe("https://api.business.githubcopilot.com"); + }); +}); From 8a0903ebf2db9d708b3d733119638f7407cdef49 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 10 Jun 2026 21:17:12 +0200 Subject: [PATCH 011/122] feat(ai): compat entrypoint, core-only root barrel (phase 5) The root barrel is now core-only and side-effect free: types, createModels/createProvider, auth substrate, lazyStream/lazyApi, faux, utils. Generated catalogs, api-registry, env-api-keys, images, global stream functions, and per-API lazy wrappers leave the root. New @earendil-works/pi-ai/compat preserves the old surface verbatim as a strict superset of the root: api-dispatch stream/complete with env key injection, the builtin registration side effect (skip-if-present so it cannot clobber earlier overrides), deprecated getModel/getModels/ getProviders aliases of the new getBuiltin* reads in providers/all, lazy api wrappers + setBedrockProviderModule, and image generation. Compat dies with the coding-agent ModelManager migration. Packaging: exports map gains ./compat, ./providers/*, ./api/*; sideEffects array lists only the effectful modules. Old-global imports across agent/coding-agent/examples and pi-ai tests switch to /compat (path-only; compat is a superset). The coding-agent extension loader resolves the pi-ai ROOT specifier to compat, so existing user extensions using the old global API keep working at runtime until compat is removed. vitest configs alias /compat to src; browser smoke imports old globals from /compat. --- packages/agent/docs/models.md | 26 ++++++----- packages/agent/src/agent-loop.ts | 2 +- packages/agent/src/agent.ts | 2 +- packages/agent/src/harness/agent-harness.ts | 2 +- .../compaction/branch-summarization.ts | 4 +- .../src/harness/compaction/compaction.ts | 4 +- packages/agent/src/types.ts | 2 +- packages/agent/test/agent.test.ts | 2 +- .../agent/test/harness/agent-harness.test.ts | 2 +- packages/agent/test/scratch/simple.ts | 2 +- packages/agent/vitest.config.ts | 10 +++++ packages/agent/vitest.harness.config.ts | 10 +++++ packages/ai/package.json | 17 +++++++ packages/ai/src/{stream.ts => compat.ts} | 45 ++++++++++++++++++- packages/ai/src/index.ts | 21 +++------ packages/ai/src/models.ts | 37 --------------- packages/ai/src/providers/all.ts | 33 +++++++++++--- packages/ai/src/utils/oauth/github-copilot.ts | 4 +- packages/ai/test/abort.test.ts | 3 +- ...anthropic-adaptive-thinking-models.test.ts | 2 +- .../anthropic-eager-tool-input-e2e.test.ts | 3 +- ...ic-empty-thinking-signature-compat.test.ts | 2 +- .../anthropic-force-adaptive-thinking.test.ts | 3 +- ...anthropic-long-cache-retention-e2e.test.ts | 3 +- packages/ai/test/anthropic-oauth.test.ts | 37 ++++++++++++++- .../ai/test/anthropic-opus-4-8-smoke.test.ts | 3 +- .../ai/test/anthropic-sse-parsing.test.ts | 2 +- .../test/anthropic-temperature-compat.test.ts | 3 +- .../test/anthropic-thinking-disable.test.ts | 3 +- .../anthropic-tool-name-normalization.test.ts | 3 +- .../ai/test/azure-openai-base-url.test.ts | 2 +- .../ai/test/bedrock-convert-messages.test.ts | 2 +- .../ai/test/bedrock-custom-headers.test.ts | 2 +- .../test/bedrock-endpoint-resolution.test.ts | 2 +- packages/ai/test/bedrock-models.test.ts | 3 +- .../ai/test/bedrock-thinking-payload.test.ts | 2 +- packages/ai/test/cache-retention.test.ts | 3 +- .../ai/test/codex-websocket-cached-probe.ts | 2 +- packages/ai/test/context-overflow.test.ts | 3 +- .../ai/test/cross-provider-handoff.test.ts | 3 +- packages/ai/test/empty.test.ts | 3 +- packages/ai/test/faux-provider.test.ts | 2 +- packages/ai/test/fireworks-models.test.ts | 2 +- .../ai/test/github-copilot-anthropic.test.ts | 2 +- .../ai/test/google-thinking-disable.test.ts | 3 +- .../google-vertex-api-key-resolution.test.ts | 2 +- packages/ai/test/image-tool-result.test.ts | 4 +- packages/ai/test/interleaved-thinking.test.ts | 3 +- packages/ai/test/lazy-module-load.test.ts | 16 +++++-- .../ai/test/mistral-reasoning-mode.test.ts | 3 +- packages/ai/test/mistral-tool-schema.test.ts | 3 +- packages/ai/test/oauth-auth.test.ts | 35 --------------- .../openai-codex-cache-affinity-e2e.test.ts | 3 +- ...i-completions-cache-control-format.test.ts | 2 +- .../openai-completions-empty-tools.test.ts | 3 +- .../openai-completions-prompt-cache.test.ts | 2 +- .../openai-completions-response-model.test.ts | 2 +- .../openai-completions-tool-choice.test.ts | 3 +- ...nai-completions-tool-result-images.test.ts | 2 +- ...penai-responses-cache-affinity-e2e.test.ts | 3 +- .../openai-responses-copilot-provider.test.ts | 2 +- ...enai-responses-foreign-toolcall-id.test.ts | 2 +- .../test/openai-responses-message-id.test.ts | 2 +- ...nai-responses-reasoning-replay-e2e.test.ts | 3 +- ...penai-responses-tool-result-images.test.ts | 4 +- .../test/openrouter-cache-write-repro.test.ts | 3 +- packages/ai/test/responseid.test.ts | 3 +- packages/ai/test/stream.test.ts | 3 +- packages/ai/test/supports-xhigh.test.ts | 2 +- packages/ai/test/together-models.test.ts | 2 +- packages/ai/test/tokens.test.ts | 3 +- .../test/tool-call-id-normalization.test.ts | 3 +- .../ai/test/tool-call-without-result.test.ts | 3 +- packages/ai/test/total-tokens.test.ts | 3 +- packages/ai/test/unicode-surrogate.test.ts | 3 +- packages/ai/test/xhigh.test.ts | 3 +- packages/ai/test/xiaomi-models.test.ts | 2 +- ...ms-anthropic-empty-signature-smoke.test.ts | 2 +- packages/ai/test/zen.test.ts | 2 +- .../examples/extensions/custom-compaction.ts | 2 +- .../custom-provider-gitlab-duo/index.ts | 2 +- .../custom-provider-gitlab-duo/test.ts | 2 +- .../examples/extensions/handoff.ts | 2 +- .../coding-agent/examples/extensions/qna.ts | 2 +- .../examples/extensions/summarize.ts | 2 +- .../examples/sdk/02-custom-model.ts | 2 +- .../examples/sdk/12-full-control.ts | 2 +- .../coding-agent/src/bun/register-bedrock.ts | 2 +- .../coding-agent/src/core/agent-session.ts | 4 +- .../coding-agent/src/core/auth-storage.ts | 2 +- .../core/compaction/branch-summarization.ts | 4 +- .../src/core/compaction/compaction.ts | 4 +- .../src/core/extensions/loader.ts | 22 ++++++--- .../coding-agent/src/core/model-registry.ts | 2 +- packages/coding-agent/src/core/sdk.ts | 2 +- .../src/modes/interactive/interactive-mode.ts | 2 +- ...gent-session-auto-compaction-queue.test.ts | 2 +- .../test/agent-session-branching.test.ts | 2 +- .../test/agent-session-compaction.test.ts | 2 +- .../test/agent-session-concurrent.test.ts | 2 +- .../agent-session-dynamic-provider.test.ts | 2 +- .../test/agent-session-dynamic-tools.test.ts | 2 +- .../test/agent-session-retry.test.ts | 2 +- .../test/agent-session-stats.test.ts | 2 +- .../test/compaction-extensions.test.ts | 2 +- .../test/compaction-summary-reasoning.test.ts | 4 +- packages/coding-agent/test/compaction.test.ts | 4 +- .../coding-agent/test/model-registry.test.ts | 10 ++++- .../rpc-prompt-response-semantics.test.ts | 2 +- .../test/sdk-codex-cache-probe-tool-loop.ts | 2 +- .../test/sdk-session-manager.test.ts | 2 +- ...-allowlist-filters-extension-tools.test.ts | 2 +- ...uiltin-tools-keeps-extension-tools.test.ts | 2 +- packages/coding-agent/test/utilities.ts | 2 +- packages/coding-agent/vitest.config.ts | 2 + scripts/browser-smoke-entry.ts | 3 +- 116 files changed, 316 insertions(+), 261 deletions(-) rename packages/ai/src/{stream.ts => compat.ts} (62%) diff --git a/packages/agent/docs/models.md b/packages/agent/docs/models.md index 0925072e..cffcecd7 100644 --- a/packages/agent/docs/models.md +++ b/packages/agent/docs/models.md @@ -686,16 +686,17 @@ Built-in provider factories use `createProvider()` internally. models.json custo `@earendil-works/pi-ai/compat` preserves the old global API surface until the coding-agent migration deletes it. New code never imports it. -Old semantics being preserved: global `stream()` dispatched purely on `model.api` via the api-registry, with env API key injection. The compat module reproduces this: +Old semantics being preserved: global `stream()` dispatched purely on `model.api` via the api-registry, with env API key injection. The compat module reproduces this exactly — it does not route through a `Models` collection, so compat consumers get zero behavioral drift (a `Models`-routed variant was considered and dropped: a model with a known provider id but a different api would dispatch wrong, and auth semantics would shift mid-migration). The harness `Models` instance (Phase 6/7) is where new-path streaming happens. -- Lazily creates a default `Models` singleton from `builtinModels()` on first use. -- `stream/complete/streamSimple/completeSimple(model, ctx, opts)`: look up `getProvider(model.provider)`; if found, route through the singleton (auth resolution included). If not found (custom models.json/extension models), fall back to api-dispatch through a hidden `createProvider()` map containing all builtin API implementations plus anything registered via compat `registerApiProvider()`. -- `registerApiProvider()/unregisterApiProviders()` feed that fallback dispatch map. `api-registry.ts` dies as a real mechanism. -- Sync `getModel/getModels/getProviders` become deprecated aliases of `getBuiltinModel/getBuiltinModels/getBuiltinProviders` (they were always pure generated-catalog reads — verified: nothing ever mutated the old `modelRegistry`). -- Re-exports `setBedrockProviderModule` from the bedrock lazy wrapper. -- `getEnvApiKey`/`env-api-keys.ts` stays available from compat only; provider auth methods own env lookup in the new design. +- `stream/complete/streamSimple/completeSimple(model, ctx, opts)`: api-dispatch via the api-registry plus `getEnvApiKey` injection, verbatim old behavior. +- The builtin api registration side effect moves from the root barrel into compat. It skips api ids that already have a registration, since compat may load after a test or extension registered an override. `registerApiProvider()/unregisterApiProviders()` keep feeding the registry; `resetApiProviders()` clears and re-registers builtins. +- Sync `getModel/getModels/getProviders` are deprecated aliases of `getBuiltinModel/getBuiltinModels/getBuiltinProviders` from `providers/all` (they were always pure generated-catalog reads — verified: nothing ever mutated the old `modelRegistry`). +- Re-exports the per-API lazy stream wrappers (incl. `setBedrockProviderModule`), `env-api-keys.ts`, and the image-generation registry/catalogs; none of these stay on the root barrel. +- `export * from "./index.ts"`: compat is a strict superset of the core entrypoint, so consumers switch a file's import path wholesale without symbol surgery. -coding-agent switches imports of these symbols from `@earendil-works/pi-ai` to `@earendil-works/pi-ai/compat` (import-path-only change) and is otherwise untouched until the ModelManager migration. +coding-agent (and the interim agent package) switch imports of these symbols from `@earendil-works/pi-ai` to `@earendil-works/pi-ai/compat` (import-path-only change) and are otherwise untouched until the ModelManager migration. + +Extension grace period: the coding-agent extension loader (jiti aliases + Bun `virtualModules`) resolves the `@earendil-works/pi-ai` ROOT specifier to the compat entrypoint. Existing user extensions using the old global API (`complete`, `getModel`, `registerApiProvider`, ...) keep working at runtime without changes; they break only when compat is removed at the ModelManager migration, with a migration guide in the changelog. Typechecking is the nudge: editors resolve the root to the slim core types, so extension sources that typecheck must import old globals from `/compat` — which is what the repo example extensions demonstrate. ## Builtin static helpers @@ -812,10 +813,10 @@ Check items off as they land. Keep this list current; it is the working state fo ### Phase 5 — packaging -- [ ] `index.ts` core-only (no catalogs, no provider factories, no OAuth, no compat). -- [ ] `compat.ts`: default builtin singleton, `stream/complete/streamSimple/completeSimple` with api-dispatch fallback, `registerApiProvider`/`unregisterApiProviders`, deprecated `getModel/getModels/getProviders` aliases, `setBedrockProviderModule` re-export, `getEnvApiKey`. -- [ ] Subpath exports map; `sideEffects: false`. -- [ ] Browser smoke + shrinkwrap checks green. +- [x] `index.ts` core-only and side-effect free (no catalogs, no provider factories, no api-registry, no env-api-keys, no images, no OAuth, no compat). Typed catalog reads (`getBuiltin*`) implemented in `providers/all.ts`; `models.ts` no longer imports `models.generated.ts`. +- [x] `compat.ts`: superset of index + old api-dispatch globals, deprecated `getModel/getModels/getProviders` aliases, lazy api wrappers + `setBedrockProviderModule`, `getEnvApiKey`, images. Registration side effect lives here (skip-if-present). +- [x] Subpath exports map (`./compat`, `./providers/*`, `./api/*`); `sideEffects` array listing the effectful modules (`compat`, images registration) instead of `false`. +- [x] Browser smoke (entry now imports old globals from `/compat`) + shrinkwrap checks green. Internal old-global imports switched to `/compat` already (42 files in agent/coding-agent/examples; vitest configs alias `/compat` to src; spawn-CLI tests resolve workspace dist, so `packages/ai` + `packages/agent` dists were rebuilt). ### Phase 6 — AgentHarness @@ -828,6 +829,7 @@ Check items off as they land. Keep this list current; it is the working state fo - [ ] Construct `Models` for the harness (builtins + legacy api-dispatch fallback for ModelRegistry custom providers). - [ ] Switch old-global imports to `@earendil-works/pi-ai/compat`. - [ ] Login dialog adapter for `prompt()/notify()` callbacks. +- [ ] Cloudflare cleanup (only after builtin streaming goes through `Models.getAuth`): the cloudflare provider factories' `ApiKeyAuth.resolve` reads key + `CLOUDFLARE_ACCOUNT_ID` (+ `CLOUDFLARE_GATEWAY_ID`) from credential metadata/env, substitutes the `{...}` placeholders in `model.baseUrl`, and returns it as `ModelAuth.baseUrl` (Copilot pattern); unconfigured ids report "not configured" instead of throwing mid-request. Then `resolveCloudflareBaseUrl`/`isCloudflareProvider` drop out of `api/anthropic-messages.ts`, `api/openai-completions.ts`, and `api/openai-responses.ts`; `api/cloudflare.ts` shrinks to the generator's baseUrl constants. The full AuthStorage deletion (`FileCredentialStore` + decorators, see "Replacing AuthStorage") happens in the later ModelManager migration, not this pass. diff --git a/packages/agent/src/agent-loop.ts b/packages/agent/src/agent-loop.ts index 28f037f5..a3d270df 100644 --- a/packages/agent/src/agent-loop.ts +++ b/packages/agent/src/agent-loop.ts @@ -10,7 +10,7 @@ import { streamSimple, type ToolResultMessage, validateToolArguments, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/compat"; import type { AgentContext, AgentEvent, diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index db6684a8..54020435 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -7,7 +7,7 @@ import { type TextContent, type ThinkingBudgets, type Transport, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/compat"; import { runAgentLoop, runAgentLoopContinue } from "./agent-loop.ts"; import type { AfterToolCallContext, diff --git a/packages/agent/src/harness/agent-harness.ts b/packages/agent/src/harness/agent-harness.ts index 96563465..8d6350a5 100644 --- a/packages/agent/src/harness/agent-harness.ts +++ b/packages/agent/src/harness/agent-harness.ts @@ -4,7 +4,7 @@ import { type Model, streamSimple, type UserMessage, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/compat"; import { runAgentLoop } from "../agent-loop.ts"; import type { AgentContext, diff --git a/packages/agent/src/harness/compaction/branch-summarization.ts b/packages/agent/src/harness/compaction/branch-summarization.ts index c1824ebf..a4563889 100644 --- a/packages/agent/src/harness/compaction/branch-summarization.ts +++ b/packages/agent/src/harness/compaction/branch-summarization.ts @@ -1,5 +1,5 @@ -import type { Model } from "@earendil-works/pi-ai"; -import { completeSimple } from "@earendil-works/pi-ai"; +import type { Model } from "@earendil-works/pi-ai/compat"; +import { completeSimple } from "@earendil-works/pi-ai/compat"; import type { AgentMessage } from "../../types.ts"; import { convertToLlm, diff --git a/packages/agent/src/harness/compaction/compaction.ts b/packages/agent/src/harness/compaction/compaction.ts index dba753d7..93c01e2f 100644 --- a/packages/agent/src/harness/compaction/compaction.ts +++ b/packages/agent/src/harness/compaction/compaction.ts @@ -1,5 +1,5 @@ -import type { AssistantMessage, ImageContent, Model, TextContent, Usage } from "@earendil-works/pi-ai"; -import { completeSimple } from "@earendil-works/pi-ai"; +import type { AssistantMessage, ImageContent, Model, TextContent, Usage } from "@earendil-works/pi-ai/compat"; +import { completeSimple } from "@earendil-works/pi-ai/compat"; import type { AgentMessage, ThinkingLevel } from "../../types.ts"; import { convertToLlm, diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts index f24d2496..3365b304 100644 --- a/packages/agent/src/types.ts +++ b/packages/agent/src/types.ts @@ -9,7 +9,7 @@ import type { TextContent, Tool, ToolResultMessage, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/compat"; import type { Static, TSchema } from "typebox"; /** diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts index 82cc58de..6fa00dd8 100644 --- a/packages/agent/test/agent.test.ts +++ b/packages/agent/test/agent.test.ts @@ -1,4 +1,4 @@ -import { type AssistantMessage, type AssistantMessageEvent, EventStream, getModel } from "@earendil-works/pi-ai"; +import { type AssistantMessage, type AssistantMessageEvent, EventStream, getModel } from "@earendil-works/pi-ai/compat"; import { describe, expect, it } from "vitest"; import { Agent } from "../src/index.ts"; diff --git a/packages/agent/test/harness/agent-harness.test.ts b/packages/agent/test/harness/agent-harness.test.ts index 1d24eb4c..d54040a8 100644 --- a/packages/agent/test/harness/agent-harness.test.ts +++ b/packages/agent/test/harness/agent-harness.test.ts @@ -1,4 +1,4 @@ -import { fauxAssistantMessage, fauxToolCall, getModel, registerFauxProvider } from "@earendil-works/pi-ai"; +import { fauxAssistantMessage, fauxToolCall, getModel, registerFauxProvider } from "@earendil-works/pi-ai/compat"; import { afterEach, describe, expect, it } from "vitest"; import { AgentHarness } from "../../src/harness/agent-harness.ts"; import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts"; diff --git a/packages/agent/test/scratch/simple.ts b/packages/agent/test/scratch/simple.ts index de6ba3a2..6d7bc038 100644 --- a/packages/agent/test/scratch/simple.ts +++ b/packages/agent/test/scratch/simple.ts @@ -1,6 +1,6 @@ import { homedir } from "node:os"; import { join } from "node:path"; -import { getModel } from "@earendil-works/pi-ai"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts"; import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.ts"; import { diff --git a/packages/agent/vitest.config.ts b/packages/agent/vitest.config.ts index bcc497fa..b0f0bb43 100644 --- a/packages/agent/vitest.config.ts +++ b/packages/agent/vitest.config.ts @@ -1,9 +1,19 @@ +import { fileURLToPath } from "node:url"; import { defineConfig } from "vitest/config"; +const aiSrcIndex = fileURLToPath(new URL("../ai/src/index.ts", import.meta.url)); +const aiSrcCompat = fileURLToPath(new URL("../ai/src/compat.ts", import.meta.url)); + export default defineConfig({ test: { globals: true, environment: "node", testTimeout: 30000, // 30 seconds for API calls }, + resolve: { + alias: [ + { find: /^@earendil-works\/pi-ai$/, replacement: aiSrcIndex }, + { find: /^@earendil-works\/pi-ai\/compat$/, replacement: aiSrcCompat }, + ], + }, }); diff --git a/packages/agent/vitest.harness.config.ts b/packages/agent/vitest.harness.config.ts index 9421e5a9..91c0d471 100644 --- a/packages/agent/vitest.harness.config.ts +++ b/packages/agent/vitest.harness.config.ts @@ -1,5 +1,9 @@ +import { fileURLToPath } from "node:url"; import { defineConfig } from "vitest/config"; +const aiSrcIndex = fileURLToPath(new URL("../ai/src/index.ts", import.meta.url)); +const aiSrcCompat = fileURLToPath(new URL("../ai/src/compat.ts", import.meta.url)); + export default defineConfig({ test: { globals: true, @@ -15,4 +19,10 @@ export default defineConfig({ reportsDirectory: "coverage/harness", }, }, + resolve: { + alias: [ + { find: /^@earendil-works\/pi-ai$/, replacement: aiSrcIndex }, + { find: /^@earendil-works\/pi-ai\/compat$/, replacement: aiSrcCompat }, + ], + }, }); diff --git a/packages/ai/package.json b/packages/ai/package.json index 7b361b9a..b4112894 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -5,11 +5,28 @@ "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", + "sideEffects": [ + "./dist/compat.js", + "./dist/images.js", + "./dist/providers/images/register-builtins.js" + ], "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" }, + "./compat": { + "types": "./dist/compat.d.ts", + "import": "./dist/compat.js" + }, + "./providers/*": { + "types": "./dist/providers/*.d.ts", + "import": "./dist/providers/*.js" + }, + "./api/*": { + "types": "./dist/api/*.d.ts", + "import": "./dist/api/*.js" + }, "./anthropic": { "types": "./dist/api/anthropic-messages.d.ts", "import": "./dist/api/anthropic-messages.js" diff --git a/packages/ai/src/stream.ts b/packages/ai/src/compat.ts similarity index 62% rename from packages/ai/src/stream.ts rename to packages/ai/src/compat.ts index f874f03d..7ddbbaaf 100644 --- a/packages/ai/src/stream.ts +++ b/packages/ai/src/compat.ts @@ -1,3 +1,32 @@ +/** + * Temporary compatibility entrypoint preserving the old global pi-ai API + * surface: api-dispatch `stream()`/`complete()` with env API key injection, + * the api-registry, generated catalog reads (`getModel`/`getModels`/ + * `getProviders`), per-API lazy stream wrappers, and image generation. + * + * Existing apps switch imports from "@earendil-works/pi-ai" to + * "@earendil-works/pi-ai/compat" unchanged; new code uses `createModels()` + * and the provider factories. This module is deleted with the coding-agent + * ModelManager migration. + */ + +export * from "./api/anthropic-messages.lazy.ts"; +export * from "./api/azure-openai-responses.lazy.ts"; +export * from "./api/bedrock-converse-stream.lazy.ts"; +export * from "./api/google-generative-ai.lazy.ts"; +export * from "./api/google-vertex.lazy.ts"; +export * from "./api/mistral-conversations.lazy.ts"; +export * from "./api/openai-codex-responses.lazy.ts"; +export * from "./api/openai-completions.lazy.ts"; +export * from "./api/openai-responses.lazy.ts"; +export * from "./api-registry.ts"; +export * from "./env-api-keys.ts"; +export * from "./image-models.ts"; +export * from "./images.ts"; +export * from "./images-api-registry.ts"; +export * from "./index.ts"; +export * from "./providers/images/register-builtins.ts"; + import { anthropicMessagesApi } from "./api/anthropic-messages.lazy.ts"; import { azureOpenAIResponsesApi } from "./api/azure-openai-responses.lazy.ts"; import { bedrockConverseStreamApi } from "./api/bedrock-converse-stream.lazy.ts"; @@ -9,6 +38,7 @@ import { openAICompletionsApi } from "./api/openai-completions.lazy.ts"; import { openAIResponsesApi } from "./api/openai-responses.lazy.ts"; import { clearApiProviders, getApiProvider, registerApiProvider } from "./api-registry.ts"; import { getEnvApiKey } from "./env-api-keys.ts"; +import { getBuiltinModel, getBuiltinModels, getBuiltinProviders } from "./providers/all.ts"; import type { Api, AssistantMessage, @@ -21,7 +51,14 @@ import type { StreamOptions, } from "./types.ts"; -export { getEnvApiKey } from "./env-api-keys.ts"; +/** @deprecated Static catalog read. Use `getBuiltinModel` from "@earendil-works/pi-ai/providers/all" or `Models.getModel()`. */ +export const getModel = getBuiltinModel; + +/** @deprecated Static catalog read. Use `getBuiltinModels` from "@earendil-works/pi-ai/providers/all" or `Models.getModels()`. */ +export const getModels = getBuiltinModels; + +/** @deprecated Static catalog read. Use `getBuiltinProviders` from "@earendil-works/pi-ai/providers/all" or `Models.getProviders()`. */ +export const getProviders = getBuiltinProviders; const BUILTIN_APIS: [Api, ProviderStreams][] = [ ["anthropic-messages", anthropicMessagesApi()], @@ -35,8 +72,14 @@ const BUILTIN_APIS: [Api, ProviderStreams][] = [ ["bedrock-converse-stream", bedrockConverseStreamApi()], ]; +/** + * Registers the builtin API implementations into the api-registry without + * clobbering existing entries: compat may load after a test or extension has + * already registered an override for a builtin api id. + */ export function registerBuiltInApiProviders(): void { for (const [api, streams] of BUILTIN_APIS) { + if (getApiProvider(api)) continue; registerApiProvider({ api, stream: streams.stream, streamSimple: streams.streamSimple }); } } diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index cd531d5b..53a6f719 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -1,40 +1,29 @@ export type { Static, TSchema } from "typebox"; export { Type } from "typebox"; -export * from "./api/anthropic-messages.lazy.ts"; +// Core only, side-effect free: no generated catalogs, no provider factories, +// no api-registry, no OAuth implementations, no compat. Provider factories +// live under "@earendil-works/pi-ai/providers/*", API implementations under +// "@earendil-works/pi-ai/api/*", the old global API under +// "@earendil-works/pi-ai/compat". export type { AnthropicEffort, AnthropicOptions, AnthropicThinkingDisplay } from "./api/anthropic-messages.ts"; -export * from "./api/azure-openai-responses.lazy.ts"; export type { AzureOpenAIResponsesOptions } from "./api/azure-openai-responses.ts"; -export * from "./api/bedrock-converse-stream.lazy.ts"; export type { BedrockOptions, BedrockThinkingDisplay } from "./api/bedrock-converse-stream.ts"; -export * from "./api/google-generative-ai.lazy.ts"; export type { GoogleOptions } from "./api/google-generative-ai.ts"; export type { GoogleThinkingLevel } from "./api/google-shared.ts"; -export * from "./api/google-vertex.lazy.ts"; export type { GoogleVertexOptions } from "./api/google-vertex.ts"; export * from "./api/lazy.ts"; -export * from "./api/mistral-conversations.lazy.ts"; export type { MistralOptions } from "./api/mistral-conversations.ts"; -export * from "./api/openai-codex-responses.lazy.ts"; export type { OpenAICodexResponsesOptions, OpenAICodexWebSocketDebugStats } from "./api/openai-codex-responses.ts"; -export * from "./api/openai-completions.lazy.ts"; export type { OpenAICompletionsOptions } from "./api/openai-completions.ts"; -export * from "./api/openai-responses.lazy.ts"; export type { OpenAIResponsesOptions } from "./api/openai-responses.ts"; -export * from "./api-registry.ts"; export * from "./auth/context.ts"; export * from "./auth/credential-store.ts"; export * from "./auth/helpers.ts"; export * from "./auth/types.ts"; -export * from "./env-api-keys.ts"; -export * from "./image-models.ts"; -export * from "./images.ts"; -export * from "./images-api-registry.ts"; export * from "./models.ts"; export * from "./providers/faux.ts"; -export * from "./providers/images/register-builtins.ts"; export * from "./session-resources.ts"; -export * from "./stream.ts"; export * from "./types.ts"; export * from "./utils/diagnostics.ts"; export * from "./utils/event-stream.ts"; diff --git a/packages/ai/src/models.ts b/packages/ai/src/models.ts index dd698831..6cb7a87d 100644 --- a/packages/ai/src/models.ts +++ b/packages/ai/src/models.ts @@ -12,14 +12,12 @@ import type { OAuthCredential, ProviderAuth, } from "./auth/types.ts"; -import { MODELS } from "./models.generated.ts"; import type { Api, ApiStreamOptions, AssistantMessage, AssistantMessageEventStream, Context, - KnownProvider, Model, ModelThinkingLevel, ProviderStreams, @@ -421,41 +419,6 @@ export function hasApi(model: Model, api: TApi): model is return model.api === api; } -const modelRegistry: Map>> = new Map(); - -// Initialize registry from MODELS on module load -for (const [provider, models] of Object.entries(MODELS)) { - const providerModels = new Map>(); - for (const [id, model] of Object.entries(models)) { - providerModels.set(id, model as Model); - } - modelRegistry.set(provider, providerModels); -} - -type ModelApi< - TProvider extends KnownProvider, - TModelId extends keyof (typeof MODELS)[TProvider], -> = (typeof MODELS)[TProvider][TModelId] extends { api: infer TApi } ? (TApi extends Api ? TApi : never) : never; - -export function getModel( - provider: TProvider, - modelId: TModelId, -): Model> { - const providerModels = modelRegistry.get(provider); - return providerModels?.get(modelId as string) as Model>; -} - -export function getProviders(): KnownProvider[] { - return Array.from(modelRegistry.keys()) as KnownProvider[]; -} - -export function getModels( - provider: TProvider, -): Model>[] { - const models = modelRegistry.get(provider); - return models ? (Array.from(models.values()) as Model>[]) : []; -} - export function calculateCost(model: Model, usage: Usage): Usage["cost"] { usage.cost.input = (model.cost.input / 1000000) * usage.input; usage.cost.output = (model.cost.output / 1000000) * usage.output; diff --git a/packages/ai/src/providers/all.ts b/packages/ai/src/providers/all.ts index 7097fcdd..1c4c70b1 100644 --- a/packages/ai/src/providers/all.ts +++ b/packages/ai/src/providers/all.ts @@ -1,4 +1,6 @@ +import { MODELS } from "../models.generated.ts"; import { type CreateModelsOptions, createModels, type MutableModels, type Provider } from "../models.ts"; +import type { Api, KnownProvider, Model } from "../types.ts"; import { amazonBedrockProvider } from "./amazon-bedrock.ts"; import { antLingProvider } from "./ant-ling.ts"; import { anthropicProvider } from "./anthropic.ts"; @@ -35,11 +37,32 @@ import { xiaomiTokenPlanSgpProvider } from "./xiaomi-token-plan-sgp.ts"; import { zaiProvider } from "./zai.ts"; import { zaiCodingCnProvider } from "./zai-coding-cn.ts"; -export { - getModel as getBuiltinModel, - getModels as getBuiltinModels, - getProviders as getBuiltinProviders, -} from "../models.ts"; +type BuiltinModelApi< + TProvider extends KnownProvider, + TModelId extends keyof (typeof MODELS)[TProvider], +> = (typeof MODELS)[TProvider][TModelId] extends { api: infer TApi } ? (TApi extends Api ? TApi : never) : never; + +/** Typed read of the generated built-in catalog. */ +export function getBuiltinModel( + provider: TProvider, + modelId: TModelId, +): Model> { + const models = MODELS[provider] as Record> | undefined; + return models?.[modelId as string] as Model>; +} + +export function getBuiltinProviders(): KnownProvider[] { + return Object.keys(MODELS) as KnownProvider[]; +} + +export function getBuiltinModels( + provider: TProvider, +): Model>[] { + const models = MODELS[provider] as Record> | undefined; + return models + ? (Object.values(models) as Model>[]) + : []; +} /** All built-in providers, freshly constructed. */ export function builtinProviders(): Provider[] { diff --git a/packages/ai/src/utils/oauth/github-copilot.ts b/packages/ai/src/utils/oauth/github-copilot.ts index cf7e293b..e63741a8 100644 --- a/packages/ai/src/utils/oauth/github-copilot.ts +++ b/packages/ai/src/utils/oauth/github-copilot.ts @@ -3,7 +3,7 @@ */ import type { OAuthAuth, OAuthCredential } from "../../auth/types.ts"; -import { getModels } from "../../models.ts"; +import { GITHUB_COPILOT_MODELS } from "../../providers/github-copilot.models.ts"; import type { Api, Model } from "../../types.ts"; import { pollOAuthDeviceCodeFlow } from "./device-code.ts"; import type { OAuthCredentials, OAuthDeviceCodeInfo, OAuthLoginCallbacks, OAuthProviderInterface } from "./types.ts"; @@ -274,7 +274,7 @@ async function enableAllGitHubCopilotModels( enterpriseDomain?: string, onProgress?: (model: string, success: boolean) => void, ): Promise { - const models = getModels("github-copilot"); + const models = Object.values(GITHUB_COPILOT_MODELS); await Promise.all( models.map(async (model) => { const success = await enableGitHubCopilotModel(token, model.id, enterpriseDomain); diff --git a/packages/ai/test/abort.test.ts b/packages/ai/test/abort.test.ts index 27c274aa..e4424c5e 100644 --- a/packages/ai/test/abort.test.ts +++ b/packages/ai/test/abort.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { complete, stream } from "../src/stream.ts"; +import { complete, getModel, stream } from "../src/compat.ts"; import type { Api, Context, Model, StreamOptions } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/anthropic-adaptive-thinking-models.test.ts b/packages/ai/test/anthropic-adaptive-thinking-models.test.ts index da042e2f..e3cf0601 100644 --- a/packages/ai/test/anthropic-adaptive-thinking-models.test.ts +++ b/packages/ai/test/anthropic-adaptive-thinking-models.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModels, getProviders } from "../src/models.ts"; +import { getModels, getProviders } from "../src/compat.ts"; import type { Api, Model } from "../src/types.ts"; const EXPECTED_CURRENT_ADAPTIVE_THINKING_MODELS = [ diff --git a/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts b/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts index 2483e1a4..680f0648 100644 --- a/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts +++ b/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts @@ -1,8 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; +import { complete, getModels, getProviders } from "../src/compat.ts"; import { getEnvApiKey } from "../src/env-api-keys.ts"; -import { getModels, getProviders } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Api, KnownProvider, Model, ProviderStreamOptions, Tool } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts b/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts index 69e58e27..f5c88368 100644 --- a/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts +++ b/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { streamSimple } from "../src/stream.ts"; +import { streamSimple } from "../src/compat.ts"; import type { AssistantMessage, Context, Model } from "../src/types.ts"; interface AnthropicPayload { diff --git a/packages/ai/test/anthropic-force-adaptive-thinking.test.ts b/packages/ai/test/anthropic-force-adaptive-thinking.test.ts index e797c3a9..6629782f 100644 --- a/packages/ai/test/anthropic-force-adaptive-thinking.test.ts +++ b/packages/ai/test/anthropic-force-adaptive-thinking.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; +import { getModel, streamSimple } from "../src/compat.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface AnthropicThinkingPayload { diff --git a/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts b/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts index 2b7667d7..042d3536 100644 --- a/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts +++ b/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from "vitest"; +import { complete, getModels, getProviders } from "../src/compat.ts"; import { getEnvApiKey } from "../src/env-api-keys.ts"; -import { getModels, getProviders } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Api, KnownProvider, Model, ProviderStreamOptions } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/anthropic-oauth.test.ts b/packages/ai/test/anthropic-oauth.test.ts index 36585a5f..ae3ae093 100644 --- a/packages/ai/test/anthropic-oauth.test.ts +++ b/packages/ai/test/anthropic-oauth.test.ts @@ -1,5 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { loginAnthropic, refreshAnthropicToken } from "../src/utils/oauth/anthropic.ts"; +import type { AuthEvent, AuthPrompt } from "../src/auth/types.ts"; +import { anthropicOAuth, loginAnthropic, refreshAnthropicToken } from "../src/utils/oauth/anthropic.ts"; function jsonResponse(body: unknown, status: number = 200): Response { return new Response(JSON.stringify(body), { @@ -96,4 +97,38 @@ describe.sequential("Anthropic OAuth", () => { expect(credentials.refresh).toBe("new-refresh-token"); expect(fetchMock).toHaveBeenCalledOnce(); }); + + it("anthropicOAuth.login resolves through the manual_code prompt and aborts it after settling", async () => { + const fetchMock = vi.fn(async (input: unknown): Promise => { + const url = typeof input === "string" ? input : String(input); + if (url.includes("/oauth/token")) { + return jsonResponse({ access_token: "access", refresh_token: "refresh", expires_in: 3600 }); + } + throw new Error(`Unexpected fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + + const events: AuthEvent[] = []; + const prompts: AuthPrompt[] = []; + let manualSignal: AbortSignal | undefined; + + const credential = await anthropicOAuth.login({ + notify: (event) => events.push(event), + prompt: async (prompt) => { + prompts.push(prompt); + if (prompt.type === "manual_code") { + manualSignal = prompt.signal; + return "the-code"; + } + throw new Error(`Unexpected prompt: ${prompt.type}`); + }, + }); + + expect(credential.type).toBe("oauth"); + expect(credential.access).toBe("access"); + expect(events.some((e) => e.type === "auth_url")).toBe(true); + expect(prompts.some((p) => p.type === "manual_code")).toBe(true); + // the prompt's signal is aborted once login settles, so UIs can dismiss it + expect(manualSignal?.aborted).toBe(true); + }); }); diff --git a/packages/ai/test/anthropic-opus-4-8-smoke.test.ts b/packages/ai/test/anthropic-opus-4-8-smoke.test.ts index bb4b739b..44fa5aeb 100644 --- a/packages/ai/test/anthropic-opus-4-8-smoke.test.ts +++ b/packages/ai/test/anthropic-opus-4-8-smoke.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; +import { getModel, streamSimple } from "../src/compat.ts"; import type { Context } from "../src/types.ts"; interface AnthropicThinkingPayload { diff --git a/packages/ai/test/anthropic-sse-parsing.test.ts b/packages/ai/test/anthropic-sse-parsing.test.ts index 88943582..0ed58b81 100644 --- a/packages/ai/test/anthropic-sse-parsing.test.ts +++ b/packages/ai/test/anthropic-sse-parsing.test.ts @@ -2,7 +2,7 @@ import type Anthropic from "@anthropic-ai/sdk"; import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts"; -import { getModel } from "../src/models.ts"; +import { getModel } from "../src/compat.ts"; import type { Context, ToolCall } from "../src/types.ts"; function createSseResponse(events: Array<{ event: string; data: string }>): Response { diff --git a/packages/ai/test/anthropic-temperature-compat.test.ts b/packages/ai/test/anthropic-temperature-compat.test.ts index 00161ab8..4b059237 100644 --- a/packages/ai/test/anthropic-temperature-compat.test.ts +++ b/packages/ai/test/anthropic-temperature-compat.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; +import { getModel, streamSimple } from "../src/compat.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface AnthropicTemperaturePayload { diff --git a/packages/ai/test/anthropic-thinking-disable.test.ts b/packages/ai/test/anthropic-thinking-disable.test.ts index 13d333e5..9ecfeb4f 100644 --- a/packages/ai/test/anthropic-thinking-disable.test.ts +++ b/packages/ai/test/anthropic-thinking-disable.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; +import { getModel, streamSimple } from "../src/compat.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface AnthropicThinkingPayload { diff --git a/packages/ai/test/anthropic-tool-name-normalization.test.ts b/packages/ai/test/anthropic-tool-name-normalization.test.ts index bb454081..b8c45049 100644 --- a/packages/ai/test/anthropic-tool-name-normalization.test.ts +++ b/packages/ai/test/anthropic-tool-name-normalization.test.ts @@ -1,7 +1,6 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { stream } from "../src/stream.ts"; +import { getModel, stream } from "../src/compat.ts"; import type { Context, Tool } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/azure-openai-base-url.test.ts b/packages/ai/test/azure-openai-base-url.test.ts index b80bae58..e372168b 100644 --- a/packages/ai/test/azure-openai-base-url.test.ts +++ b/packages/ai/test/azure-openai-base-url.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { stream as streamAzureOpenAIResponses } from "../src/api/azure-openai-responses.ts"; -import { getModel } from "../src/models.ts"; +import { getModel } from "../src/compat.ts"; import type { Context } from "../src/types.ts"; interface CapturedAzureClientOptions { diff --git a/packages/ai/test/bedrock-convert-messages.test.ts b/packages/ai/test/bedrock-convert-messages.test.ts index ba3ff137..c43f7978 100644 --- a/packages/ai/test/bedrock-convert-messages.test.ts +++ b/packages/ai/test/bedrock-convert-messages.test.ts @@ -45,7 +45,7 @@ vi.mock("@aws-sdk/client-bedrock-runtime", () => { }); import { stream as streamBedrock } from "../src/api/bedrock-converse-stream.ts"; -import { getModel } from "../src/models.ts"; +import { getModel } from "../src/compat.ts"; import type { Context, Message } from "../src/types.ts"; const baseModel = getModel("amazon-bedrock", "us.anthropic.claude-sonnet-4-5-20250929-v1:0"); diff --git a/packages/ai/test/bedrock-custom-headers.test.ts b/packages/ai/test/bedrock-custom-headers.test.ts index e0899068..43ee692c 100644 --- a/packages/ai/test/bedrock-custom-headers.test.ts +++ b/packages/ai/test/bedrock-custom-headers.test.ts @@ -53,7 +53,7 @@ vi.mock("@aws-sdk/client-bedrock-runtime", () => { import type { BedrockOptions } from "../src/api/bedrock-converse-stream.ts"; import { stream as streamBedrock, streamSimple as streamSimpleBedrock } from "../src/api/bedrock-converse-stream.ts"; -import { getModel } from "../src/models.ts"; +import { getModel } from "../src/compat.ts"; import type { Context, Model } from "../src/types.ts"; const context: Context = { diff --git a/packages/ai/test/bedrock-endpoint-resolution.test.ts b/packages/ai/test/bedrock-endpoint-resolution.test.ts index 28221d5c..db2ae04b 100644 --- a/packages/ai/test/bedrock-endpoint-resolution.test.ts +++ b/packages/ai/test/bedrock-endpoint-resolution.test.ts @@ -45,7 +45,7 @@ vi.mock("@aws-sdk/client-bedrock-runtime", () => { }); import { stream as streamBedrock } from "../src/api/bedrock-converse-stream.ts"; -import { getModel } from "../src/models.ts"; +import { getModel } from "../src/compat.ts"; import type { Context, Model } from "../src/types.ts"; const context: Context = { diff --git a/packages/ai/test/bedrock-models.test.ts b/packages/ai/test/bedrock-models.test.ts index 2cfd8fb9..08f95e7a 100644 --- a/packages/ai/test/bedrock-models.test.ts +++ b/packages/ai/test/bedrock-models.test.ts @@ -17,8 +17,7 @@ */ import { describe, expect, it } from "vitest"; -import { getModels } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; +import { complete, getModels } from "../src/compat.ts"; import type { Context } from "../src/types.ts"; import { hasBedrockCredentials } from "./bedrock-utils.ts"; diff --git a/packages/ai/test/bedrock-thinking-payload.test.ts b/packages/ai/test/bedrock-thinking-payload.test.ts index ff4509cb..d2de4913 100644 --- a/packages/ai/test/bedrock-thinking-payload.test.ts +++ b/packages/ai/test/bedrock-thinking-payload.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { type BedrockOptions, stream as streamBedrock } from "../src/api/bedrock-converse-stream.ts"; -import { getModel } from "../src/models.ts"; +import { getModel } from "../src/compat.ts"; import type { Context, Model } from "../src/types.ts"; import { hasBedrockCredentials } from "./bedrock-utils.ts"; diff --git a/packages/ai/test/cache-retention.test.ts b/packages/ai/test/cache-retention.test.ts index b3745198..1296cebd 100644 --- a/packages/ai/test/cache-retention.test.ts +++ b/packages/ai/test/cache-retention.test.ts @@ -2,8 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts"; import { stream as streamOpenAICompletions } from "../src/api/openai-completions.ts"; import { stream as streamOpenAIResponses } from "../src/api/openai-responses.ts"; -import { getModel } from "../src/models.ts"; -import { stream } from "../src/stream.ts"; +import { getModel, stream } from "../src/compat.ts"; import type { Context, Model } from "../src/types.ts"; class PayloadCaptured extends Error { diff --git a/packages/ai/test/codex-websocket-cached-probe.ts b/packages/ai/test/codex-websocket-cached-probe.ts index ca08c6c7..d8154ac1 100644 --- a/packages/ai/test/codex-websocket-cached-probe.ts +++ b/packages/ai/test/codex-websocket-cached-probe.ts @@ -16,7 +16,7 @@ import { resetOpenAICodexWebSocketDebugStats, stream as streamOpenAICodexResponses, } from "../src/api/openai-codex-responses.ts"; -import { getModel } from "../src/models.ts"; +import { getModel } from "../src/compat.ts"; import type { AssistantMessage, Context, Message, Model, Tool, ToolResultMessage, Transport } from "../src/types.ts"; type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh"; diff --git a/packages/ai/test/context-overflow.test.ts b/packages/ai/test/context-overflow.test.ts index 9f021a41..68305949 100644 --- a/packages/ai/test/context-overflow.test.ts +++ b/packages/ai/test/context-overflow.test.ts @@ -14,8 +14,7 @@ import type { ChildProcess } from "child_process"; import { execSync, spawn } from "child_process"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { getModel, getModels } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; +import { complete, getModel, getModels } from "../src/compat.ts"; import type { AssistantMessage, Context, Model, Usage } from "../src/types.ts"; import { isContextOverflow } from "../src/utils/overflow.ts"; import { hasAzureOpenAICredentials } from "./azure-utils.ts"; diff --git a/packages/ai/test/cross-provider-handoff.test.ts b/packages/ai/test/cross-provider-handoff.test.ts index 23593394..57a43b0a 100644 --- a/packages/ai/test/cross-provider-handoff.test.ts +++ b/packages/ai/test/cross-provider-handoff.test.ts @@ -25,8 +25,7 @@ import { writeFileSync } from "fs"; import { Type } from "typebox"; import { beforeAll, describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { completeSimple, getEnvApiKey } from "../src/stream.ts"; +import { completeSimple, getEnvApiKey, getModel } from "../src/compat.ts"; import type { Api, AssistantMessage, Message, Model, Tool, ToolResultMessage } from "../src/types.ts"; import { hasAzureOpenAICredentials } from "./azure-utils.ts"; import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.ts"; diff --git a/packages/ai/test/empty.test.ts b/packages/ai/test/empty.test.ts index a8453dcc..86c25aa4 100644 --- a/packages/ai/test/empty.test.ts +++ b/packages/ai/test/empty.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; +import { complete, getModel } from "../src/compat.ts"; import type { Api, AssistantMessage, Context, Model, StreamOptions, UserMessage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/faux-provider.test.ts b/packages/ai/test/faux-provider.test.ts index 4f110c54..3d8a190f 100644 --- a/packages/ai/test/faux-provider.test.ts +++ b/packages/ai/test/faux-provider.test.ts @@ -8,7 +8,7 @@ import { registerFauxProvider, stream, Type, -} from "../src/index.ts"; +} from "../src/compat.ts"; import type { AssistantMessageEvent, Context } from "../src/types.ts"; async function collectEvents(streamResult: ReturnType): Promise { diff --git a/packages/ai/test/fireworks-models.test.ts b/packages/ai/test/fireworks-models.test.ts index 0f7ac4c1..6980f318 100644 --- a/packages/ai/test/fireworks-models.test.ts +++ b/packages/ai/test/fireworks-models.test.ts @@ -3,8 +3,8 @@ import type { AddressInfo } from "node:net"; import { Type } from "typebox"; import { afterEach, describe, expect, it } from "vitest"; import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts"; +import { getModel, getModels } from "../src/compat.ts"; import { findEnvKeys, getEnvApiKey } from "../src/env-api-keys.ts"; -import { getModel, getModels } from "../src/models.ts"; import type { Context, Model, Tool } from "../src/types.ts"; const originalFireworksApiKey = process.env.FIREWORKS_API_KEY; diff --git a/packages/ai/test/github-copilot-anthropic.test.ts b/packages/ai/test/github-copilot-anthropic.test.ts index c1c1a30f..7900d22e 100644 --- a/packages/ai/test/github-copilot-anthropic.test.ts +++ b/packages/ai/test/github-copilot-anthropic.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts"; -import { getModel } from "../src/models.ts"; +import { getModel } from "../src/compat.ts"; import type { Context } from "../src/types.ts"; const mockState = vi.hoisted(() => ({ diff --git a/packages/ai/test/google-thinking-disable.test.ts b/packages/ai/test/google-thinking-disable.test.ts index 30df3638..3f6e36c7 100644 --- a/packages/ai/test/google-thinking-disable.test.ts +++ b/packages/ai/test/google-thinking-disable.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; +import { getModel, streamSimple } from "../src/compat.ts"; import type { Api, Context, Model, SimpleStreamOptions } from "../src/types.ts"; type SimpleOptionsWithExtras = SimpleStreamOptions & Record; diff --git a/packages/ai/test/google-vertex-api-key-resolution.test.ts b/packages/ai/test/google-vertex-api-key-resolution.test.ts index 7fa182f9..46f24a77 100644 --- a/packages/ai/test/google-vertex-api-key-resolution.test.ts +++ b/packages/ai/test/google-vertex-api-key-resolution.test.ts @@ -46,7 +46,7 @@ vi.mock("@google/genai", () => { }); import { stream as streamGoogleVertex } from "../src/api/google-vertex.ts"; -import { getModel } from "../src/models.ts"; +import { getModel } from "../src/compat.ts"; import type { Context, Model } from "../src/types.ts"; const model = getModel("google-vertex", "gemini-3-flash-preview"); diff --git a/packages/ai/test/image-tool-result.test.ts b/packages/ai/test/image-tool-result.test.ts index a752b7ce..946a443a 100644 --- a/packages/ai/test/image-tool-result.test.ts +++ b/packages/ai/test/image-tool-result.test.ts @@ -2,8 +2,8 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import type { Api, Context, Model, Tool, ToolResultMessage } from "../src/index.ts"; -import { complete, getModel } from "../src/index.ts"; +import type { Api, Context, Model, Tool, ToolResultMessage } from "../src/compat.ts"; +import { complete, getModel } from "../src/compat.ts"; import type { StreamOptions } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/interleaved-thinking.test.ts b/packages/ai/test/interleaved-thinking.test.ts index 4cf387ce..b4da1283 100644 --- a/packages/ai/test/interleaved-thinking.test.ts +++ b/packages/ai/test/interleaved-thinking.test.ts @@ -1,8 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; +import { completeSimple, getModel } from "../src/compat.ts"; import { getEnvApiKey } from "../src/env-api-keys.ts"; -import { getModel } from "../src/models.ts"; -import { completeSimple } from "../src/stream.ts"; import type { Api, Context, Model, StopReason, Tool, ToolCall, ToolResultMessage } from "../src/types.ts"; import { StringEnum } from "../src/utils/typebox-helpers.ts"; import { hasBedrockCredentials } from "./bedrock-utils.ts"; diff --git a/packages/ai/test/lazy-module-load.test.ts b/packages/ai/test/lazy-module-load.test.ts index aa25d9a1..6f5464ff 100644 --- a/packages/ai/test/lazy-module-load.test.ts +++ b/packages/ai/test/lazy-module-load.test.ts @@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest"; const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const aiEntryUrl = new URL("../src/index.ts", import.meta.url).href; +const compatEntryUrl = new URL("../src/compat.ts", import.meta.url).href; const providersAllUrl = new URL("../src/providers/all.ts", import.meta.url).href; const SDK_SPECIFIERS = [ @@ -76,8 +77,16 @@ describe("lazy provider module loading", () => { expect(result.loadedSpecifiers).toEqual([]); }); + it("does not load provider SDKs when importing the compat entrypoint", () => { + const result = runProbe(` + await import(${JSON.stringify(compatEntryUrl)}); + `); + expect(result.loadedSpecifiers).toEqual([]); + }); + it("loads only the Anthropic SDK when streaming through the lazy API wrapper", () => { const result = runProbe(` + const compat = await import(${JSON.stringify(compatEntryUrl)}); const model = { id: "claude-sonnet-4-6", name: "Claude Sonnet 4", @@ -91,7 +100,7 @@ describe("lazy provider module loading", () => { maxTokens: 8192, }; const context = { messages: [{ role: "user", content: "hi" }] }; - await mod.anthropicMessagesApi().streamSimple(model, context).result(); + await compat.anthropicMessagesApi().streamSimple(model, context).result(); `); expect(result.loadedSpecifiers).toEqual(["@anthropic-ai/sdk"]); @@ -99,9 +108,10 @@ describe("lazy provider module loading", () => { it("loads only the Anthropic SDK when dispatching through streamSimple", () => { const result = runProbe(` - const model = mod.getModel("anthropic", "claude-sonnet-4-6"); + const compat = await import(${JSON.stringify(compatEntryUrl)}); + const model = compat.getModel("anthropic", "claude-sonnet-4-6"); const context = { messages: [{ role: "user", content: "hi" }] }; - await mod.streamSimple(model, context).result(); + await compat.streamSimple(model, context).result(); `); expect(result.loadedSpecifiers).toEqual(["@anthropic-ai/sdk"]); diff --git a/packages/ai/test/mistral-reasoning-mode.test.ts b/packages/ai/test/mistral-reasoning-mode.test.ts index 35a5b96b..4bd24f79 100644 --- a/packages/ai/test/mistral-reasoning-mode.test.ts +++ b/packages/ai/test/mistral-reasoning-mode.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; +import { getModel, streamSimple } from "../src/compat.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface MistralPayload { diff --git a/packages/ai/test/mistral-tool-schema.test.ts b/packages/ai/test/mistral-tool-schema.test.ts index c6898fc6..7691775a 100644 --- a/packages/ai/test/mistral-tool-schema.test.ts +++ b/packages/ai/test/mistral-tool-schema.test.ts @@ -1,7 +1,6 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; +import { complete, getModel } from "../src/compat.ts"; import type { Context, Model } from "../src/types.ts"; interface MistralToolPayload { diff --git a/packages/ai/test/oauth-auth.test.ts b/packages/ai/test/oauth-auth.test.ts index 689fc2cf..579fdb37 100644 --- a/packages/ai/test/oauth-auth.test.ts +++ b/packages/ai/test/oauth-auth.test.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { InMemoryCredentialStore } from "../src/auth/credential-store.ts"; -import type { AuthEvent, AuthPrompt } from "../src/auth/types.ts"; import { createModels } from "../src/models.ts"; import { anthropicProvider } from "../src/providers/anthropic.ts"; import { githubCopilotProvider } from "../src/providers/github-copilot.ts"; @@ -86,40 +85,6 @@ describe.sequential("OAuthAuth adapters", () => { expect(refreshed.enterpriseUrl).toBe("company.ghe.com"); expect(fetchedUrls[0]).toContain("api.company.ghe.com"); }); - - it("anthropic login resolves through the manual_code prompt and aborts it after settling", async () => { - const fetchMock = vi.fn(async (input: unknown) => { - const url = typeof input === "string" ? input : String(input); - if (url.includes("/oauth/token")) { - return jsonResponse({ access_token: "access", refresh_token: "refresh", expires_in: 3600 }); - } - throw new Error(`Unexpected fetch: ${url}`); - }); - vi.stubGlobal("fetch", fetchMock); - - const events: AuthEvent[] = []; - const prompts: AuthPrompt[] = []; - let manualSignal: AbortSignal | undefined; - - const credential = await anthropicOAuth.login({ - notify: (event) => events.push(event), - prompt: async (prompt) => { - prompts.push(prompt); - if (prompt.type === "manual_code") { - manualSignal = prompt.signal; - return "the-code"; - } - throw new Error(`Unexpected prompt: ${prompt.type}`); - }, - }); - - expect(credential.type).toBe("oauth"); - expect(credential.access).toBe("access"); - expect(events.some((e) => e.type === "auth_url")).toBe(true); - expect(prompts.some((p) => p.type === "manual_code")).toBe(true); - // the prompt's signal is aborted once login settles, so UIs can dismiss it - expect(manualSignal?.aborted).toBe(true); - }); }); describe("OAuth through Models.getAuth (lazy load chain)", () => { diff --git a/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts b/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts index 0abb46f6..19bf857b 100644 --- a/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts +++ b/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; +import { complete, getModel } from "../src/compat.ts"; import type { Context } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/openai-completions-cache-control-format.test.ts b/packages/ai/test/openai-completions-cache-control-format.test.ts index 04fd2361..d87a95eb 100644 --- a/packages/ai/test/openai-completions-cache-control-format.test.ts +++ b/packages/ai/test/openai-completions-cache-control-format.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { stream as streamOpenAICompletions } from "../src/api/openai-completions.ts"; -import { getModel } from "../src/models.ts"; +import { getModel } from "../src/compat.ts"; import type { Model } from "../src/types.ts"; interface CacheControl { diff --git a/packages/ai/test/openai-completions-empty-tools.test.ts b/packages/ai/test/openai-completions-empty-tools.test.ts index a743351c..71297a23 100644 --- a/packages/ai/test/openai-completions-empty-tools.test.ts +++ b/packages/ai/test/openai-completions-empty-tools.test.ts @@ -1,6 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; +import { getModel, streamSimple } from "../src/compat.ts"; // Empty tools arrays must NOT be serialized as `tools: []` — some OpenAI-compatible // backends (e.g. DashScope / Aliyun Qwen via compatible-mode) reject the request with diff --git a/packages/ai/test/openai-completions-prompt-cache.test.ts b/packages/ai/test/openai-completions-prompt-cache.test.ts index 0b25e857..5e905712 100644 --- a/packages/ai/test/openai-completions-prompt-cache.test.ts +++ b/packages/ai/test/openai-completions-prompt-cache.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { stream as streamOpenAICompletions } from "../src/api/openai-completions.ts"; -import { getModel } from "../src/models.ts"; +import { getModel } from "../src/compat.ts"; import type { Model } from "../src/types.ts"; interface FakeOpenAIClientOptions { diff --git a/packages/ai/test/openai-completions-response-model.test.ts b/packages/ai/test/openai-completions-response-model.test.ts index d8e4d2f9..5139bd54 100644 --- a/packages/ai/test/openai-completions-response-model.test.ts +++ b/packages/ai/test/openai-completions-response-model.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { complete } from "../src/stream.ts"; +import { complete } from "../src/compat.ts"; import type { Model } from "../src/types.ts"; // Router/virtual ids (e.g. OpenRouter `auto`) keep `model` pinned to the diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index 61054aad..61083572 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -1,8 +1,7 @@ import { Type } from "typebox"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { convertMessages } from "../src/api/openai-completions.ts"; -import { getModel } from "../src/models.ts"; -import { stream, streamSimple } from "../src/stream.ts"; +import { getModel, stream, streamSimple } from "../src/compat.ts"; import type { AssistantMessage, Model, Tool, ToolResultMessage } from "../src/types.ts"; const mockState = vi.hoisted(() => ({ diff --git a/packages/ai/test/openai-completions-tool-result-images.test.ts b/packages/ai/test/openai-completions-tool-result-images.test.ts index 4fa0fd68..758b8767 100644 --- a/packages/ai/test/openai-completions-tool-result-images.test.ts +++ b/packages/ai/test/openai-completions-tool-result-images.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { convertMessages } from "../src/api/openai-completions.ts"; -import { getModel } from "../src/models.ts"; +import { getModel } from "../src/compat.ts"; import type { AssistantMessage, Context, diff --git a/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts b/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts index d173694f..a2f1197e 100644 --- a/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts +++ b/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; +import { complete, getModel } from "../src/compat.ts"; import type { Context } from "../src/types.ts"; describe.skipIf(!process.env.OPENAI_API_KEY)("openai responses cache affinity e2e", () => { diff --git a/packages/ai/test/openai-responses-copilot-provider.test.ts b/packages/ai/test/openai-responses-copilot-provider.test.ts index 92b3d740..57c319f2 100644 --- a/packages/ai/test/openai-responses-copilot-provider.test.ts +++ b/packages/ai/test/openai-responses-copilot-provider.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { stream as streamOpenAIResponses } from "../src/api/openai-responses.ts"; -import { getModel } from "../src/models.ts"; +import { getModel } from "../src/compat.ts"; import type { Model } from "../src/types.ts"; type CapturedHeaders = Headers | string[][] | Record | undefined; diff --git a/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts b/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts index 7eea6821..b8231658 100644 --- a/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts +++ b/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { convertResponsesMessages } from "../src/api/openai-responses-shared.ts"; -import { getModel } from "../src/models.ts"; +import { getModel } from "../src/compat.ts"; import type { AssistantMessage, Context, ToolResultMessage, Usage } from "../src/types.ts"; import { shortHash } from "../src/utils/hash.ts"; diff --git a/packages/ai/test/openai-responses-message-id.test.ts b/packages/ai/test/openai-responses-message-id.test.ts index 06687fa1..cb2fd0c0 100644 --- a/packages/ai/test/openai-responses-message-id.test.ts +++ b/packages/ai/test/openai-responses-message-id.test.ts @@ -1,7 +1,7 @@ import type { ResponseOutputMessage } from "openai/resources/responses/responses.js"; import { describe, expect, it } from "vitest"; import { convertResponsesMessages } from "../src/api/openai-responses-shared.ts"; -import { getModel } from "../src/models.ts"; +import { getModel } from "../src/compat.ts"; import type { AssistantMessage, Context, Usage } from "../src/types.ts"; const usage: Usage = { diff --git a/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts b/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts index aa6f2e23..85753c03 100644 --- a/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts +++ b/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts @@ -1,7 +1,6 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { complete, getEnvApiKey } from "../src/stream.ts"; +import { complete, getEnvApiKey, getModel } from "../src/compat.ts"; import type { AssistantMessage, Context, Message, Tool, ToolCall } from "../src/types.ts"; const testToolSchema = Type.Object({ diff --git a/packages/ai/test/openai-responses-tool-result-images.test.ts b/packages/ai/test/openai-responses-tool-result-images.test.ts index c6131d91..da33ab86 100644 --- a/packages/ai/test/openai-responses-tool-result-images.test.ts +++ b/packages/ai/test/openai-responses-tool-result-images.test.ts @@ -4,8 +4,8 @@ import { fileURLToPath } from "node:url"; import type { ResponseFunctionCallOutputItemList } from "openai/resources/responses/responses.js"; import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import type { Api, Context, Model, StreamOptions, Tool, ToolResultMessage } from "../src/index.ts"; -import { complete, getModel } from "../src/index.ts"; +import type { Api, Context, Model, StreamOptions, Tool, ToolResultMessage } from "../src/compat.ts"; +import { complete, getModel } from "../src/compat.ts"; import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/openrouter-cache-write-repro.test.ts b/packages/ai/test/openrouter-cache-write-repro.test.ts index 4bdeb286..2292ec91 100644 --- a/packages/ai/test/openrouter-cache-write-repro.test.ts +++ b/packages/ai/test/openrouter-cache-write-repro.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { completeSimple } from "../src/stream.ts"; +import { completeSimple, getModel } from "../src/compat.ts"; function createLongSystemPrompt(): string { const nonce = `${Date.now()}-${Math.random()}`; diff --git a/packages/ai/test/responseid.test.ts b/packages/ai/test/responseid.test.ts index d250f5f4..cd566057 100644 --- a/packages/ai/test/responseid.test.ts +++ b/packages/ai/test/responseid.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; +import { complete, getModel } from "../src/compat.ts"; import type { Api, Context, Model, StreamOptions } from "../src/types.ts"; import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index 9a70b6c2..fbc95f50 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -4,8 +4,7 @@ import { dirname, join } from "path"; import { Type } from "typebox"; import { fileURLToPath } from "url"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { complete, stream } from "../src/stream.ts"; +import { complete, getModel, stream } from "../src/compat.ts"; import type { Api, Context, ImageContent, Model, StreamOptions, Tool, ToolResultMessage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/supports-xhigh.test.ts b/packages/ai/test/supports-xhigh.test.ts index ec6dc87b..ada30669 100644 --- a/packages/ai/test/supports-xhigh.test.ts +++ b/packages/ai/test/supports-xhigh.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel, getSupportedThinkingLevels } from "../src/models.ts"; +import { getModel, getSupportedThinkingLevels } from "../src/compat.ts"; describe("getSupportedThinkingLevels", () => { it("includes xhigh for Anthropic Opus 4.6 on anthropic-messages API", () => { diff --git a/packages/ai/test/together-models.test.ts b/packages/ai/test/together-models.test.ts index cb5ea943..0d766d65 100644 --- a/packages/ai/test/together-models.test.ts +++ b/packages/ai/test/together-models.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it } from "vitest"; +import { getModel } from "../src/compat.ts"; import { findEnvKeys, getEnvApiKey } from "../src/env-api-keys.ts"; -import { getModel } from "../src/models.ts"; const originalTogetherApiKey = process.env.TOGETHER_API_KEY; diff --git a/packages/ai/test/tokens.test.ts b/packages/ai/test/tokens.test.ts index 676f5a29..e99c7134 100644 --- a/packages/ai/test/tokens.test.ts +++ b/packages/ai/test/tokens.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel, getModels } from "../src/models.ts"; -import { stream } from "../src/stream.ts"; +import { getModel, getModels, stream } from "../src/compat.ts"; import type { Api, Context, Model, StreamOptions } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/tool-call-id-normalization.test.ts b/packages/ai/test/tool-call-id-normalization.test.ts index 0672181b..fd59d9e6 100644 --- a/packages/ai/test/tool-call-id-normalization.test.ts +++ b/packages/ai/test/tool-call-id-normalization.test.ts @@ -12,8 +12,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { completeSimple, getEnvApiKey } from "../src/stream.ts"; +import { completeSimple, getEnvApiKey, getModel } from "../src/compat.ts"; import type { AssistantMessage, Message, Tool, ToolResultMessage } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/tool-call-without-result.test.ts b/packages/ai/test/tool-call-without-result.test.ts index 11b832f3..198c48c5 100644 --- a/packages/ai/test/tool-call-without-result.test.ts +++ b/packages/ai/test/tool-call-without-result.test.ts @@ -1,7 +1,6 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; +import { complete, getModel } from "../src/compat.ts"; import type { Api, Context, Model, StreamOptions, Tool } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/total-tokens.test.ts b/packages/ai/test/total-tokens.test.ts index 972913a7..d07fc5ad 100644 --- a/packages/ai/test/total-tokens.test.ts +++ b/packages/ai/test/total-tokens.test.ts @@ -13,8 +13,7 @@ */ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; +import { complete, getModel } from "../src/compat.ts"; import type { Api, Context, Model, StreamOptions, Usage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/unicode-surrogate.test.ts b/packages/ai/test/unicode-surrogate.test.ts index 9cdddefa..f4ea7450 100644 --- a/packages/ai/test/unicode-surrogate.test.ts +++ b/packages/ai/test/unicode-surrogate.test.ts @@ -1,7 +1,6 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; +import { complete, getModel } from "../src/compat.ts"; import type { Api, Context, Model, StreamOptions, ToolResultMessage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/xhigh.test.ts b/packages/ai/test/xhigh.test.ts index 3c279863..f1722b9a 100644 --- a/packages/ai/test/xhigh.test.ts +++ b/packages/ai/test/xhigh.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { stream } from "../src/stream.ts"; +import { getModel, stream } from "../src/compat.ts"; import type { Context, Model } from "../src/types.ts"; function makeContext(): Context { diff --git a/packages/ai/test/xiaomi-models.test.ts b/packages/ai/test/xiaomi-models.test.ts index 6fb1f4b8..277b8495 100644 --- a/packages/ai/test/xiaomi-models.test.ts +++ b/packages/ai/test/xiaomi-models.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel, getModels } from "../src/models.ts"; +import { getModel, getModels } from "../src/compat.ts"; describe("Xiaomi MiMo models", () => { it("keeps mimo-v2-flash on the API billing provider", () => { diff --git a/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts b/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts index ab2fec30..74d27089 100644 --- a/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts +++ b/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { completeSimple, getEnvApiKey, streamSimple } from "../src/stream.ts"; +import { completeSimple, getEnvApiKey, streamSimple } from "../src/compat.ts"; import type { AssistantMessage, Context, Model } from "../src/types.ts"; const provider = "xiaomi-token-plan-ams"; diff --git a/packages/ai/test/zen.test.ts b/packages/ai/test/zen.test.ts index 8ca80014..0f731b46 100644 --- a/packages/ai/test/zen.test.ts +++ b/packages/ai/test/zen.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { complete } from "../src/compat.ts"; import { MODELS } from "../src/models.generated.ts"; -import { complete } from "../src/stream.ts"; import type { Model } from "../src/types.ts"; describe.skipIf(!process.env.OPENCODE_API_KEY)("OpenCode Models Smoke Test", () => { diff --git a/packages/coding-agent/examples/extensions/custom-compaction.ts b/packages/coding-agent/examples/extensions/custom-compaction.ts index 02c6ceb3..310e81d8 100644 --- a/packages/coding-agent/examples/extensions/custom-compaction.ts +++ b/packages/coding-agent/examples/extensions/custom-compaction.ts @@ -13,7 +13,7 @@ * pi --extension examples/extensions/custom-compaction.ts */ -import { complete } from "@earendil-works/pi-ai"; +import { complete } from "@earendil-works/pi-ai/compat"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { convertToLlm, serializeConversation } from "@earendil-works/pi-coding-agent"; diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/index.ts b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/index.ts index 2efe64ac..60e5a279 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/index.ts +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/index.ts @@ -21,7 +21,7 @@ import { openAIResponsesApi, type SimpleStreamOptions, type ThinkingLevelMap, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/compat"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; // ============================================================================= diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/test.ts b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/test.ts index 0077aca8..79ba3d71 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/test.ts +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/test.ts @@ -8,7 +8,7 @@ * npx tsx test.ts claude-sonnet-4-5-20250929 --thinking */ -import { type Api, type Context, type Model, registerApiProvider, streamSimple } from "@earendil-works/pi-ai"; +import { type Api, type Context, type Model, registerApiProvider, streamSimple } from "@earendil-works/pi-ai/compat"; import { readFileSync } from "fs"; import { getAgentDir } from "packages/coding-agent/src/config.js"; import { join } from "path"; diff --git a/packages/coding-agent/examples/extensions/handoff.ts b/packages/coding-agent/examples/extensions/handoff.ts index 4af6661f..a161fad6 100644 --- a/packages/coding-agent/examples/extensions/handoff.ts +++ b/packages/coding-agent/examples/extensions/handoff.ts @@ -13,7 +13,7 @@ */ import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import { complete, type Message } from "@earendil-works/pi-ai"; +import { complete, type Message } from "@earendil-works/pi-ai/compat"; import type { ExtensionAPI, SessionEntry } from "@earendil-works/pi-coding-agent"; import { BorderedLoader, convertToLlm, serializeConversation } from "@earendil-works/pi-coding-agent"; diff --git a/packages/coding-agent/examples/extensions/qna.ts b/packages/coding-agent/examples/extensions/qna.ts index 70dbef7b..524c2785 100644 --- a/packages/coding-agent/examples/extensions/qna.ts +++ b/packages/coding-agent/examples/extensions/qna.ts @@ -7,7 +7,7 @@ * 3. Loads the result into the editor for user to fill in answers */ -import { complete, type UserMessage } from "@earendil-works/pi-ai"; +import { complete, type UserMessage } from "@earendil-works/pi-ai/compat"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { BorderedLoader } from "@earendil-works/pi-coding-agent"; diff --git a/packages/coding-agent/examples/extensions/summarize.ts b/packages/coding-agent/examples/extensions/summarize.ts index e6480974..66ce5340 100644 --- a/packages/coding-agent/examples/extensions/summarize.ts +++ b/packages/coding-agent/examples/extensions/summarize.ts @@ -1,4 +1,4 @@ -import { complete, getModel } from "@earendil-works/pi-ai"; +import { complete, getModel } from "@earendil-works/pi-ai/compat"; import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"; import { DynamicBorder, getMarkdownTheme } from "@earendil-works/pi-coding-agent"; import { Container, Markdown, matchesKey, Text } from "@earendil-works/pi-tui"; diff --git a/packages/coding-agent/examples/sdk/02-custom-model.ts b/packages/coding-agent/examples/sdk/02-custom-model.ts index 531a51c8..641d553e 100644 --- a/packages/coding-agent/examples/sdk/02-custom-model.ts +++ b/packages/coding-agent/examples/sdk/02-custom-model.ts @@ -4,7 +4,7 @@ * Shows how to select a specific model and thinking level. */ -import { getModel } from "@earendil-works/pi-ai"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { AuthStorage, createAgentSession, ModelRegistry } from "@earendil-works/pi-coding-agent"; // Set up auth storage and model registry diff --git a/packages/coding-agent/examples/sdk/12-full-control.ts b/packages/coding-agent/examples/sdk/12-full-control.ts index cd8ba343..12b7607d 100644 --- a/packages/coding-agent/examples/sdk/12-full-control.ts +++ b/packages/coding-agent/examples/sdk/12-full-control.ts @@ -4,7 +4,7 @@ * Replace everything - no discovery, explicit configuration. */ -import { getModel } from "@earendil-works/pi-ai"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { AuthStorage, createAgentSession, diff --git a/packages/coding-agent/src/bun/register-bedrock.ts b/packages/coding-agent/src/bun/register-bedrock.ts index 92af0dde..18e80dcc 100644 --- a/packages/coding-agent/src/bun/register-bedrock.ts +++ b/packages/coding-agent/src/bun/register-bedrock.ts @@ -1,4 +1,4 @@ -import { setBedrockProviderModule } from "@earendil-works/pi-ai"; import { bedrockProviderModule } from "@earendil-works/pi-ai/bedrock-provider"; +import { setBedrockProviderModule } from "@earendil-works/pi-ai/compat"; setBedrockProviderModule(bedrockProviderModule); diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 201b8084..1b5f1c7d 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -23,7 +23,7 @@ import type { AgentTool, ThinkingLevel, } from "@earendil-works/pi-agent-core"; -import type { AssistantMessage, ImageContent, Message, Model, TextContent } from "@earendil-works/pi-ai"; +import type { AssistantMessage, ImageContent, Message, Model, TextContent } from "@earendil-works/pi-ai/compat"; import { clampThinkingLevel, cleanupSessionResources, @@ -32,7 +32,7 @@ import { modelsAreEqual, resetApiProviders, streamSimple, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/compat"; import { theme } from "../modes/interactive/theme/theme.ts"; import { stripFrontmatter } from "../utils/frontmatter.ts"; import { resolvePath } from "../utils/paths.ts"; diff --git a/packages/coding-agent/src/core/auth-storage.ts b/packages/coding-agent/src/core/auth-storage.ts index 9a0564ec..6b9f2f3a 100644 --- a/packages/coding-agent/src/core/auth-storage.ts +++ b/packages/coding-agent/src/core/auth-storage.ts @@ -12,7 +12,7 @@ import { type OAuthCredentials, type OAuthLoginCallbacks, type OAuthProviderId, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/compat"; import { getOAuthApiKey, getOAuthProvider, getOAuthProviders } from "@earendil-works/pi-ai/oauth"; import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; import { dirname, join } from "path"; diff --git a/packages/coding-agent/src/core/compaction/branch-summarization.ts b/packages/coding-agent/src/core/compaction/branch-summarization.ts index f6ce3d67..caa05bb3 100644 --- a/packages/coding-agent/src/core/compaction/branch-summarization.ts +++ b/packages/coding-agent/src/core/compaction/branch-summarization.ts @@ -6,8 +6,8 @@ */ import type { AgentMessage, StreamFn } from "@earendil-works/pi-agent-core"; -import type { Model, SimpleStreamOptions } from "@earendil-works/pi-ai"; -import { completeSimple } from "@earendil-works/pi-ai"; +import type { Model, SimpleStreamOptions } from "@earendil-works/pi-ai/compat"; +import { completeSimple } from "@earendil-works/pi-ai/compat"; import { convertToLlm, createBranchSummaryMessage, diff --git a/packages/coding-agent/src/core/compaction/compaction.ts b/packages/coding-agent/src/core/compaction/compaction.ts index eae19794..43ee79ef 100644 --- a/packages/coding-agent/src/core/compaction/compaction.ts +++ b/packages/coding-agent/src/core/compaction/compaction.ts @@ -6,8 +6,8 @@ */ import type { AgentMessage, StreamFn, ThinkingLevel } from "@earendil-works/pi-agent-core"; -import type { AssistantMessage, Context, Model, SimpleStreamOptions, Usage } from "@earendil-works/pi-ai"; -import { completeSimple } from "@earendil-works/pi-ai"; +import type { AssistantMessage, Context, Model, SimpleStreamOptions, Usage } from "@earendil-works/pi-ai/compat"; +import { completeSimple } from "@earendil-works/pi-ai/compat"; import { convertToLlm, createBranchSummaryMessage, diff --git a/packages/coding-agent/src/core/extensions/loader.ts b/packages/coding-agent/src/core/extensions/loader.ts index 081d2d11..37f66164 100644 --- a/packages/coding-agent/src/core/extensions/loader.ts +++ b/packages/coding-agent/src/core/extensions/loader.ts @@ -8,7 +8,7 @@ import { createRequire } from "node:module"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; import * as _bundledPiAgentCore from "@earendil-works/pi-agent-core"; -import * as _bundledPiAi from "@earendil-works/pi-ai"; +import * as _bundledPiAiCompat from "@earendil-works/pi-ai/compat"; import * as _bundledPiAiOauth from "@earendil-works/pi-ai/oauth"; import type { KeyId } from "@earendil-works/pi-tui"; import * as _bundledPiTui from "@earendil-works/pi-tui"; @@ -50,12 +50,17 @@ const VIRTUAL_MODULES: Record = { "@sinclair/typebox/value": _bundledTypeboxValue, "@earendil-works/pi-agent-core": _bundledPiAgentCore, "@earendil-works/pi-tui": _bundledPiTui, - "@earendil-works/pi-ai": _bundledPiAi, + // Extensions resolve the pi-ai root to the compat entrypoint (a strict + // superset of the core entrypoint): existing extensions using the old + // global API keep working at runtime until compat is removed. + "@earendil-works/pi-ai": _bundledPiAiCompat, + "@earendil-works/pi-ai/compat": _bundledPiAiCompat, "@earendil-works/pi-ai/oauth": _bundledPiAiOauth, "@earendil-works/pi-coding-agent": _bundledPiCodingAgent, "@mariozechner/pi-agent-core": _bundledPiAgentCore, "@mariozechner/pi-tui": _bundledPiTui, - "@mariozechner/pi-ai": _bundledPiAi, + "@mariozechner/pi-ai": _bundledPiAiCompat, + "@mariozechner/pi-ai/compat": _bundledPiAiCompat, "@mariozechner/pi-ai/oauth": _bundledPiAiOauth, "@mariozechner/pi-coding-agent": _bundledPiCodingAgent, }; @@ -90,19 +95,24 @@ function getAliases(): Record { const piCodingAgentEntry = packageIndex; const piAgentCoreEntry = resolveWorkspaceOrImport("agent/dist/index.js", "@earendil-works/pi-agent-core"); const piTuiEntry = resolveWorkspaceOrImport("tui/dist/index.js", "@earendil-works/pi-tui"); - const piAiEntry = resolveWorkspaceOrImport("ai/dist/index.js", "@earendil-works/pi-ai"); + // Extensions resolve the pi-ai root to the compat entrypoint (a strict + // superset of the core entrypoint): existing extensions using the old + // global API keep working at runtime until compat is removed. + const piAiCompatEntry = resolveWorkspaceOrImport("ai/dist/compat.js", "@earendil-works/pi-ai/compat"); const piAiOauthEntry = resolveWorkspaceOrImport("ai/dist/oauth.js", "@earendil-works/pi-ai/oauth"); _aliases = { "@earendil-works/pi-coding-agent": piCodingAgentEntry, "@earendil-works/pi-agent-core": piAgentCoreEntry, "@earendil-works/pi-tui": piTuiEntry, - "@earendil-works/pi-ai": piAiEntry, + "@earendil-works/pi-ai": piAiCompatEntry, + "@earendil-works/pi-ai/compat": piAiCompatEntry, "@earendil-works/pi-ai/oauth": piAiOauthEntry, "@mariozechner/pi-coding-agent": piCodingAgentEntry, "@mariozechner/pi-agent-core": piAgentCoreEntry, "@mariozechner/pi-tui": piTuiEntry, - "@mariozechner/pi-ai": piAiEntry, + "@mariozechner/pi-ai": piAiCompatEntry, + "@mariozechner/pi-ai/compat": piAiCompatEntry, "@mariozechner/pi-ai/oauth": piAiOauthEntry, typebox: typeboxEntry, "typebox/compile": typeboxCompileEntry, diff --git a/packages/coding-agent/src/core/model-registry.ts b/packages/coding-agent/src/core/model-registry.ts index 00c81576..114cbb00 100644 --- a/packages/coding-agent/src/core/model-registry.ts +++ b/packages/coding-agent/src/core/model-registry.ts @@ -17,7 +17,7 @@ import { registerApiProvider, resetApiProviders, type SimpleStreamOptions, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/compat"; import { registerOAuthProvider, resetOAuthProviders } from "@earendil-works/pi-ai/oauth"; import { existsSync, readFileSync } from "fs"; import { join } from "path"; diff --git a/packages/coding-agent/src/core/sdk.ts b/packages/coding-agent/src/core/sdk.ts index e6db747b..49d13535 100644 --- a/packages/coding-agent/src/core/sdk.ts +++ b/packages/coding-agent/src/core/sdk.ts @@ -1,6 +1,6 @@ import { join } from "node:path"; import { Agent, type AgentMessage, type ThinkingLevel } from "@earendil-works/pi-agent-core"; -import { clampThinkingLevel, type Message, type Model, streamSimple } from "@earendil-works/pi-ai"; +import { clampThinkingLevel, type Message, type Model, streamSimple } from "@earendil-works/pi-ai/compat"; import { getAgentDir } from "../config.ts"; import { resolvePath } from "../utils/paths.ts"; import { AgentSession } from "./agent-session.ts"; diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index d50611af..12e7e819 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -16,7 +16,7 @@ import { type Model, type OAuthProviderId, type OAuthSelectPrompt, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/compat"; import type { AutocompleteItem, AutocompleteProvider, diff --git a/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts b/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts index 1abe3e28..cc0f9714 100644 --- a/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts +++ b/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts @@ -2,7 +2,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Agent } from "@earendil-works/pi-agent-core"; -import { type AssistantMessage, getModel } from "@earendil-works/pi-ai"; +import { type AssistantMessage, getModel } from "@earendil-works/pi-ai/compat"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { AgentSession } from "../src/core/agent-session.ts"; import { AuthStorage } from "../src/core/auth-storage.ts"; diff --git a/packages/coding-agent/test/agent-session-branching.test.ts b/packages/coding-agent/test/agent-session-branching.test.ts index f516e6d6..76d355e1 100644 --- a/packages/coding-agent/test/agent-session-branching.test.ts +++ b/packages/coding-agent/test/agent-session-branching.test.ts @@ -10,7 +10,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { getModel } from "@earendil-works/pi-ai"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import type { AgentSession } from "../src/core/agent-session.ts"; import { diff --git a/packages/coding-agent/test/agent-session-compaction.test.ts b/packages/coding-agent/test/agent-session-compaction.test.ts index 5724d684..516c3b45 100644 --- a/packages/coding-agent/test/agent-session-compaction.test.ts +++ b/packages/coding-agent/test/agent-session-compaction.test.ts @@ -11,7 +11,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Agent } from "@earendil-works/pi-agent-core"; -import { getModel } from "@earendil-works/pi-ai"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { AgentSession, type AgentSessionEvent } from "../src/core/agent-session.ts"; import { AuthStorage } from "../src/core/auth-storage.ts"; diff --git a/packages/coding-agent/test/agent-session-concurrent.test.ts b/packages/coding-agent/test/agent-session-concurrent.test.ts index dcd2ac9b..46599068 100644 --- a/packages/coding-agent/test/agent-session-concurrent.test.ts +++ b/packages/coding-agent/test/agent-session-concurrent.test.ts @@ -13,7 +13,7 @@ import { getModel, type ImageContent, type TextContent, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/compat"; import { Type } from "typebox"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { AgentSession } from "../src/core/agent-session.ts"; diff --git a/packages/coding-agent/test/agent-session-dynamic-provider.test.ts b/packages/coding-agent/test/agent-session-dynamic-provider.test.ts index a6da8c9e..eee5583d 100644 --- a/packages/coding-agent/test/agent-session-dynamic-provider.test.ts +++ b/packages/coding-agent/test/agent-session-dynamic-provider.test.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { getModel } from "@earendil-works/pi-ai"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { AuthStorage } from "../src/core/auth-storage.ts"; import { DefaultResourceLoader } from "../src/core/resource-loader.ts"; diff --git a/packages/coding-agent/test/agent-session-dynamic-tools.test.ts b/packages/coding-agent/test/agent-session-dynamic-tools.test.ts index cf64a17f..88871ac8 100644 --- a/packages/coding-agent/test/agent-session-dynamic-tools.test.ts +++ b/packages/coding-agent/test/agent-session-dynamic-tools.test.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { getModel } from "@earendil-works/pi-ai"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { Type } from "typebox"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { DefaultResourceLoader } from "../src/core/resource-loader.ts"; diff --git a/packages/coding-agent/test/agent-session-retry.test.ts b/packages/coding-agent/test/agent-session-retry.test.ts index ebba143b..6e3d0582 100644 --- a/packages/coding-agent/test/agent-session-retry.test.ts +++ b/packages/coding-agent/test/agent-session-retry.test.ts @@ -2,7 +2,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Agent, type AgentEvent, type AgentTool } from "@earendil-works/pi-agent-core"; -import { type AssistantMessage, type AssistantMessageEvent, EventStream, getModel } from "@earendil-works/pi-ai"; +import { type AssistantMessage, type AssistantMessageEvent, EventStream, getModel } from "@earendil-works/pi-ai/compat"; import { Type } from "typebox"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { AgentSession } from "../src/core/agent-session.ts"; diff --git a/packages/coding-agent/test/agent-session-stats.test.ts b/packages/coding-agent/test/agent-session-stats.test.ts index b435246e..9ba30a94 100644 --- a/packages/coding-agent/test/agent-session-stats.test.ts +++ b/packages/coding-agent/test/agent-session-stats.test.ts @@ -1,5 +1,5 @@ import { Agent } from "@earendil-works/pi-agent-core"; -import { type AssistantMessage, getModel, type Usage } from "@earendil-works/pi-ai"; +import { type AssistantMessage, getModel, type Usage } from "@earendil-works/pi-ai/compat"; import { describe, expect, it } from "vitest"; import { AgentSession } from "../src/core/agent-session.ts"; import { AuthStorage } from "../src/core/auth-storage.ts"; diff --git a/packages/coding-agent/test/compaction-extensions.test.ts b/packages/coding-agent/test/compaction-extensions.test.ts index 2c41210e..6554e89a 100644 --- a/packages/coding-agent/test/compaction-extensions.test.ts +++ b/packages/coding-agent/test/compaction-extensions.test.ts @@ -6,7 +6,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Agent } from "@earendil-works/pi-agent-core"; -import { getModel } from "@earendil-works/pi-ai"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { AgentSession } from "../src/core/agent-session.ts"; import { AuthStorage } from "../src/core/auth-storage.ts"; diff --git a/packages/coding-agent/test/compaction-summary-reasoning.test.ts b/packages/coding-agent/test/compaction-summary-reasoning.test.ts index 6609aca1..306a3b1c 100644 --- a/packages/coding-agent/test/compaction-summary-reasoning.test.ts +++ b/packages/coding-agent/test/compaction-summary-reasoning.test.ts @@ -7,8 +7,8 @@ const { completeSimpleMock } = vi.hoisted(() => ({ completeSimpleMock: vi.fn(), })); -vi.mock("@earendil-works/pi-ai", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("@earendil-works/pi-ai/compat", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, completeSimple: completeSimpleMock, diff --git a/packages/coding-agent/test/compaction.test.ts b/packages/coding-agent/test/compaction.test.ts index 929d06ec..c74fe870 100644 --- a/packages/coding-agent/test/compaction.test.ts +++ b/packages/coding-agent/test/compaction.test.ts @@ -1,6 +1,6 @@ import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { AssistantMessage, Usage } from "@earendil-works/pi-ai"; -import { getModel } from "@earendil-works/pi-ai"; +import type { AssistantMessage, Usage } from "@earendil-works/pi-ai/compat"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { readFileSync } from "fs"; import { join } from "path"; import { beforeEach, describe, expect, it } from "vitest"; diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index 95b70fbd..32132c1b 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -1,8 +1,14 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import type { AnthropicMessagesCompat, Api, Context, Model, OpenAICompletionsCompat } from "@earendil-works/pi-ai"; -import { getApiProvider } from "@earendil-works/pi-ai"; +import type { + AnthropicMessagesCompat, + Api, + Context, + Model, + OpenAICompletionsCompat, +} from "@earendil-works/pi-ai/compat"; +import { getApiProvider } from "@earendil-works/pi-ai/compat"; import { getOAuthProvider } from "@earendil-works/pi-ai/oauth"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { AuthStorage } from "../src/core/auth-storage.ts"; diff --git a/packages/coding-agent/test/rpc-prompt-response-semantics.test.ts b/packages/coding-agent/test/rpc-prompt-response-semantics.test.ts index 08b3a56f..5e4d5b08 100644 --- a/packages/coding-agent/test/rpc-prompt-response-semantics.test.ts +++ b/packages/coding-agent/test/rpc-prompt-response-semantics.test.ts @@ -8,7 +8,7 @@ import { EventStream, getModel, type Model, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/compat"; import { afterEach, describe, expect, it, vi } from "vitest"; import { AgentSession } from "../src/core/agent-session.ts"; import type { AgentSessionRuntime } from "../src/core/agent-session-runtime.ts"; diff --git a/packages/coding-agent/test/sdk-codex-cache-probe-tool-loop.ts b/packages/coding-agent/test/sdk-codex-cache-probe-tool-loop.ts index 6d00a1a0..ff6a9798 100644 --- a/packages/coding-agent/test/sdk-codex-cache-probe-tool-loop.ts +++ b/packages/coding-agent/test/sdk-codex-cache-probe-tool-loop.ts @@ -20,7 +20,7 @@ import { type Model, type SimpleStreamOptions, Type, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/compat"; import { getOpenAICodexWebSocketDebugStats, streamSimple as streamSimpleOpenAICodexResponses, diff --git a/packages/coding-agent/test/sdk-session-manager.test.ts b/packages/coding-agent/test/sdk-session-manager.test.ts index cb7e505c..9cdf7774 100644 --- a/packages/coding-agent/test/sdk-session-manager.test.ts +++ b/packages/coding-agent/test/sdk-session-manager.test.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, realpathSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { getModel } from "@earendil-works/pi-ai"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { createAgentSession } from "../src/core/sdk.ts"; import { SessionManager } from "../src/core/session-manager.ts"; diff --git a/packages/coding-agent/test/suite/regressions/2835-tools-allowlist-filters-extension-tools.test.ts b/packages/coding-agent/test/suite/regressions/2835-tools-allowlist-filters-extension-tools.test.ts index e0003aa6..198b45a8 100644 --- a/packages/coding-agent/test/suite/regressions/2835-tools-allowlist-filters-extension-tools.test.ts +++ b/packages/coding-agent/test/suite/regressions/2835-tools-allowlist-filters-extension-tools.test.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { getModel } from "@earendil-works/pi-ai"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { Type } from "typebox"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { DefaultResourceLoader } from "../../../src/core/resource-loader.ts"; diff --git a/packages/coding-agent/test/suite/regressions/3592-no-builtin-tools-keeps-extension-tools.test.ts b/packages/coding-agent/test/suite/regressions/3592-no-builtin-tools-keeps-extension-tools.test.ts index d7d1376c..3d900ee3 100644 --- a/packages/coding-agent/test/suite/regressions/3592-no-builtin-tools-keeps-extension-tools.test.ts +++ b/packages/coding-agent/test/suite/regressions/3592-no-builtin-tools-keeps-extension-tools.test.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { getModel } from "@earendil-works/pi-ai"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { Type } from "typebox"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { diff --git a/packages/coding-agent/test/utilities.ts b/packages/coding-agent/test/utilities.ts index 5ebf6813..73177ce9 100644 --- a/packages/coding-agent/test/utilities.ts +++ b/packages/coding-agent/test/utilities.ts @@ -6,7 +6,7 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } import { homedir, tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { Agent } from "@earendil-works/pi-agent-core"; -import { getModel, type OAuthCredentials, type OAuthProvider } from "@earendil-works/pi-ai"; +import { getModel, type OAuthCredentials, type OAuthProvider } from "@earendil-works/pi-ai/compat"; import { getOAuthApiKey } from "@earendil-works/pi-ai/oauth"; import { AgentSession } from "../src/core/agent-session.ts"; import { AuthStorage } from "../src/core/auth-storage.ts"; diff --git a/packages/coding-agent/vitest.config.ts b/packages/coding-agent/vitest.config.ts index 67ce0fca..fe12421c 100644 --- a/packages/coding-agent/vitest.config.ts +++ b/packages/coding-agent/vitest.config.ts @@ -2,6 +2,7 @@ import { fileURLToPath } from "node:url"; import { defineConfig } from "vitest/config"; const aiSrcIndex = fileURLToPath(new URL("../ai/src/index.ts", import.meta.url)); +const aiSrcCompat = fileURLToPath(new URL("../ai/src/compat.ts", import.meta.url)); const aiSrcOAuth = fileURLToPath(new URL("../ai/src/oauth.ts", import.meta.url)); const agentSrcIndex = fileURLToPath(new URL("../agent/src/index.ts", import.meta.url)); const tuiSrcIndex = fileURLToPath(new URL("../tui/src/index.ts", import.meta.url)); @@ -20,6 +21,7 @@ export default defineConfig({ resolve: { alias: [ { find: /^@earendil-works\/pi-ai$/, replacement: aiSrcIndex }, + { find: /^@earendil-works\/pi-ai\/compat$/, replacement: aiSrcCompat }, { find: /^@earendil-works\/pi-ai\/oauth$/, replacement: aiSrcOAuth }, { find: /^@earendil-works\/pi-agent-core$/, replacement: agentSrcIndex }, { find: /^@earendil-works\/pi-tui$/, replacement: tuiSrcIndex }, diff --git a/scripts/browser-smoke-entry.ts b/scripts/browser-smoke-entry.ts index 066a6099..3ac7bcf0 100644 --- a/scripts/browser-smoke-entry.ts +++ b/scripts/browser-smoke-entry.ts @@ -1,4 +1,5 @@ -import { complete, createAssistantMessageEventStream, getModel, getProviders, Type } from "@earendil-works/pi-ai"; +import { createAssistantMessageEventStream, Type } from "@earendil-works/pi-ai"; +import { complete, getModel, getProviders } from "@earendil-works/pi-ai/compat"; import { Agent, bashExecutionToText, From f0ccbbf0115c1e26e8f4e4de4ff036039bec50a1 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 10 Jun 2026 21:27:21 +0200 Subject: [PATCH 012/122] feat(agent): AgentHarness streams through a required Models instance (phase 6) AgentHarnessOptions.models is required; the harness stream path, compaction, and branch summarization go through models.streamSimple()/ completeSimple() instead of the compat globals. getApiKeyAndHeaders stays and wins per-field over provider-resolved auth, but is no longer required: without it, requests resolve through provider auth. compact()/generateSummary()/generateBranchSummary() take a Models parameter; explicit apiKey becomes optional. StreamFn is redefined structurally (Models.streamSimple satisfies it), dropping the compat type dependency from agent types. Harness tests build per-file Models collections with fauxProvider() and unique provider ids instead of mutating the global api-registry. --- packages/agent/docs/models.md | 9 +-- packages/agent/src/harness/agent-harness.ts | 26 ++++--- .../compaction/branch-summarization.ts | 23 ++++-- .../src/harness/compaction/compaction.ts | 19 +++-- packages/agent/src/harness/types.ts | 8 ++- packages/agent/src/types.ts | 15 ++-- .../test/harness/agent-harness-stream.test.ts | 41 ++++++----- .../agent/test/harness/agent-harness.test.ts | 72 +++++++++++-------- .../agent/test/harness/compaction.test.ts | 62 +++++++++------- packages/agent/test/scratch/simple.ts | 10 ++- 10 files changed, 175 insertions(+), 110 deletions(-) diff --git a/packages/agent/docs/models.md b/packages/agent/docs/models.md index cffcecd7..23d3110c 100644 --- a/packages/agent/docs/models.md +++ b/packages/agent/docs/models.md @@ -820,9 +820,9 @@ Check items off as they land. Keep this list current; it is the working state fo ### Phase 6 — AgentHarness -- [ ] `AgentHarnessOptions.models` required; harness stream path uses `models.streamSimple()`. -- [ ] Compaction/branch-summarization paths use the harness `Models` instance. -- [ ] Harness tests use `createModels()` + faux provider. +- [x] `AgentHarnessOptions.models` required (`readonly models` on the harness); the harness stream path uses `models.streamSimple()`. `StreamFn` redefined structurally (no compat type dependency); `Models.streamSimple` satisfies it. +- [x] Compaction/branch-summarization take the harness `Models` instance; explicit `getApiKeyAndHeaders` auth stays and wins per-field, but is no longer required — requests resolve through provider auth otherwise (the hard "No auth available" throws are gone). +- [x] Harness tests use `createModels()` + `fauxProvider()` with unique per-fake provider ids; no global api-registry state, no unregister bookkeeping. ### Phase 7 — coding-agent bridge (minimal) @@ -843,8 +843,9 @@ The full AuthStorage deletion (`FileCredentialStore` + decorators, see "Replacin ### Deferred / follow-ups -- [ ] Web OAuth implementations (sitegeist-style) behind `oauth: "web"`. +- [ ] Web OAuth implementations (sitegeist-style) as an alternative `OAuthAuth`. - [ ] coding-agent `ModelRegistry` -> session `ModelManager` migration; delete `/compat`. +- [ ] Move ALL internal `/compat` imports to the new API before compat is deleted: every package's src, all tests, and the example extensions (examples then demonstrate the new API; the extension-loader root-to-compat alias dies with compat). Nothing inside the repo may import `/compat` at that point. - [ ] Images API registry redesign (untouched in this pass). ## Error behavior diff --git a/packages/agent/src/harness/agent-harness.ts b/packages/agent/src/harness/agent-harness.ts index 8d6350a5..1afb30c9 100644 --- a/packages/agent/src/harness/agent-harness.ts +++ b/packages/agent/src/harness/agent-harness.ts @@ -1,10 +1,4 @@ -import { - type AssistantMessage, - type ImageContent, - type Model, - streamSimple, - type UserMessage, -} from "@earendil-works/pi-ai/compat"; +import type { AssistantMessage, ImageContent, Model, Models, UserMessage } from "@earendil-works/pi-ai"; import { runAgentLoop } from "../agent-loop.ts"; import type { AgentContext, @@ -178,6 +172,7 @@ export class AgentHarness< > { readonly env: ExecutionEnv; private session: Session; + readonly models: Models; private phase: AgentHarnessPhase = "idle"; private runAbortController?: AbortController; private runPromise?: Promise; @@ -200,6 +195,7 @@ export class AgentHarness< constructor(options: AgentHarnessOptions) { this.env = options.env; this.session = options.session; + this.models = options.models; this.resources = options.resources ?? {}; this.streamOptions = cloneStreamOptions(options.streamOptions); this.systemPrompt = options.systemPrompt; @@ -382,7 +378,7 @@ export class AgentHarness< headers: mergeHeaders(turnState.streamOptions.headers, auth?.headers), }; const requestOptions = await this.emitBeforeProviderRequest(model, turnState.sessionId, snapshotOptions); - return streamSimple(model, context, { + return this.models.streamSimple(model, context, { cacheRetention: requestOptions.cacheRetention, headers: requestOptions.headers, maxRetries: requestOptions.maxRetries, @@ -713,8 +709,8 @@ export class AgentHarness< try { const model = this.model; if (!model) throw new AgentHarnessError("invalid_state", "No model set for compaction"); + // Explicit auth wins; otherwise the request resolves through provider auth. const auth = await this.getApiKeyAndHeaders?.(model); - if (!auth) throw new AgentHarnessError("auth", "No auth available for compaction"); const branchEntries = await this.session.getBranch(); const preparationResult = prepareCompaction(branchEntries, DEFAULT_COMPACTION_SETTINGS); if (!preparationResult.ok) throw preparationResult.error; @@ -733,9 +729,10 @@ export class AgentHarness< ? { ok: true as const, value: provided } : await compact( preparation, + this.models, model, - auth.apiKey, - auth.headers, + auth?.apiKey, + auth?.headers, customInstructions, undefined, this.thinkingLevel, @@ -792,12 +789,13 @@ export class AgentHarness< if (!summaryText && options?.summarize && entries.length > 0) { const model = this.model; if (!model) throw new AgentHarnessError("invalid_state", "No model set for branch summary"); + // Explicit auth wins; otherwise the request resolves through provider auth. const auth = await this.getApiKeyAndHeaders?.(model); - if (!auth) throw new AgentHarnessError("auth", "No auth available for branch summary"); const branchSummary = await generateBranchSummary(entries, { + models: this.models, model, - apiKey: auth.apiKey, - headers: auth.headers, + apiKey: auth?.apiKey, + headers: auth?.headers, signal: new AbortController().signal, customInstructions: hookResult?.customInstructions ?? options?.customInstructions, replaceInstructions: hookResult?.replaceInstructions ?? options?.replaceInstructions, diff --git a/packages/agent/src/harness/compaction/branch-summarization.ts b/packages/agent/src/harness/compaction/branch-summarization.ts index a4563889..1df47d7a 100644 --- a/packages/agent/src/harness/compaction/branch-summarization.ts +++ b/packages/agent/src/harness/compaction/branch-summarization.ts @@ -1,5 +1,5 @@ -import type { Model } from "@earendil-works/pi-ai/compat"; -import { completeSimple } from "@earendil-works/pi-ai/compat"; +import type { Model, Models } from "@earendil-works/pi-ai"; + import type { AgentMessage } from "../../types.ts"; import { convertToLlm, @@ -49,10 +49,12 @@ export interface CollectEntriesResult { /** Options for generating a branch summary. */ export interface GenerateBranchSummaryOptions { + /** Provider collection the summarization request goes through. */ + models: Models; /** Model used for summarization. */ model: Model; - /** API key forwarded to the provider. */ - apiKey: string; + /** Explicit API key; wins over provider-resolved auth. */ + apiKey?: string; /** Optional request headers forwarded to the provider. */ headers?: Record; /** Abort signal for the summarization request. */ @@ -202,7 +204,16 @@ export async function generateBranchSummary( entries: SessionTreeEntry[], options: GenerateBranchSummaryOptions, ): Promise> { - const { model, apiKey, headers, signal, customInstructions, replaceInstructions, reserveTokens = 16384 } = options; + const { + models, + model, + apiKey, + headers, + signal, + customInstructions, + replaceInstructions, + reserveTokens = 16384, + } = options; const contextWindow = model.contextWindow || 128000; const tokenBudget = contextWindow - reserveTokens; @@ -230,7 +241,7 @@ export async function generateBranchSummary( timestamp: Date.now(), }, ]; - const response = await completeSimple( + const response = await models.completeSimple( model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, { apiKey, headers, signal, maxTokens: 2048 }, diff --git a/packages/agent/src/harness/compaction/compaction.ts b/packages/agent/src/harness/compaction/compaction.ts index 93c01e2f..55ea5b98 100644 --- a/packages/agent/src/harness/compaction/compaction.ts +++ b/packages/agent/src/harness/compaction/compaction.ts @@ -1,5 +1,4 @@ -import type { AssistantMessage, ImageContent, Model, TextContent, Usage } from "@earendil-works/pi-ai/compat"; -import { completeSimple } from "@earendil-works/pi-ai/compat"; +import type { AssistantMessage, ImageContent, Model, Models, TextContent, Usage } from "@earendil-works/pi-ai"; import type { AgentMessage, ThinkingLevel } from "../../types.ts"; import { convertToLlm, @@ -455,9 +454,10 @@ Keep each section concise. Preserve exact file paths, function names, and error /** Generate or update a conversation summary for compaction. */ export async function generateSummary( currentMessages: AgentMessage[], + models: Models, model: Model, reserveTokens: number, - apiKey: string, + apiKey?: string, headers?: Record, signal?: AbortSignal, customInstructions?: string, @@ -493,7 +493,7 @@ export async function generateSummary( ? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel } : { maxTokens, signal, apiKey, headers }; - const response = await completeSimple( + const response = await models.completeSimple( model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, completionOptions, @@ -626,8 +626,9 @@ export { serializeConversation } from "./utils.ts"; /** Generate compaction summary data from prepared session history. */ export async function compact( preparation: CompactionPreparation, + models: Models, model: Model, - apiKey: string, + apiKey?: string, headers?: Record, customInstructions?: string, signal?: AbortSignal, @@ -655,6 +656,7 @@ export async function compact( messagesToSummarize.length > 0 ? generateSummary( messagesToSummarize, + models, model, settings.reserveTokens, apiKey, @@ -667,6 +669,7 @@ export async function compact( : Promise.resolve(ok("No prior history.")), generateTurnPrefixSummary( turnPrefixMessages, + models, model, settings.reserveTokens, apiKey, @@ -681,6 +684,7 @@ export async function compact( } else { const summaryResult = await generateSummary( messagesToSummarize, + models, model, settings.reserveTokens, apiKey, @@ -706,9 +710,10 @@ export async function compact( } async function generateTurnPrefixSummary( messages: AgentMessage[], + models: Models, model: Model, reserveTokens: number, - apiKey: string, + apiKey?: string, headers?: Record, signal?: AbortSignal, thinkingLevel?: ThinkingLevel, @@ -728,7 +733,7 @@ async function generateTurnPrefixSummary( }, ]; - const response = await completeSimple( + const response = await models.completeSimple( model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, model.reasoning && thinkingLevel && thinkingLevel !== "off" diff --git a/packages/agent/src/harness/types.ts b/packages/agent/src/harness/types.ts index 4756ca84..0e0aeaaf 100644 --- a/packages/agent/src/harness/types.ts +++ b/packages/agent/src/harness/types.ts @@ -1,4 +1,4 @@ -import type { ImageContent, Model, SimpleStreamOptions, TextContent, Transport } from "@earendil-works/pi-ai"; +import type { ImageContent, Model, Models, SimpleStreamOptions, TextContent, Transport } from "@earendil-works/pi-ai"; import type { AgentEvent, AgentMessage, AgentTool, QueueMode, ThinkingLevel } from "../index.ts"; import type { Session } from "./session/session.ts"; @@ -802,6 +802,12 @@ export interface AgentHarnessOptions< > { env: ExecutionEnv; session: Session; + /** + * Provider collection used for all model requests (turn streaming, + * compaction, branch summarization). Auth resolves through the providers' + * auth; explicit per-request values (`getApiKeyAndHeaders`) win per field. + */ + models: Models; tools?: TTool[]; /** * Concrete resources available to explicit invocation methods and system-prompt callbacks. diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts index 3365b304..fb6f0d8a 100644 --- a/packages/agent/src/types.ts +++ b/packages/agent/src/types.ts @@ -1,19 +1,22 @@ import type { + Api, AssistantMessage, AssistantMessageEvent, + AssistantMessageEventStream, + Context, ImageContent, Message, Model, SimpleStreamOptions, - streamSimple, TextContent, Tool, ToolResultMessage, -} from "@earendil-works/pi-ai/compat"; +} from "@earendil-works/pi-ai"; import type { Static, TSchema } from "typebox"; /** - * Stream function used by the agent loop. + * Stream function used by the agent loop. `Models.streamSimple` satisfies + * this shape. * * Contract: * - Must not throw or return a rejected promise for request/model/runtime failures. @@ -22,8 +25,10 @@ import type { Static, TSchema } from "typebox"; * final AssistantMessage with stopReason "error" or "aborted" and errorMessage. */ export type StreamFn = ( - ...args: Parameters -) => ReturnType | Promise>; + model: Model, + context: Context, + options?: SimpleStreamOptions, +) => AssistantMessageEventStream | Promise; /** * Configuration for how tool calls from a single assistant message are executed. diff --git a/packages/agent/test/harness/agent-harness-stream.test.ts b/packages/agent/test/harness/agent-harness-stream.test.ts index ee79564b..b4ed38dc 100644 --- a/packages/agent/test/harness/agent-harness-stream.test.ts +++ b/packages/agent/test/harness/agent-harness-stream.test.ts @@ -1,18 +1,27 @@ -import { fauxAssistantMessage, fauxToolCall, registerFauxProvider, type StreamOptions } from "@earendil-works/pi-ai"; -import { afterEach, describe, expect, it } from "vitest"; +import { + createModels, + type FauxProviderHandle, + fauxAssistantMessage, + fauxProvider, + fauxToolCall, + type StreamOptions, +} from "@earendil-works/pi-ai"; +import { describe, expect, it } from "vitest"; import { AgentHarness } from "../../src/harness/agent-harness.ts"; import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts"; import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.ts"; import { Session } from "../../src/harness/session/session.ts"; import { calculateTool } from "../utils/calculate.ts"; -const registrations: Array<{ unregister(): void }> = []; +/** Shared collection; each faux provider gets a unique id so coexisting fakes route correctly. */ +const models = createModels(); +let fauxCount = 0; -afterEach(() => { - for (const registration of registrations.splice(0)) { - registration.unregister(); - } -}); +function newFaux(): FauxProviderHandle { + const faux = fauxProvider({ provider: `faux-${++fauxCount}` }); + models.setProvider(faux.provider); + return faux; +} function createHarness(options: ConstructorParameters[0]): AgentHarness { return new AgentHarness(options); @@ -29,8 +38,7 @@ function captureOptions(options: StreamOptions | undefined): StreamOptions { describe("AgentHarness stream configuration", () => { it("snapshots stream options and merges auth headers before provider request hooks", async () => { let capturedOptions: StreamOptions | undefined; - const registration = registerFauxProvider(); - registrations.push(registration); + const registration = newFaux(); registration.setResponses([ (_context, options) => { capturedOptions = options; @@ -40,6 +48,7 @@ describe("AgentHarness stream configuration", () => { const session = new Session(new InMemorySessionStorage({ metadata: { id: "session-1", createdAt: "now" } })); const harness = createHarness({ + models, env: new NodeExecutionEnv({ cwd: process.cwd() }), session, model: registration.getModel(), @@ -81,8 +90,7 @@ describe("AgentHarness stream configuration", () => { it("chains provider request patches and supports deletion semantics", async () => { let capturedOptions: StreamOptions | undefined; - const registration = registerFauxProvider(); - registrations.push(registration); + const registration = newFaux(); registration.setResponses([ (_context, options) => { capturedOptions = options; @@ -91,6 +99,7 @@ describe("AgentHarness stream configuration", () => { ]); const harness = createHarness({ + models, env: new NodeExecutionEnv({ cwd: process.cwd() }), session: new Session(new InMemorySessionStorage()), model: registration.getModel(), @@ -133,8 +142,7 @@ describe("AgentHarness stream configuration", () => { it("uses updated stream options for save-point snapshots without mutating the active request", async () => { const capturedOptions: StreamOptions[] = []; - const registration = registerFauxProvider(); - registrations.push(registration); + const registration = newFaux(); registration.setResponses([ (_context, options) => { capturedOptions.push(captureOptions(options)); @@ -149,6 +157,7 @@ describe("AgentHarness stream configuration", () => { ]); const harness = createHarness({ + models, env: new NodeExecutionEnv({ cwd: process.cwd() }), session: new Session(new InMemorySessionStorage()), model: registration.getModel(), @@ -174,8 +183,7 @@ describe("AgentHarness stream configuration", () => { it("chains provider payload hooks", async () => { const seenPayloads: unknown[] = []; let finalPayload: unknown; - const registration = registerFauxProvider(); - registrations.push(registration); + const registration = newFaux(); registration.setResponses([ async (_context, options, _state, model) => { finalPayload = await options?.onPayload?.({ steps: ["provider"] }, model); @@ -184,6 +192,7 @@ describe("AgentHarness stream configuration", () => { ]); const harness = createHarness({ + models, env: new NodeExecutionEnv({ cwd: process.cwd() }), session: new Session(new InMemorySessionStorage()), model: registration.getModel(), diff --git a/packages/agent/test/harness/agent-harness.test.ts b/packages/agent/test/harness/agent-harness.test.ts index d54040a8..d13eca84 100644 --- a/packages/agent/test/harness/agent-harness.test.ts +++ b/packages/agent/test/harness/agent-harness.test.ts @@ -1,5 +1,13 @@ -import { fauxAssistantMessage, fauxToolCall, getModel, registerFauxProvider } from "@earendil-works/pi-ai/compat"; -import { afterEach, describe, expect, it } from "vitest"; +import { + createModels, + type FauxProviderHandle, + fauxAssistantMessage, + fauxProvider, + fauxToolCall, + type RegisterFauxProviderOptions, +} from "@earendil-works/pi-ai"; +import { getModel } from "@earendil-works/pi-ai/compat"; +import { describe, expect, it } from "vitest"; import { AgentHarness } from "../../src/harness/agent-harness.ts"; import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts"; import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.ts"; @@ -17,7 +25,15 @@ interface AppPromptTemplate extends PromptTemplate { source: "project" | "user"; } -const registrations: Array<{ unregister(): void }> = []; +/** Shared collection; each faux provider gets a unique id so coexisting fakes route correctly. */ +const models = createModels(); +let fauxCount = 0; + +function newFaux(options: RegisterFauxProviderOptions = {}): FauxProviderHandle { + const faux = fauxProvider({ provider: `faux-${++fauxCount}`, ...options }); + models.setProvider(faux.provider); + return faux; +} function textFromUserMessages(messages: Array<{ role: string; content: unknown }>): string[] { return messages.flatMap((message) => { @@ -44,18 +60,13 @@ function getReasoning(options: unknown): unknown { return options.reasoning; } -afterEach(() => { - for (const registration of registrations.splice(0)) { - registration.unregister(); - } -}); - describe("AgentHarness", () => { it("constructs directly and exposes queue modes", () => { const session = new Session(new InMemorySessionStorage()); const env = new NodeExecutionEnv({ cwd: process.cwd() }); const initialModel = getModel("anthropic", "claude-sonnet-4-5"); const harness = new AgentHarness({ + models, env, session, model: initialModel, @@ -76,8 +87,7 @@ describe("AgentHarness", () => { }); it("drains one queued steering message at a time and emits queue updates", async () => { - const registration = registerFauxProvider(); - registrations.push(registration); + const registration = newFaux(); const userCounts: number[] = []; registration.setResponses([ (context) => { @@ -94,6 +104,7 @@ describe("AgentHarness", () => { }, ]); const harness = new AgentHarness({ + models, env: new NodeExecutionEnv({ cwd: process.cwd() }), session: new Session(new InMemorySessionStorage()), model: registration.getModel(), @@ -119,8 +130,7 @@ describe("AgentHarness", () => { }); it("appends before_agent_start messages and persists them", async () => { - const registration = registerFauxProvider(); - registrations.push(registration); + const registration = newFaux(); let requestText: string[] = []; registration.setResponses([ (context) => { @@ -130,6 +140,7 @@ describe("AgentHarness", () => { ]); const session = new Session(new InMemorySessionStorage()); const harness = new AgentHarness({ + models, env: new NodeExecutionEnv({ cwd: process.cwd() }), session, model: registration.getModel(), @@ -151,8 +162,7 @@ describe("AgentHarness", () => { }); it("abort clears steer and follow-up queues but preserves next-turn messages", async () => { - const registration = registerFauxProvider(); - registrations.push(registration); + const registration = newFaux(); let releaseFirstResponse: (() => void) | undefined; let abortedSignal: AbortSignal | undefined; const firstResponseReleased = new Promise((resolve) => { @@ -171,6 +181,7 @@ describe("AgentHarness", () => { }, ]); const harness = new AgentHarness({ + models, env: new NodeExecutionEnv({ cwd: process.cwd() }), session: new Session(new InMemorySessionStorage()), model: registration.getModel(), @@ -206,8 +217,7 @@ describe("AgentHarness", () => { }); it("drains follow-up messages one at a time after the agent would otherwise stop", async () => { - const registration = registerFauxProvider(); - registrations.push(registration); + const registration = newFaux(); const userCounts: number[] = []; registration.setResponses([ (context) => { @@ -224,6 +234,7 @@ describe("AgentHarness", () => { }, ]); const harness = new AgentHarness({ + models, env: new NodeExecutionEnv({ cwd: process.cwd() }), session: new Session(new InMemorySessionStorage()), model: registration.getModel(), @@ -249,11 +260,11 @@ describe("AgentHarness", () => { }); it("settles thrown hook failures with persisted assistant error messages", async () => { - const registration = registerFauxProvider(); - registrations.push(registration); + const registration = newFaux(); registration.setResponses([() => fauxAssistantMessage("should not be used")]); const session = new Session(new InMemorySessionStorage()); const harness = new AgentHarness({ + models, env: new NodeExecutionEnv({ cwd: process.cwd() }), session, model: registration.getModel(), @@ -280,13 +291,12 @@ describe("AgentHarness", () => { }); it("refreshes model, thinking level, resources, system prompt, and active tools at save points", async () => { - const registration = registerFauxProvider({ + const registration = newFaux({ models: [ { id: "first", reasoning: true }, { id: "second", reasoning: true }, ], }); - registrations.push(registration); const secondModel = registration.getModel("second"); if (!secondModel) throw new Error("missing second faux model"); const captured: Array<{ modelId: string; reasoning: unknown; systemPrompt: string; tools: string[] }> = []; @@ -313,6 +323,7 @@ describe("AgentHarness", () => { }, ]); const harness = new AgentHarness({ + models, env: new NodeExecutionEnv({ cwd: process.cwd() }), session: new Session(new InMemorySessionStorage()), model: registration.getModel(), @@ -345,11 +356,11 @@ describe("AgentHarness", () => { }); it("orders pending listener session writes after agent-emitted messages", async () => { - const registration = registerFauxProvider(); - registrations.push(registration); + const registration = newFaux(); registration.setResponses([() => fauxAssistantMessage("ok")]); const session = new Session(new InMemorySessionStorage()); const harness = new AgentHarness({ + models, env: new NodeExecutionEnv({ cwd: process.cwd() }), session, model: registration.getModel(), @@ -376,11 +387,11 @@ describe("AgentHarness", () => { }); it("waitForIdle waits for external run settlement and awaited listeners", async () => { - const registration = registerFauxProvider(); - registrations.push(registration); + const registration = newFaux(); registration.setResponses([() => fauxAssistantMessage("ok")]); const barrier = deferred(); const harness = new AgentHarness({ + models, env: new NodeExecutionEnv({ cwd: process.cwd() }), session: new Session(new InMemorySessionStorage()), model: registration.getModel(), @@ -408,8 +419,7 @@ describe("AgentHarness", () => { }); it("runs tool_call and tool_result hooks through the direct loop", async () => { - const registration = registerFauxProvider(); - registrations.push(registration); + const registration = newFaux(); registration.setResponses([ () => fauxAssistantMessage(fauxToolCall("calculate", { expression: "2 + 2" }, { id: "call-1" }), { @@ -418,6 +428,7 @@ describe("AgentHarness", () => { ]); const session = new Session(new InMemorySessionStorage()); const harness = new AgentHarness({ + models, env: new NodeExecutionEnv({ cwd: process.cwd() }), session, model: registration.getModel(), @@ -462,6 +473,7 @@ describe("AgentHarness", () => { const inspectTool: AppTool = { ...calculateTool, name: "inspect", source: "builtin" }; const searchTool: AppTool = { ...calculateTool, name: "search", source: "extension" }; const harness = new AgentHarness({ + models, env, session, model, @@ -530,11 +542,12 @@ describe("AgentHarness", () => { const env = new NodeExecutionEnv({ cwd: process.cwd() }); const model = getModel("anthropic", "claude-sonnet-4-5"); expect( - () => new AgentHarness({ env, session, model, tools: [calculateTool], activeToolNames: ["missing"] }), + () => new AgentHarness({ env, session, models, model, tools: [calculateTool], activeToolNames: ["missing"] }), ).toThrow(/Unknown tool/); expect( () => new AgentHarness({ + models, env, session, model, @@ -545,6 +558,7 @@ describe("AgentHarness", () => { expect( () => new AgentHarness({ + models, env, session, model, @@ -558,7 +572,7 @@ describe("AgentHarness", () => { const session = new Session(new InMemorySessionStorage()); const env = new NodeExecutionEnv({ cwd: process.cwd() }); const model = getModel("anthropic", "claude-sonnet-4-5"); - const harness = new AgentHarness({ env, session, model }); + const harness = new AgentHarness({ env, session, models, model }); const skill: AppSkill = { name: "inspect", description: "Inspect things", diff --git a/packages/agent/test/harness/compaction.test.ts b/packages/agent/test/harness/compaction.test.ts index b694d9f5..540cb332 100644 --- a/packages/agent/test/harness/compaction.test.ts +++ b/packages/agent/test/harness/compaction.test.ts @@ -1,13 +1,14 @@ import { type AssistantMessage, - type FauxProviderRegistration, + createModels, + type FauxProviderHandle, fauxAssistantMessage, + fauxProvider, type Message, type Model, - registerFauxProvider, type Usage, } from "@earendil-works/pi-ai"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; import { type CompactionPreparation, calculateContextTokens, @@ -121,11 +122,13 @@ function createModelChangeEntry(provider: string, modelId: string, parentId: str }; } -function createFauxModel( - reasoning: boolean, - maxTokens = 8192, -): { faux: FauxProviderRegistration; model: Model } { - const faux = registerFauxProvider({ +/** Shared collection; each faux provider gets a unique id so coexisting fakes route correctly. */ +const models = createModels(); +let fauxCount = 0; + +function createFauxModel(reasoning: boolean, maxTokens = 8192): { faux: FauxProviderHandle; model: Model } { + const faux = fauxProvider({ + provider: `faux-${++fauxCount}`, models: [ { id: reasoning ? "reasoning-model" : "non-reasoning-model", @@ -135,18 +138,10 @@ function createFauxModel( }, ], }); - fauxRegistrations.push(faux); + models.setProvider(faux.provider); return { faux, model: faux.getModel() }; } -const fauxRegistrations: FauxProviderRegistration[] = []; - -afterEach(() => { - while (fauxRegistrations.length > 0) { - fauxRegistrations.pop()?.unregister(); - } -}); - describe("harness compaction", () => { beforeEach(() => { nextId = 0; @@ -447,6 +442,7 @@ describe("harness compaction", () => { getOrThrow( await generateSummary( messages, + models, reasoningModel, 2000, "test-key", @@ -467,7 +463,18 @@ describe("harness compaction", () => { }, ]); getOrThrow( - await generateSummary(messages, offModel, 2000, "test-key", undefined, undefined, undefined, undefined, "off"), + await generateSummary( + messages, + models, + offModel, + 2000, + "test-key", + undefined, + undefined, + undefined, + undefined, + "off", + ), ); expect(seenOptions[1]).not.toHaveProperty("reasoning"); @@ -481,6 +488,7 @@ describe("harness compaction", () => { getOrThrow( await generateSummary( messages, + models, nonReasoningModel, 2000, "test-key", @@ -510,6 +518,7 @@ describe("harness compaction", () => { const summary = getOrThrow( await generateSummary( messages, + models, model, 2000, "test-key", @@ -529,7 +538,7 @@ describe("harness compaction", () => { const messages: AgentMessage[] = [createUserMessage("Summarize this.")]; const { faux: errorFaux, model: errorModel } = createFauxModel(false); errorFaux.setResponses([fauxAssistantMessage("", { stopReason: "error", errorMessage: "boom" })]); - const errorResult = await generateSummary(messages, errorModel, 2000, "test-key"); + const errorResult = await generateSummary(messages, models, errorModel, 2000, "test-key"); expect(errorResult).toMatchObject({ ok: false, error: { code: "summarization_failed", message: "Summarization failed: boom" }, @@ -537,7 +546,7 @@ describe("harness compaction", () => { const { faux: abortedFaux, model: abortedModel } = createFauxModel(false); abortedFaux.setResponses([fauxAssistantMessage("", { stopReason: "aborted", errorMessage: "stopped" })]); - const abortedResult = await generateSummary(messages, abortedModel, 2000, "test-key"); + const abortedResult = await generateSummary(messages, models, abortedModel, 2000, "test-key"); expect(abortedResult).toMatchObject({ ok: false, error: { code: "aborted", message: "stopped" } }); }); @@ -565,7 +574,7 @@ describe("harness compaction", () => { settings: { enabled: true, reserveTokens: 500000, keepRecentTokens: 20000 }, }; - getOrThrow(await compact(preparation, model, "test-key")); + getOrThrow(await compact(preparation, models, model, "test-key")); expect(seenOptions.map((options) => options?.maxTokens)).toEqual([128000, 128000]); }); @@ -583,7 +592,7 @@ describe("harness compaction", () => { }; const { faux: historyFaux, model: historyModel } = createFauxModel(false); historyFaux.setResponses([fauxAssistantMessage("", { stopReason: "error", errorMessage: "history failed" })]); - expect(await compact(preparation, historyModel, "test-key")).toMatchObject({ + expect(await compact(preparation, models, historyModel, "test-key")).toMatchObject({ ok: false, error: { code: "summarization_failed", message: "Summarization failed: history failed" }, }); @@ -591,6 +600,7 @@ describe("harness compaction", () => { const { model: invalidModel } = createFauxModel(false); const invalidResult = await compact( { ...preparation, messagesToSummarize: [], firstKeptEntryId: "" }, + models, invalidModel, "test-key", ); @@ -617,7 +627,7 @@ describe("harness compaction", () => { settings: { enabled: true, reserveTokens: 2000, keepRecentTokens: 20 }, }; - getOrThrow(await compact(preparation, model, "test-key", undefined, undefined, undefined, "high")); + getOrThrow(await compact(preparation, models, model, "test-key", undefined, undefined, undefined, "high")); expect(seenOptions[0]).toMatchObject({ reasoning: "high" }); }); @@ -636,14 +646,14 @@ describe("harness compaction", () => { const { faux, model } = createFauxModel(false); faux.setResponses([fauxAssistantMessage("", { stopReason: "error", errorMessage: "prefix failed" })]); - expect(await compact(preparation, model, "test-key")).toMatchObject({ + expect(await compact(preparation, models, model, "test-key")).toMatchObject({ ok: false, error: { code: "summarization_failed", message: "Turn prefix summarization failed: prefix failed" }, }); const { faux: abortedFaux, model: abortedModel } = createFauxModel(false); abortedFaux.setResponses([fauxAssistantMessage("", { stopReason: "aborted", errorMessage: "prefix stopped" })]); - expect(await compact(preparation, abortedModel, "test-key")).toMatchObject({ + expect(await compact(preparation, models, abortedModel, "test-key")).toMatchObject({ ok: false, error: { code: "aborted", message: "prefix stopped" }, }); @@ -662,7 +672,7 @@ describe("harness compaction", () => { expect(preparation).toBeDefined(); const { faux, model } = createFauxModel(false); faux.setResponses([fauxAssistantMessage("## Goal\nTest summary")]); - const result = getOrThrow(await compact(preparation!, model, "test-key")); + const result = getOrThrow(await compact(preparation!, models, model, "test-key")); expect(result.summary.length).toBeGreaterThan(0); expect(result.firstKeptEntryId).toBeTruthy(); expect(result.details).toBeDefined(); diff --git a/packages/agent/test/scratch/simple.ts b/packages/agent/test/scratch/simple.ts index 6d7bc038..86eae721 100644 --- a/packages/agent/test/scratch/simple.ts +++ b/packages/agent/test/scratch/simple.ts @@ -1,6 +1,8 @@ import { homedir } from "node:os"; import { join } from "node:path"; -import { getModel } from "@earendil-works/pi-ai/compat"; +import { createModels } from "@earendil-works/pi-ai"; +import { getBuiltinModel } from "@earendil-works/pi-ai/providers/all"; +import { openaiProvider } from "@earendil-works/pi-ai/providers/openai"; import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts"; import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.ts"; import { @@ -35,11 +37,15 @@ const { promptTemplates: sourcedPromptTemplates } = await loadSourcedPromptTempl (promptTemplate, source) => ({ ...promptTemplate, source }), ); +const models = createModels(); +models.setProvider(openaiProvider()); + const session = new Session(new InMemorySessionStorage()); const agent = new AgentHarness({ env, session, - model: getModel("openai", "gpt-5.5"), + models, + model: getBuiltinModel("openai", "gpt-5.5"), thinkingLevel: "low", systemPrompt: ({ env, resources }) => [ From 10a575b76b574a6cdefb9b529aff0de00618bc25 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 10 Jun 2026 21:31:56 +0200 Subject: [PATCH 013/122] docs: changelogs and phase restructure for the models refactor (phase 8) Breaking-change entries with migration guides: pi-ai (compat entrypoint, Provider -> ProviderId, api module moves, new Models/ provider/auth API), agent (required AgentHarnessOptions.models, compaction signatures, structural StreamFn), coding-agent (extension author note: runtime unaffected via loader compat alias). models.md: Phase 7 closed out (import switch landed with phase 5; the gated bullets move to a new Phase 9 ModelManager-migration outline: per-session Models, AgentSession streaming, AuthStorage replacement, login UI on OAuthAuth, cloudflare cleanup, internal compat removal, compat deletion). --- packages/agent/CHANGELOG.md | 6 ++++++ packages/agent/docs/models.md | 32 ++++++++++++++++++------------ packages/ai/CHANGELOG.md | 14 +++++++++++++ packages/coding-agent/CHANGELOG.md | 4 ++++ 4 files changed, 43 insertions(+), 13 deletions(-) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index c1c02515..06e8cad7 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -2,6 +2,12 @@ ## [Unreleased] +### Breaking Changes + +- `AgentHarnessOptions.models` is required: the harness streams turns, compaction, and branch summarization through the provided `Models` instance (`models.streamSimple()`/`completeSimple()`) instead of the pi-ai global stream functions. Build one with `createModels()` + provider factories (or `builtinModels()` from `@earendil-works/pi-ai/providers/all`); tests use `fauxProvider()`. `getApiKeyAndHeaders` still wins per field but is no longer required — without it, requests resolve auth through the providers. +- `compact()`, `generateSummary()`, and `generateBranchSummary()` take a `Models` parameter; the explicit `apiKey` is now optional. +- `StreamFn` is defined structurally (`(model, context, options?) => AssistantMessageEventStream | Promise<...>`); `Models.streamSimple` satisfies it. + ## [0.79.1] - 2026-06-09 ## [0.79.0] - 2026-06-08 diff --git a/packages/agent/docs/models.md b/packages/agent/docs/models.md index 23d3110c..cff784ae 100644 --- a/packages/agent/docs/models.md +++ b/packages/agent/docs/models.md @@ -826,26 +826,32 @@ Check items off as they land. Keep this list current; it is the working state fo ### Phase 7 — coding-agent bridge (minimal) -- [ ] Construct `Models` for the harness (builtins + legacy api-dispatch fallback for ModelRegistry custom providers). -- [ ] Switch old-global imports to `@earendil-works/pi-ai/compat`. -- [ ] Login dialog adapter for `prompt()/notify()` callbacks. -- [ ] Cloudflare cleanup (only after builtin streaming goes through `Models.getAuth`): the cloudflare provider factories' `ApiKeyAuth.resolve` reads key + `CLOUDFLARE_ACCOUNT_ID` (+ `CLOUDFLARE_GATEWAY_ID`) from credential metadata/env, substitutes the `{...}` placeholders in `model.baseUrl`, and returns it as `ModelAuth.baseUrl` (Copilot pattern); unconfigured ids report "not configured" instead of throwing mid-request. Then `resolveCloudflareBaseUrl`/`isCloudflareProvider` drop out of `api/anthropic-messages.ts`, `api/openai-completions.ts`, and `api/openai-responses.ts`; `api/cloudflare.ts` shrinks to the generator's baseUrl constants. - -The full AuthStorage deletion (`FileCredentialStore` + decorators, see "Replacing AuthStorage") happens in the later ModelManager migration, not this pass. +- [x] Switch old-global imports to `@earendil-works/pi-ai/compat` (landed with Phase 5; compat is a superset so the switch was path-only). Extension loader resolves the pi-ai root to compat as the runtime grace period. +- [x] Everything else originally sketched here is gated on coding-agent actually streaming through a `Models` instance — coding-agent's `AgentSession` drives the low-level `Agent` via `streamFn`, not the harness — and moved to Phase 9. ### Phase 8 — wrap-up -- [ ] Update/add tests; run affected suites (`./test.sh` or per-package vitest). -- [ ] `packages/ai/CHANGELOG.md`: `### Breaking Changes` entry with a migration guide (old global `stream/streamSimple/complete/completeSimple`, `getModel/getModels/getProviders`, `registerApiProvider`, `Provider` -> `ProviderId` rename, OAuth callback changes; old API -> `createModels()`/provider factories or `/compat` as interim). -- [ ] `packages/coding-agent/CHANGELOG.md`: `### Breaking Changes` entry with a migration guide for extension authors who work directly with pi-ai through coding-agent (e.g. custom providers via `registerApiProvider`, model access, login/auth hooks): what changed, what to import now, compat timeline. -- [ ] `packages/agent/CHANGELOG.md`: `### Breaking Changes` entry for required `AgentHarnessOptions.models`. -- [ ] `npm run check` clean. +- [x] Update/add tests; run affected suites (tests landed with each phase; `./test.sh` green throughout). +- [x] `packages/ai/CHANGELOG.md`: `### Breaking Changes` with migration guide (compat entrypoint, `Provider` -> `ProviderId`, api module moves) + `### Added` for the new Models/provider/auth API. +- [x] `packages/coding-agent/CHANGELOG.md`: `### Changed` entry for extension authors — runtime unaffected (loader resolves the pi-ai root to compat), typecheck nudges to `/compat` or the new API; removal happens later with a migration guide. +- [x] `packages/agent/CHANGELOG.md`: `### Breaking Changes` for required `AgentHarnessOptions.models`, compaction signature changes, structural `StreamFn`. +- [x] `npm run check` clean. + +### Phase 9 — ModelManager migration (separate pass, not started) + +The big one: coding-agent moves off ModelRegistry/AuthStorage onto `Models` + `CredentialStore`, and compat dies. Ordering sketch: + +- [ ] coding-agent constructs a `Models` instance per session: builtins (`builtinModels()`), models.json custom providers via `createProvider()` + `withProviderOverrides`, extension custom providers as real providers (legacy `registerApiProvider` extensions bridged by a catch-all api-dispatch provider until extensions migrate). +- [ ] `AgentSession` streams through that instance (directly or by adopting `AgentHarness`); `agent.streamFn` identity checks and env-key injection die. +- [ ] AuthStorage replaced per "Replacing AuthStorage": `FileCredentialStore` (ports the lock backend), `withConfigValues` (`$ENV`/`!command`), `withRuntimeOverrides` (`--api-key`); custom providers carry their own `ApiKeyAuth` (kills `fallbackResolver`). +- [ ] Login/logout/status UIs move to `ProviderAuth` (`OAuthAuth.login` + `prompt()/notify()` adapter in the login dialog); the old `pi-ai/oauth` registry and `OAuthProviderInterface` (incl. `usesCallbackServer`) are deleted. +- [ ] Cloudflare cleanup (gated on builtin streaming going through `Models.getAuth`): cloudflare factories' `ApiKeyAuth.resolve` reads key + `CLOUDFLARE_ACCOUNT_ID` (+ `CLOUDFLARE_GATEWAY_ID`) from credential metadata/env, substitutes the `{...}` placeholders in `model.baseUrl`, and returns `ModelAuth.baseUrl` (Copilot pattern); unconfigured ids report "not configured". `resolveCloudflareBaseUrl`/`isCloudflareProvider` drop out of `api/anthropic-messages.ts`, `api/openai-completions.ts`, `api/openai-responses.ts`. +- [ ] 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`, `api-registry.ts`, `env-api-keys.ts`, and the extension-loader root-to-compat alias. This is the extension-author breaking release; changelog carries the migration guide. ### Deferred / follow-ups - [ ] Web OAuth implementations (sitegeist-style) as an alternative `OAuthAuth`. -- [ ] coding-agent `ModelRegistry` -> session `ModelManager` migration; delete `/compat`. -- [ ] Move ALL internal `/compat` imports to the new API before compat is deleted: every package's src, all tests, and the example extensions (examples then demonstrate the new API; the extension-loader root-to-compat alias dies with compat). Nothing inside the repo may import `/compat` at that point. - [ ] Images API registry redesign (untouched in this pass). ## Error behavior diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 5af75b1e..101fabf2 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,20 @@ ## [Unreleased] +### Breaking Changes + +- The root entrypoint (`@earendil-works/pi-ai`) is now core-only and side-effect free. The old global API moved to the temporary `@earendil-works/pi-ai/compat` entrypoint, a strict superset of the root: switching a file's import path is the only migration step. Moved symbols include `stream`/`complete`/`streamSimple`/`completeSimple`, `getModel`/`getModels`/`getProviders` (now deprecated aliases of `getBuiltinModel`/`getBuiltinModels`/`getBuiltinProviders` from `@earendil-works/pi-ai/providers/all`), `registerApiProvider`/`unregisterApiProviders`/`resetApiProviders`/`getApiProvider`, `getEnvApiKey`/`findEnvKeys`, `setBedrockProviderModule`, the per-API lazy stream wrappers (`anthropicMessagesApi`, ...), and the image-generation API. `/compat` will be removed with the coding-agent ModelManager migration; new code uses `createModels()` and the provider factories instead. +- Renamed the `Provider` type to `ProviderId`. `Provider` now names the runtime provider interface (id, name, auth, model listing, stream behavior). +- API implementation modules moved from `src/providers/` to `@earendil-works/pi-ai/api/*`, renamed by API id (`anthropic` -> `api/anthropic-messages`, `google` -> `api/google-generative-ai`, `mistral` -> `api/mistral-conversations`, `amazon-bedrock` -> `api/bedrock-converse-stream`), each exporting exactly `stream` and `streamSimple`. The old per-impl export names (`streamAnthropic`, `streamSimpleAnthropic`, ...) are gone; the legacy package subpaths (`./anthropic`, `./google`, ...) keep working and point at the new modules. + +### Added + +- New `Models` runtime: `createModels()` builds an isolated provider collection with async model listing, auth resolution (`getAuth`), and `stream`/`complete`/`streamSimple`/`completeSimple` that resolve auth through the owning provider. `createProvider()` builds providers from parts (single API implementation or a map dispatched on `model.api`); `hasApi()` narrows dynamically listed models. +- Provider auth substrate: `ProviderAuth` (`{ apiKey?, oauth? }`), one type-tagged credential per provider, `CredentialStore` (`read`/`modify`/`delete` with serialized writes; in-memory default), `envApiKeyAuth()`, `lazyOAuth()`, and injectable `AuthContext`. OAuth refresh runs under the store lock with double-checked expiry; a stored credential owns its provider (no silent env fallback). +- One provider factory per built-in provider under `@earendil-works/pi-ai/providers/*` (e.g. `anthropicProvider()`, `openrouterProvider()`), plus `@earendil-works/pi-ai/providers/all` with `builtinProviders()`/`builtinModels()` and typed `getBuiltin*` catalog reads. Generated catalogs are split per provider, so importing one provider pulls one catalog; `sideEffects` metadata makes the package tree-shakeable. +- OAuth flows (Anthropic, OpenAI Codex, GitHub Copilot) gained `OAuthAuth` adapters (`login`/`refresh`/`toAuth`) on unified `prompt()`/`notify()` login callbacks; Copilot's per-credential base URL is derived in `toAuth()`. +- `fauxProvider()` returns a faux `Provider` for tests built on explicit `Models` collections. + - When Amazon Bedrock rejects an unsupported data retention mode, the error now links the AWS data retention documentation ([#5561](https://github.com/earendil-works/pi/pull/5561) by [@unexge](https://github.com/unexge)). ### Fixed diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index f24e827e..1f234c1e 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- pi-ai's old global API (`stream`/`complete`/`completeSimple`, `getModel`/`getModels`/`getProviders`, `registerApiProvider`, `getEnvApiKey`, ...) moved off the `@earendil-works/pi-ai` root entrypoint to `@earendil-works/pi-ai/compat`. Extensions are not affected at runtime: the extension loader resolves the pi-ai root to the compat entrypoint (a strict superset), so existing extensions keep working unchanged. Extension sources that typecheck against pi-ai's published types should switch those imports to `@earendil-works/pi-ai/compat` (or migrate to the new `createModels()`/provider-factory API). The compat entrypoint and the loader alias will be removed in a future release with a migration guide. + ### Added - Added an experimental first-time setup flow behind `PI_EXPERIMENTAL=1` that asks for a dark/light theme choice (preselecting the detected appearance) and opt-in analytics data sharing on first launch with the default agent directory; opting in stores a `trackingId` in `settings.json`. From 9ab1292679bb4137cf0ddcd5523099a06f159d43 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 10 Jun 2026 21:39:13 +0200 Subject: [PATCH 014/122] feat(agent): Models is the harness's only auth path Remove AgentHarnessOptions.getApiKeyAndHeaders: turn streaming, compaction, and branch summarization resolve auth exclusively through the injected Models instance. compact()/generateSummary()/ generateBranchSummary() lose their explicit apiKey/headers parameters. --- packages/agent/CHANGELOG.md | 4 +- packages/agent/docs/models.md | 2 +- packages/agent/src/harness/agent-harness.ts | 37 +--------- .../compaction/branch-summarization.ts | 19 +---- .../src/harness/compaction/compaction.ts | 29 ++------ packages/agent/src/harness/types.ts | 5 +- .../test/harness/agent-harness-stream.test.ts | 8 +-- .../agent/test/harness/compaction.test.ts | 72 ++++--------------- 8 files changed, 30 insertions(+), 146 deletions(-) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 06e8cad7..8aed7436 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -4,8 +4,8 @@ ### Breaking Changes -- `AgentHarnessOptions.models` is required: the harness streams turns, compaction, and branch summarization through the provided `Models` instance (`models.streamSimple()`/`completeSimple()`) instead of the pi-ai global stream functions. Build one with `createModels()` + provider factories (or `builtinModels()` from `@earendil-works/pi-ai/providers/all`); tests use `fauxProvider()`. `getApiKeyAndHeaders` still wins per field but is no longer required — without it, requests resolve auth through the providers. -- `compact()`, `generateSummary()`, and `generateBranchSummary()` take a `Models` parameter; the explicit `apiKey` is now optional. +- `AgentHarnessOptions.models` is required and is the only auth path: the harness streams turns, compaction, and branch summarization through the provided `Models` instance (`models.streamSimple()`/`completeSimple()`), resolving auth through the providers. `AgentHarnessOptions.getApiKeyAndHeaders` is removed — apps that resolved keys per request now express that as provider auth (`ApiKeyAuth`/`OAuthAuth`) on the providers in the `Models` collection. Build one with `createModels()` + provider factories (or `builtinModels()` from `@earendil-works/pi-ai/providers/all`); tests use `fauxProvider()`. +- `compact()`, `generateSummary()`, and `generateBranchSummary()` take a `Models` parameter and no longer accept explicit `apiKey`/`headers`. - `StreamFn` is defined structurally (`(model, context, options?) => AssistantMessageEventStream | Promise<...>`); `Models.streamSimple` satisfies it. ## [0.79.1] - 2026-06-09 diff --git a/packages/agent/docs/models.md b/packages/agent/docs/models.md index cff784ae..f5cb00ce 100644 --- a/packages/agent/docs/models.md +++ b/packages/agent/docs/models.md @@ -821,7 +821,7 @@ Check items off as they land. Keep this list current; it is the working state fo ### Phase 6 — AgentHarness - [x] `AgentHarnessOptions.models` required (`readonly models` on the harness); the harness stream path uses `models.streamSimple()`. `StreamFn` redefined structurally (no compat type dependency); `Models.streamSimple` satisfies it. -- [x] Compaction/branch-summarization take the harness `Models` instance; explicit `getApiKeyAndHeaders` auth stays and wins per-field, but is no longer required — requests resolve through provider auth otherwise (the hard "No auth available" throws are gone). +- [x] Compaction/branch-summarization take the harness `Models` instance. `getApiKeyAndHeaders` is removed entirely — `Models` is the only auth path; per-request key resolution becomes provider auth on the collection. `compact()`/`generateSummary()`/`generateBranchSummary()` lose their explicit `apiKey`/`headers` parameters. - [x] Harness tests use `createModels()` + `fauxProvider()` with unique per-fake provider ids; no global api-registry state, no unregister bookkeeping. ### Phase 7 — coding-agent bridge (minimal) diff --git a/packages/agent/src/harness/agent-harness.ts b/packages/agent/src/harness/agent-harness.ts index 1afb30c9..1d09b054 100644 --- a/packages/agent/src/harness/agent-harness.ts +++ b/packages/agent/src/harness/agent-harness.ts @@ -69,17 +69,6 @@ function cloneStreamOptions(streamOptions?: AgentHarnessStreamOptions): AgentHar }; } -function mergeHeaders(...headers: Array | undefined>): Record | undefined { - const merged: Record = {}; - let hasHeaders = false; - for (const entry of headers) { - if (!entry) continue; - Object.assign(merged, entry); - hasHeaders = true; - } - return hasHeaders ? merged : undefined; -} - function findDuplicateNames(names: string[]): string[] { const seen = new Set(); const duplicates = new Set(); @@ -181,7 +170,6 @@ export class AgentHarness< private thinkingLevel: ThinkingLevel; private systemPrompt: AgentHarnessOptions["systemPrompt"]; private streamOptions: AgentHarnessStreamOptions; - private getApiKeyAndHeaders?: AgentHarnessOptions["getApiKeyAndHeaders"]; private resources: AgentHarnessResources; private tools = new Map(); private activeToolNames: string[]; @@ -199,7 +187,6 @@ export class AgentHarness< this.resources = options.resources ?? {}; this.streamOptions = cloneStreamOptions(options.streamOptions); this.systemPrompt = options.systemPrompt; - this.getApiKeyAndHeaders = options.getApiKeyAndHeaders; this.validateUniqueNames( (options.tools ?? []).map((tool) => tool.name), "Duplicate tool name(s)", @@ -372,11 +359,7 @@ export class AgentHarness< private createStreamFn(getTurnState: () => AgentHarnessTurnState): StreamFn { return async (model, context, streamOptions) => { const turnState = getTurnState(); - const auth = await this.getApiKeyAndHeaders?.(model); - const snapshotOptions: AgentHarnessStreamOptions = { - ...turnState.streamOptions, - headers: mergeHeaders(turnState.streamOptions.headers, auth?.headers), - }; + const snapshotOptions: AgentHarnessStreamOptions = { ...turnState.streamOptions }; const requestOptions = await this.emitBeforeProviderRequest(model, turnState.sessionId, snapshotOptions); return this.models.streamSimple(model, context, { cacheRetention: requestOptions.cacheRetention, @@ -397,7 +380,6 @@ export class AgentHarness< sessionId: turnState.sessionId, timeoutMs: requestOptions.timeoutMs, transport: requestOptions.transport, - apiKey: auth?.apiKey, }); }; } @@ -709,8 +691,6 @@ export class AgentHarness< try { const model = this.model; if (!model) throw new AgentHarnessError("invalid_state", "No model set for compaction"); - // Explicit auth wins; otherwise the request resolves through provider auth. - const auth = await this.getApiKeyAndHeaders?.(model); const branchEntries = await this.session.getBranch(); const preparationResult = prepareCompaction(branchEntries, DEFAULT_COMPACTION_SETTINGS); if (!preparationResult.ok) throw preparationResult.error; @@ -727,16 +707,7 @@ export class AgentHarness< const provided = hookResult?.compaction; const compactResult = provided ? { ok: true as const, value: provided } - : await compact( - preparation, - this.models, - model, - auth?.apiKey, - auth?.headers, - customInstructions, - undefined, - this.thinkingLevel, - ); + : await compact(preparation, this.models, model, customInstructions, undefined, this.thinkingLevel); if (!compactResult.ok) throw compactResult.error; const result = compactResult.value; const entryId = await this.session.appendCompaction( @@ -789,13 +760,9 @@ export class AgentHarness< if (!summaryText && options?.summarize && entries.length > 0) { const model = this.model; if (!model) throw new AgentHarnessError("invalid_state", "No model set for branch summary"); - // Explicit auth wins; otherwise the request resolves through provider auth. - const auth = await this.getApiKeyAndHeaders?.(model); const branchSummary = await generateBranchSummary(entries, { models: this.models, model, - apiKey: auth?.apiKey, - headers: auth?.headers, signal: new AbortController().signal, customInstructions: hookResult?.customInstructions ?? options?.customInstructions, replaceInstructions: hookResult?.replaceInstructions ?? options?.replaceInstructions, diff --git a/packages/agent/src/harness/compaction/branch-summarization.ts b/packages/agent/src/harness/compaction/branch-summarization.ts index 1df47d7a..fdf1df49 100644 --- a/packages/agent/src/harness/compaction/branch-summarization.ts +++ b/packages/agent/src/harness/compaction/branch-summarization.ts @@ -49,14 +49,10 @@ export interface CollectEntriesResult { /** Options for generating a branch summary. */ export interface GenerateBranchSummaryOptions { - /** Provider collection the summarization request goes through. */ + /** Provider collection the summarization request goes through; owns auth resolution. */ models: Models; /** Model used for summarization. */ model: Model; - /** Explicit API key; wins over provider-resolved auth. */ - apiKey?: string; - /** Optional request headers forwarded to the provider. */ - headers?: Record; /** Abort signal for the summarization request. */ signal: AbortSignal; /** Optional instructions appended to or replacing the default prompt. */ @@ -204,16 +200,7 @@ export async function generateBranchSummary( entries: SessionTreeEntry[], options: GenerateBranchSummaryOptions, ): Promise> { - const { - models, - model, - apiKey, - headers, - signal, - customInstructions, - replaceInstructions, - reserveTokens = 16384, - } = options; + const { models, model, signal, customInstructions, replaceInstructions, reserveTokens = 16384 } = options; const contextWindow = model.contextWindow || 128000; const tokenBudget = contextWindow - reserveTokens; @@ -244,7 +231,7 @@ export async function generateBranchSummary( const response = await models.completeSimple( model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, - { apiKey, headers, signal, maxTokens: 2048 }, + { signal, maxTokens: 2048 }, ); if (response.stopReason === "aborted") { return err(new BranchSummaryError("aborted", response.errorMessage || "Branch summary aborted")); diff --git a/packages/agent/src/harness/compaction/compaction.ts b/packages/agent/src/harness/compaction/compaction.ts index 55ea5b98..d6874c33 100644 --- a/packages/agent/src/harness/compaction/compaction.ts +++ b/packages/agent/src/harness/compaction/compaction.ts @@ -457,8 +457,6 @@ export async function generateSummary( models: Models, model: Model, reserveTokens: number, - apiKey?: string, - headers?: Record, signal?: AbortSignal, customInstructions?: string, previousSummary?: string, @@ -490,8 +488,8 @@ export async function generateSummary( const completionOptions = model.reasoning && thinkingLevel && thinkingLevel !== "off" - ? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel } - : { maxTokens, signal, apiKey, headers }; + ? { maxTokens, signal, reasoning: thinkingLevel } + : { maxTokens, signal }; const response = await models.completeSimple( model, @@ -628,8 +626,6 @@ export async function compact( preparation: CompactionPreparation, models: Models, model: Model, - apiKey?: string, - headers?: Record, customInstructions?: string, signal?: AbortSignal, thinkingLevel?: ThinkingLevel, @@ -659,24 +655,13 @@ export async function compact( models, model, settings.reserveTokens, - apiKey, - headers, signal, customInstructions, previousSummary, thinkingLevel, ) : Promise.resolve(ok("No prior history.")), - generateTurnPrefixSummary( - turnPrefixMessages, - models, - model, - settings.reserveTokens, - apiKey, - headers, - signal, - thinkingLevel, - ), + generateTurnPrefixSummary(turnPrefixMessages, models, model, settings.reserveTokens, signal, thinkingLevel), ]); if (!historyResult.ok) return err(historyResult.error); if (!turnPrefixResult.ok) return err(turnPrefixResult.error); @@ -687,8 +672,6 @@ export async function compact( models, model, settings.reserveTokens, - apiKey, - headers, signal, customInstructions, previousSummary, @@ -713,8 +696,6 @@ async function generateTurnPrefixSummary( models: Models, model: Model, reserveTokens: number, - apiKey?: string, - headers?: Record, signal?: AbortSignal, thinkingLevel?: ThinkingLevel, ): Promise> { @@ -737,8 +718,8 @@ async function generateTurnPrefixSummary( model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, model.reasoning && thinkingLevel && thinkingLevel !== "off" - ? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel } - : { maxTokens, signal, apiKey, headers }, + ? { maxTokens, signal, reasoning: thinkingLevel } + : { maxTokens, signal }, ); if (response.stopReason === "aborted") { return err(new CompactionError("aborted", response.errorMessage || "Turn prefix summarization aborted")); diff --git a/packages/agent/src/harness/types.ts b/packages/agent/src/harness/types.ts index 0e0aeaaf..f7bdf6dd 100644 --- a/packages/agent/src/harness/types.ts +++ b/packages/agent/src/harness/types.ts @@ -805,7 +805,7 @@ export interface AgentHarnessOptions< /** * Provider collection used for all model requests (turn streaming, * compaction, branch summarization). Auth resolves through the providers' - * auth; explicit per-request values (`getApiKeyAndHeaders`) win per field. + * auth. */ models: Models; tools?: TTool[]; @@ -824,9 +824,6 @@ export interface AgentHarnessOptions< activeTools: TTool[]; resources: AgentHarnessResources; }) => string | Promise); - getApiKeyAndHeaders?: ( - model: Model, - ) => Promise<{ apiKey: string; headers?: Record } | undefined>; /** Curated stream/provider request options. Snapshotted at turn start. */ streamOptions?: AgentHarnessStreamOptions; model: Model; diff --git a/packages/agent/test/harness/agent-harness-stream.test.ts b/packages/agent/test/harness/agent-harness-stream.test.ts index b4ed38dc..f5a4021d 100644 --- a/packages/agent/test/harness/agent-harness-stream.test.ts +++ b/packages/agent/test/harness/agent-harness-stream.test.ts @@ -36,7 +36,7 @@ function captureOptions(options: StreamOptions | undefined): StreamOptions { } describe("AgentHarness stream configuration", () => { - it("snapshots stream options and merges auth headers before provider request hooks", async () => { + it("snapshots stream options before provider request hooks", async () => { let capturedOptions: StreamOptions | undefined; const registration = newFaux(); registration.setResponses([ @@ -60,12 +60,11 @@ describe("AgentHarness stream configuration", () => { metadata: { base: true }, cacheRetention: "none", }, - getApiKeyAndHeaders: async () => ({ apiKey: "secret", headers: { "x-auth": "auth" } }), }); harness.on("before_provider_request", (event) => { expect(event.sessionId).toBe("session-1"); - expect(event.streamOptions.headers).toEqual({ "x-base": "base", "x-auth": "auth" }); + expect(event.streamOptions.headers).toEqual({ "x-base": "base" }); return { streamOptions: { headers: { "x-hook": "hook" }, @@ -77,14 +76,13 @@ describe("AgentHarness stream configuration", () => { await harness.prompt("hello"); expect(capturedOptions).toMatchObject({ - apiKey: "secret", timeoutMs: 1000, maxRetries: 2, maxRetryDelayMs: 3000, sessionId: "session-1", cacheRetention: "none", }); - expect(capturedOptions?.headers).toEqual({ "x-base": "base", "x-auth": "auth", "x-hook": "hook" }); + expect(capturedOptions?.headers).toEqual({ "x-base": "base", "x-hook": "hook" }); expect(capturedOptions?.metadata).toEqual({ base: true, hook: true }); }); diff --git a/packages/agent/test/harness/compaction.test.ts b/packages/agent/test/harness/compaction.test.ts index 540cb332..95148c3b 100644 --- a/packages/agent/test/harness/compaction.test.ts +++ b/packages/agent/test/harness/compaction.test.ts @@ -440,20 +440,9 @@ describe("harness compaction", () => { }, ]); getOrThrow( - await generateSummary( - messages, - models, - reasoningModel, - 2000, - "test-key", - undefined, - undefined, - undefined, - undefined, - "medium", - ), + await generateSummary(messages, models, reasoningModel, 2000, undefined, undefined, undefined, "medium"), ); - expect(seenOptions[0]).toMatchObject({ reasoning: "medium", apiKey: "test-key" }); + expect(seenOptions[0]).toMatchObject({ reasoning: "medium" }); const { faux: fauxOff, model: offModel } = createFauxModel(true); fauxOff.setResponses([ @@ -462,20 +451,7 @@ describe("harness compaction", () => { return fauxAssistantMessage("## Goal\nTest summary"); }, ]); - getOrThrow( - await generateSummary( - messages, - models, - offModel, - 2000, - "test-key", - undefined, - undefined, - undefined, - undefined, - "off", - ), - ); + getOrThrow(await generateSummary(messages, models, offModel, 2000, undefined, undefined, undefined, "off")); expect(seenOptions[1]).not.toHaveProperty("reasoning"); const { faux: fauxNonReasoning, model: nonReasoningModel } = createFauxModel(false); @@ -486,18 +462,7 @@ describe("harness compaction", () => { }, ]); getOrThrow( - await generateSummary( - messages, - models, - nonReasoningModel, - 2000, - "test-key", - undefined, - undefined, - undefined, - undefined, - "medium", - ), + await generateSummary(messages, models, nonReasoningModel, 2000, undefined, undefined, undefined, "medium"), ); expect(seenOptions[2]).not.toHaveProperty("reasoning"); }); @@ -516,17 +481,7 @@ describe("harness compaction", () => { ]); const summary = getOrThrow( - await generateSummary( - messages, - models, - model, - 2000, - "test-key", - { "x-test": "yes" }, - undefined, - "focus", - "old summary", - ), + await generateSummary(messages, models, model, 2000, undefined, "focus", "old summary"), ); expect(summary).toContain("Test summary"); @@ -538,7 +493,7 @@ describe("harness compaction", () => { const messages: AgentMessage[] = [createUserMessage("Summarize this.")]; const { faux: errorFaux, model: errorModel } = createFauxModel(false); errorFaux.setResponses([fauxAssistantMessage("", { stopReason: "error", errorMessage: "boom" })]); - const errorResult = await generateSummary(messages, models, errorModel, 2000, "test-key"); + const errorResult = await generateSummary(messages, models, errorModel, 2000); expect(errorResult).toMatchObject({ ok: false, error: { code: "summarization_failed", message: "Summarization failed: boom" }, @@ -546,7 +501,7 @@ describe("harness compaction", () => { const { faux: abortedFaux, model: abortedModel } = createFauxModel(false); abortedFaux.setResponses([fauxAssistantMessage("", { stopReason: "aborted", errorMessage: "stopped" })]); - const abortedResult = await generateSummary(messages, models, abortedModel, 2000, "test-key"); + const abortedResult = await generateSummary(messages, models, abortedModel, 2000); expect(abortedResult).toMatchObject({ ok: false, error: { code: "aborted", message: "stopped" } }); }); @@ -574,7 +529,7 @@ describe("harness compaction", () => { settings: { enabled: true, reserveTokens: 500000, keepRecentTokens: 20000 }, }; - getOrThrow(await compact(preparation, models, model, "test-key")); + getOrThrow(await compact(preparation, models, model)); expect(seenOptions.map((options) => options?.maxTokens)).toEqual([128000, 128000]); }); @@ -592,7 +547,7 @@ describe("harness compaction", () => { }; const { faux: historyFaux, model: historyModel } = createFauxModel(false); historyFaux.setResponses([fauxAssistantMessage("", { stopReason: "error", errorMessage: "history failed" })]); - expect(await compact(preparation, models, historyModel, "test-key")).toMatchObject({ + expect(await compact(preparation, models, historyModel)).toMatchObject({ ok: false, error: { code: "summarization_failed", message: "Summarization failed: history failed" }, }); @@ -602,7 +557,6 @@ describe("harness compaction", () => { { ...preparation, messagesToSummarize: [], firstKeptEntryId: "" }, models, invalidModel, - "test-key", ); expect(invalidResult).toMatchObject({ ok: false, error: { code: "invalid_session" } }); }); @@ -627,7 +581,7 @@ describe("harness compaction", () => { settings: { enabled: true, reserveTokens: 2000, keepRecentTokens: 20 }, }; - getOrThrow(await compact(preparation, models, model, "test-key", undefined, undefined, undefined, "high")); + getOrThrow(await compact(preparation, models, model, undefined, undefined, "high")); expect(seenOptions[0]).toMatchObject({ reasoning: "high" }); }); @@ -646,14 +600,14 @@ describe("harness compaction", () => { const { faux, model } = createFauxModel(false); faux.setResponses([fauxAssistantMessage("", { stopReason: "error", errorMessage: "prefix failed" })]); - expect(await compact(preparation, models, model, "test-key")).toMatchObject({ + expect(await compact(preparation, models, model)).toMatchObject({ ok: false, error: { code: "summarization_failed", message: "Turn prefix summarization failed: prefix failed" }, }); const { faux: abortedFaux, model: abortedModel } = createFauxModel(false); abortedFaux.setResponses([fauxAssistantMessage("", { stopReason: "aborted", errorMessage: "prefix stopped" })]); - expect(await compact(preparation, models, abortedModel, "test-key")).toMatchObject({ + expect(await compact(preparation, models, abortedModel)).toMatchObject({ ok: false, error: { code: "aborted", message: "prefix stopped" }, }); @@ -672,7 +626,7 @@ describe("harness compaction", () => { expect(preparation).toBeDefined(); const { faux, model } = createFauxModel(false); faux.setResponses([fauxAssistantMessage("## Goal\nTest summary")]); - const result = getOrThrow(await compact(preparation!, models, model, "test-key")); + const result = getOrThrow(await compact(preparation!, models, model)); expect(result.summary.length).toBeGreaterThan(0); expect(result.firstKeptEntryId).toBeTruthy(); expect(result.details).toBeDefined(); From 6e98573f24a09c858bbdfcf1171e39f37c2bc3d1 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 10 Jun 2026 23:30:04 +0200 Subject: [PATCH 015/122] feat(ai): sync model reads, explicit async refresh Provider.getModels() is sync-only (last-known list; must not throw) with an optional refreshModels() where dynamic providers fetch. The sync-or-async union invited latent sync assumptions that would detonate on the first dynamic provider; async-only reads would force sync consumer surfaces (extension find/getAll) through Promises. Sync reads plus an explicit refresh verb keeps the contract single and the staleness visible. Models.getModels()/getModel() are sync best-effort reads; Models.refresh(provider?) rejects with ModelsError(model_source) for a single provider and is concurrent best-effort across all providers. createProvider() takes a models array plus an optional refreshModels fetcher (stored on success, in-flight calls deduped, list unchanged on rejection). forceRefresh options are gone. Also finishes the in-progress AuthStorage fallbackResolver removal (drops the now-unused includeFallback option from getApiKey). --- packages/agent/docs/models.md | 98 ++++++++++----- packages/ai/CHANGELOG.md | 4 +- packages/ai/src/models.ts | 118 ++++++++++++------ packages/ai/test/lazy-module-load.test.ts | 2 +- packages/ai/test/models-runtime.test.ts | 87 +++++++------ packages/ai/test/oauth-auth.test.ts | 4 +- packages/ai/test/providers.test.ts | 35 ++++-- packages/ai/test/scratch.ts | 2 +- .../coding-agent/src/core/auth-storage.ts | 22 +--- .../coding-agent/src/core/model-registry.ts | 4 +- 10 files changed, 237 insertions(+), 139 deletions(-) diff --git a/packages/agent/docs/models.md b/packages/agent/docs/models.md index f5cb00ce..cfa524e0 100644 --- a/packages/agent/docs/models.md +++ b/packages/agent/docs/models.md @@ -10,7 +10,7 @@ Goals: - Concrete provider factories live under `src/providers/`. - Users can import only the providers they need. - Importing a provider must not eagerly import heavy SDKs. -- Dynamic model lists are first-class and side-effect-free. +- Dynamic model lists are first-class: reads are sync (last-known list), fetching happens in an explicit async `refresh`. - `models.json` and extensions layer by wrapping providers, not by mutating provider internals ad hoc. - Old global APIs survive only in an explicit, temporary `/compat` entrypoint. @@ -129,11 +129,17 @@ export interface Models { getProviders(): readonly Provider[]; getProvider(id: string): Provider | undefined; - /** Best-effort aggregation: provider source failures yield the models that did list. */ - getModels(options?: { forceRefresh?: boolean }): Promise[]>; - getModels(provider?: string, options?: { forceRefresh?: boolean }): Promise[]>; + /** Sync read of last-known models. Best-effort: a provider whose getModels() throws yields no models. */ + getModels(provider?: string): readonly Model[]; /** Dynamic lists are honestly Model; narrow with the hasApi() guard. */ - getModel(provider: string, id: string, options?: { forceRefresh?: boolean }): Promise | undefined>; + getModel(provider: string, id: string): Model | undefined; + + /** + * Ask dynamic providers to re-fetch their model lists. With a provider id, + * rejects on that provider's failure; without, refreshes all concurrently + * best-effort. Static providers are no-ops. + */ + refresh(provider?: string): Promise; /** * Resolve request auth for a model. Includes source label for status UI. @@ -201,8 +207,11 @@ export interface Provider { */ readonly auth: ProviderAuth; - /** Sync return suits static catalogs; Models always exposes a Promise. */ - getModels(options?: { forceRefresh?: boolean }): Promise[]> | readonly Model[]; + /** Current known models, sync. Static providers: the catalog. Dynamic providers: as of the last refresh (empty before the first). */ + getModels(): readonly Model[]; + + /** Dynamic providers only: fetch and update the model list. Concurrent calls share one in-flight fetch. */ + refreshModels?(): Promise; stream(model: Model, context: Context, options?: ApiStreamOptions): AssistantMessageEventStream; @@ -268,16 +277,20 @@ For comparison: Vercel AI SDK attaches the implementation to the model object, w ## Provider model listing -`Provider.getModels()` is async and returns full `Model` objects. Static providers wrap their catalog; dynamic providers (llama.cpp, OpenRouter live listing) fetch and cache, honoring `forceRefresh`. +Reads are sync; fetching is an explicit async verb. `Provider.getModels()` returns the current known list — the full catalog for static providers, the last-refreshed list for dynamic ones (llama.cpp, OpenRouter live listing). `refreshModels()` is where dynamic providers fetch. -Dynamic model listing must be side-effect-free discovery: +This split exists because a sync-or-async union (`Promise | T`) invites latent sync assumptions that detonate on the first async provider, while async-only reads force every consumer (UI lists, extension `find`/`getAll` surfaces) through Promises for data that is almost always static. Sync reads + explicit refresh keeps the staleness visible and the contract single: `getModels()` = last known, `refresh()` = make it current. A fetched list is stale the moment it returns anyway; naming the refresh point is honest about it. + +Apps own the refresh lifecycle: startup, registry reload, opening a model selector. Freshness-critical lookups are two-step: `await models.refresh("llamacpp"); models.getModel("llamacpp", id)`. + +Dynamic refresh must be side-effect-free discovery: ```txt OK: fetch /v1/models, enumerate local catalog, refresh cached remote model list Not OK: load model, download model, mutate server state, run request probe ``` -Provider-specific model lifecycle (load/unload) belongs in app/provider-management commands, not in `getModels()`. +Provider-specific model lifecycle (load/unload) belongs in app/provider-management commands, not in `refreshModels()`. ## Streaming path @@ -301,7 +314,7 @@ function stream(model, context, options) { `stream()` returns `AssistantMessageEventStream` synchronously; async setup (auth resolution, lazy module load) happens inside the returned stream. The forwarding pattern already exists in today's `register-builtins.ts` (`createLazyStream`); extract it as `lazyStream()` in `src/api/lazy.ts`. -No request hot-path model canonicalization: `stream()` uses the supplied model object as-is. If an app wants fresh model metadata, it calls `models.getModel(provider, id, { forceRefresh: true })` before starting the turn. +No request hot-path model canonicalization: `stream()` uses the supplied model object as-is. If an app wants fresh model metadata, it refreshes the provider and re-reads (`await models.refresh(p); models.getModel(p, id)`) before starting the turn. ## API implementations under `src/api` @@ -629,10 +642,8 @@ function withProviderOverrides(base: Provider, overrides: ProviderOverrides): Pr baseUrl: overrides.baseUrl ?? base.baseUrl, headers: mergeHeaders(base.headers, overrides.headers), - async getModels(options) { - const models = await base.getModels(options); - return applyModelOverrides(models, overrides.models); - }, + getModels: () => applyModelOverrides(base.getModels(), overrides.models), + refreshModels: base.refreshModels?.bind(base), stream: base.stream, streamSimple: base.streamSimple, @@ -640,7 +651,7 @@ function withProviderOverrides(base: Provider, overrides: ProviderOverrides): Pr } ``` -This composes with dynamic providers because `getModels()` delegates to the base source. +This composes with dynamic providers because `getModels()` delegates to the base source and `refreshModels()` passes through. Request-auth config from models.json (`$ENV`, `!command`, inline keys) remains app-owned sidecar state, surfaced either as explicit request auth or as a custom `ApiKeyAuth` the app sets on the wrapped provider's `auth.apiKey`. @@ -655,9 +666,10 @@ export function createProvider(input: { baseUrl?: string; headers?: Record; auth: ProviderAuth; // required, at least one of apiKey/oauth (no "no-auth" providers) - models: - | readonly Model[] - | ((options?: { forceRefresh?: boolean }) => Promise[]>); + /** Initial model list (empty for purely dynamic providers). */ + models: readonly Model[]; + /** Dynamic providers: fetch the current list; createProvider stores it and dedupes in-flight calls. */ + refreshModels?: () => Promise[]>; /** Single implementation, or map keyed by model.api for mixed-API providers. */ api: ProviderStreams | Record; }): Provider; @@ -837,17 +849,45 @@ Check items off as they land. Keep this list current; it is the working state fo - [x] `packages/agent/CHANGELOG.md`: `### Breaking Changes` for required `AgentHarnessOptions.models`, compaction signature changes, structural `StreamFn`. - [x] `npm run check` clean. -### Phase 9 — ModelManager migration (separate pass, not started) +### Phase 9 — coding-agent on Models + CredentialStore (in scope) -The big one: coding-agent moves off ModelRegistry/AuthStorage onto `Models` + `CredentialStore`, and compat dies. Ordering sketch: +coding-agent replaces AuthStorage and ModelRegistry's internals with `FileCredentialStore` + a `MutableModels` collection. AgentSession itself stays (AgentHarness adoption is pi 2.0); only its model/auth substrate swaps. Layering is strictly one-directional: -- [ ] coding-agent constructs a `Models` instance per session: builtins (`builtinModels()`), models.json custom providers via `createProvider()` + `withProviderOverrides`, extension custom providers as real providers (legacy `registerApiProvider` extensions bridged by a catch-all api-dispatch provider until extensions migrate). -- [ ] `AgentSession` streams through that instance (directly or by adopting `AgentHarness`); `agent.streamFn` identity checks and env-key injection die. -- [ ] AuthStorage replaced per "Replacing AuthStorage": `FileCredentialStore` (ports the lock backend), `withConfigValues` (`$ENV`/`!command`), `withRuntimeOverrides` (`--api-key`); custom providers carry their own `ApiKeyAuth` (kills `fallbackResolver`). -- [ ] Login/logout/status UIs move to `ProviderAuth` (`OAuthAuth.login` + `prompt()/notify()` adapter in the login dialog); the old `pi-ai/oauth` registry and `OAuthProviderInterface` (incl. `usesCallbackServer`) are deleted. -- [ ] Cloudflare cleanup (gated on builtin streaming going through `Models.getAuth`): cloudflare factories' `ApiKeyAuth.resolve` reads key + `CLOUDFLARE_ACCOUNT_ID` (+ `CLOUDFLARE_GATEWAY_ID`) from credential metadata/env, substitutes the `{...}` placeholders in `model.baseUrl`, and returns `ModelAuth.baseUrl` (Copilot pattern); unconfigured ids report "not configured". `resolveCloudflareBaseUrl`/`isCloudflareProvider` drop out of `api/anthropic-messages.ts`, `api/openai-completions.ts`, `api/openai-responses.ts`. +```txt +FileCredentialStore (auth.json, locked) + --api-key overlay + $ENV/!command resolution + ↑ +MutableModels: builtin factories (wrapped per models.json config) + custom providers (models.json ∪ extensions) + ↑ +ModelRegistry: async facade — reads delegate to the collection; registerProvider/login/logout/status for extensions + UI + ↑ +AgentSession / sdk / interactive-mode (await added; stream via models) +``` + +Decisions: + +- `AuthStorage` is deleted as a type — it would otherwise depend on provider auth while provider auth depends on its store (circular). Its surface splits: `get`/`set`/`remove` -> `CredentialStore`; `getApiKey` -> `Models.getAuth`; `login`/`logout`/`getAuthStatus` -> ModelRegistry facade methods over `provider.auth.oauth` + the store. +- Runtime `--api-key` overrides are a 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` become async, delegating to the collection (no materialized snapshot, no sync lies; dynamic providers like llama.cpp work). The extension-facing `modelRegistry` surface changes accordingly (breaking, changelogged); 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. The api-registry `registerApiProvider` dual-write stays for compat consumers (extensions calling global `complete()`); it dies 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 metadata/env -> `ModelAuth.baseUrl`); impl-side `resolveCloudflareBaseUrl` stays until compat dies (it is idempotent on substituted URLs), keeping extension `complete()` calls working. + +Ordering: + +- [ ] pi-ai rework first: `Provider.getModels()` sync + optional `refreshModels()`; `Models.getModels`/`getModel` sync, `Models.refresh(provider?)` async; `createProvider` takes `models` array + optional `refreshModels` fetcher (in-flight dedupe). Reverses Phase 1's async-listing decision — see "Provider model listing" for rationale (sync-or-async unions breed latent sync assumptions; async-only breaks sync consumer surfaces like extension `find`/`getAll`). +- [ ] `FileCredentialStore` (ports the auth.json lock backend, reads legacy `type: "api_key"` tags) + `--api-key` overlay + `$ENV`/`!command` resolution; tests. +- [ ] Cloudflare provider auth in pi-ai factories; copilot `getModels` baseUrl wrap. +- [ ] Extension-OAuth adapter (old `OAuthProviderInterface` config -> `OAuthAuth`). +- [ ] ModelRegistry rebuild: owns `MutableModels`, async reads, models.json decoration, provider-keyed extension streams, facade auth ops; AuthStorage deleted. +- [ ] Consumer rewiring: agent-session, sdk (`credentials?: CredentialStore` option replaces `authStorage`; sdk.md updated), model-resolver, interactive login/status UI on `prompt()/notify()`, cli `--api-key`. +- [ ] Test migration; tmux validation of login flows against real providers. + +### Phase 10 — compat deletion (pi 2.0 era, separate) + +- [ ] 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`, `api-registry.ts`, `env-api-keys.ts`, and the extension-loader root-to-compat alias. This is the extension-author breaking release; changelog carries the migration guide. +- [ ] Delete `/compat`, `api-registry.ts`, `env-api-keys.ts`, the extension-loader root-to-compat alias, the old `pi-ai/oauth` registry and `OAuthProviderInterface` (incl. `usesCallbackServer`), and the impl-side cloudflare substitution. This is the extension-author breaking release; changelog carries the migration guide. ### Deferred / follow-ups @@ -860,7 +900,7 @@ The big one: coding-agent moves off ModelRegistry/AuthStorage onto `Models` + `C ```ts export type ModelsErrorCode = - | "model_source" // provider getModels() failed + | "model_source" // provider model refresh failed | "model_validation" // model object invalid | "provider" // unknown provider, dispatch failure | "stream" // stream setup failure @@ -869,6 +909,6 @@ export type ModelsErrorCode = ``` - `Models.stream()` produces stream errors (error event + error result) for async setup failures; it does not throw after returning the stream. -- `Models.getModels()` is best-effort aggregation in all forms: provider source failures yield the models that did list (empty for a single failing provider). Apps that need the concrete failure call `getProvider(id).getModels()` directly. +- `Models.getModels()` is a sync best-effort read: a provider whose `getModels()` throws yields no models. `Models.refresh(provider)` rejects on that provider's fetch failure; `Models.refresh()` (all providers) is concurrent best-effort. Apps that need a concrete listing failure refresh the single provider. - Auth resolution and credential store failures reject loudly (`ModelsError` codes `auth`/`oauth`); silent fallback to a different auth path after a failure risks billing surprises. A stored credential always blocks ambient/env fallback, including after a failed refresh. - Status/availability UIs catch `getAuth` rejections and render "needs re-login"; they do not treat rejection as "unconfigured". diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 101fabf2..0c0d3bd6 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,13 +4,13 @@ ### Breaking Changes -- The root entrypoint (`@earendil-works/pi-ai`) is now core-only and side-effect free. The old global API moved to the temporary `@earendil-works/pi-ai/compat` entrypoint, a strict superset of the root: switching a file's import path is the only migration step. Moved symbols include `stream`/`complete`/`streamSimple`/`completeSimple`, `getModel`/`getModels`/`getProviders` (now deprecated aliases of `getBuiltinModel`/`getBuiltinModels`/`getBuiltinProviders` from `@earendil-works/pi-ai/providers/all`), `registerApiProvider`/`unregisterApiProviders`/`resetApiProviders`/`getApiProvider`, `getEnvApiKey`/`findEnvKeys`, `setBedrockProviderModule`, the per-API lazy stream wrappers (`anthropicMessagesApi`, ...), and the image-generation API. `/compat` will be removed with the coding-agent ModelManager migration; new code uses `createModels()` and the provider factories instead. +- The root entrypoint (`@earendil-works/pi-ai`) is now core-only and side-effect free. The old global API moved to the temporary `@earendil-works/pi-ai/compat` entrypoint, a strict superset of the root: switching a file's import path is the only migration step. Moved symbols include `stream`/`complete`/`streamSimple`/`completeSimple`, `getModel`/`getModels`/`getProviders` (now deprecated aliases of `getBuiltinModel`/`getBuiltinModels`/`getBuiltinProviders` from `@earendil-works/pi-ai/providers/all`), `registerApiProvider`/`unregisterApiProviders`/`resetApiProviders`/`getApiProvider`, `getEnvApiKey`/`findEnvKeys`, `setBedrockProviderModule`, the per-API lazy stream wrappers (`anthropicMessagesApi`, ...), and the image-generation API. - Renamed the `Provider` type to `ProviderId`. `Provider` now names the runtime provider interface (id, name, auth, model listing, stream behavior). - API implementation modules moved from `src/providers/` to `@earendil-works/pi-ai/api/*`, renamed by API id (`anthropic` -> `api/anthropic-messages`, `google` -> `api/google-generative-ai`, `mistral` -> `api/mistral-conversations`, `amazon-bedrock` -> `api/bedrock-converse-stream`), each exporting exactly `stream` and `streamSimple`. The old per-impl export names (`streamAnthropic`, `streamSimpleAnthropic`, ...) are gone; the legacy package subpaths (`./anthropic`, `./google`, ...) keep working and point at the new modules. ### Added -- New `Models` runtime: `createModels()` builds an isolated provider collection with async model listing, auth resolution (`getAuth`), and `stream`/`complete`/`streamSimple`/`completeSimple` that resolve auth through the owning provider. `createProvider()` builds providers from parts (single API implementation or a map dispatched on `model.api`); `hasApi()` narrows dynamically listed models. +- New `Models` runtime: `createModels()` builds an isolated provider collection with sync model reads (`getModels`/`getModel` return the last-known lists), an explicit async `refresh(provider?)` for dynamic providers, auth resolution (`getAuth`), and `stream`/`complete`/`streamSimple`/`completeSimple` that resolve auth through the owning provider. `createProvider()` builds providers from parts (single API implementation or a map dispatched on `model.api`; static `models` array plus an optional `refreshModels` fetcher with in-flight dedupe); `hasApi()` narrows dynamically listed models. - Provider auth substrate: `ProviderAuth` (`{ apiKey?, oauth? }`), one type-tagged credential per provider, `CredentialStore` (`read`/`modify`/`delete` with serialized writes; in-memory default), `envApiKeyAuth()`, `lazyOAuth()`, and injectable `AuthContext`. OAuth refresh runs under the store lock with double-checked expiry; a stored credential owns its provider (no silent env fallback). - One provider factory per built-in provider under `@earendil-works/pi-ai/providers/*` (e.g. `anthropicProvider()`, `openrouterProvider()`), plus `@earendil-works/pi-ai/providers/all` with `builtinProviders()`/`builtinModels()` and typed `getBuiltin*` catalog reads. Generated catalogs are split per provider, so importing one provider pulls one catalog; `sideEffects` metadata makes the package tree-shakeable. - OAuth flows (Anthropic, OpenAI Codex, GitHub Copilot) gained `OAuthAuth` adapters (`login`/`refresh`/`toAuth`) on unified `prompt()`/`notify()` login callbacks; Copilot's per-credential base URL is derived in `toAuth()`. diff --git a/packages/ai/src/models.ts b/packages/ai/src/models.ts index 6cb7a87d..37d33841 100644 --- a/packages/ai/src/models.ts +++ b/packages/ai/src/models.ts @@ -64,10 +64,21 @@ export interface Provider { readonly auth: ProviderAuth; /** - * List models. Async and side-effect-free discovery only; provider-specific - * model lifecycle (load/unload) belongs in app commands. + * Current known models, sync. Static providers return their catalog; + * dynamic providers return the list as of the last `refreshModels()` + * (empty before the first). Must not throw; `Models` treats a throwing + * implementation as having no models. */ - getModels(options?: { forceRefresh?: boolean }): Promise[]> | readonly Model[]; + getModels(): readonly Model[]; + + /** + * Dynamic providers only: fetch and update the model list. Side-effect-free + * discovery (no loading/downloading); provider-specific model lifecycle + * belongs in app commands. Concurrent calls share one in-flight fetch. + * May reject (network); on rejection the model list stays at its last-known + * state and a later call retries. + */ + refreshModels?(): Promise; stream( model: Model, @@ -88,19 +99,24 @@ export interface Models { getProvider(id: string): Provider | undefined; /** - * List models from one provider or all providers. Best-effort aggregation: - * provider source failures yield the models that did list (empty for a - * single failing provider). Apps that need the failure call - * `getProvider(id).getModels()` directly. + * Sync read of last-known models from one provider or all providers. + * Best-effort: a provider whose `getModels()` throws yields no models. */ - getModels(options?: { forceRefresh?: boolean }): Promise[]>; - getModels(provider?: string, options?: { forceRefresh?: boolean }): Promise[]>; + getModels(provider?: string): readonly Model[]; /** - * Runtime model lookup. Dynamic model lists are typed as `Model`; - * narrow with the `hasApi()` type guard. + * Sync runtime model lookup against last-known lists. Dynamic model lists + * are typed as `Model`; narrow with the `hasApi()` type guard. */ - getModel(provider: string, id: string, options?: { forceRefresh?: boolean }): Promise | undefined>; + getModel(provider: string, id: string): Model | undefined; + + /** + * Ask dynamic providers to re-fetch their model lists. With a provider id, + * rejects with `ModelsError` ("model_source") on that provider's fetch + * failure; without one, refreshes all providers concurrently best-effort. + * Static providers (no `refreshModels`) are no-ops. + */ + refresh(provider?: string): Promise; /** * Resolve request auth for a model. Includes a source label for status UI. @@ -171,37 +187,48 @@ class ModelsImpl implements MutableModels { return this.providers.get(id); } - async getModels( - providerOrOptions?: string | { forceRefresh?: boolean }, - maybeOptions?: { forceRefresh?: boolean }, - ): Promise[]> { - const provider = typeof providerOrOptions === "string" ? providerOrOptions : undefined; - const options = typeof providerOrOptions === "string" ? maybeOptions : providerOrOptions; - + getModels(provider?: string): readonly Model[] { if (provider !== undefined) { const entry = this.providers.get(provider); if (!entry) return []; try { - return await entry.getModels(options); + return entry.getModels(); } catch { return []; } } - // Async wrapper turns sync throws from ill-behaved providers into rejections. - const results = await Promise.allSettled( - Array.from(this.providers.values(), async (entry) => entry.getModels(options)), - ); const models: Model[] = []; - for (const result of results) { - if (result.status === "fulfilled") models.push(...result.value); + for (const entry of this.providers.values()) { + try { + models.push(...entry.getModels()); + } catch { + // Best-effort: ill-behaved providers yield no models. + } } return models; } - async getModel(provider: string, id: string, options?: { forceRefresh?: boolean }): Promise | undefined> { - const models = await this.getModels(provider, options); - return models.find((model) => model.id === id); + getModel(provider: string, id: string): Model | undefined { + return this.getModels(provider).find((model) => model.id === id); + } + + async refresh(provider?: string): Promise { + if (provider !== undefined) { + const entry = this.providers.get(provider); + if (!entry?.refreshModels) return; + try { + await entry.refreshModels(); + } catch (error) { + if (error instanceof ModelsError) throw error; + throw new ModelsError("model_source", `Model refresh failed for ${provider}`, { cause: error }); + } + return; + } + + // Cannot reject: the async mapper turns even sync throws from ill-behaved + // providers into rejections, and allSettled captures all of them. + await Promise.allSettled(Array.from(this.providers.values(), async (entry) => entry.refreshModels?.())); } async getAuth(model: Model): Promise { @@ -358,9 +385,16 @@ export interface CreateProviderOptions { headers?: Record; /** Required — every provider has auth semantics, even ambient/keyless ones. */ auth: ProviderAuth; - models: - | readonly Model[] - | ((options?: { forceRefresh?: boolean }) => Promise[]> | readonly Model[]); + /** Initial model list (empty for purely dynamic providers). */ + models: readonly Model[]; + /** + * Dynamic providers: fetch the current list. Stored on success; concurrent + * calls share one in-flight fetch. May reject: the stored list then stays + * at its last-known state, the rejection propagates to the caller of + * `refreshModels()` (wrapped as ModelsError "model_source" by + * `Models.refresh(provider)`), and a later call retries. + */ + refreshModels?: () => Promise[]>; /** Single implementation, or map keyed by `model.api` for mixed-API providers. */ api: ProviderStreams | Partial>; } @@ -372,7 +406,9 @@ export interface CreateProviderOptions { * produces a stream error. */ export function createProvider(input: CreateProviderOptions): Provider { - const { models } = input; + let models = input.models; + let inflightRefresh: Promise | undefined; + const refreshModels = input.refreshModels; const single = typeof (input.api as ProviderStreams).stream === "function" ? (input.api as ProviderStreams) : undefined; const byApi = single ? undefined : (input.api as Partial>); @@ -398,7 +434,19 @@ export function createProvider(input: CreateProviderOpti baseUrl: input.baseUrl, headers: input.headers, auth: input.auth, - getModels: typeof models === "function" ? (options) => models(options) : () => models, + getModels: () => models, + refreshModels: refreshModels + ? () => { + inflightRefresh ??= (async () => { + try { + models = await refreshModels(); + } finally { + inflightRefresh = undefined; + } + })(); + return inflightRefresh; + } + : undefined, stream: (model, context, options) => dispatch(model, (streams) => streams.stream(model, context, options)), streamSimple: (model, context, options) => dispatch(model, (streams) => streams.streamSimple(model, context, options)), @@ -409,7 +457,7 @@ export function createProvider(input: CreateProviderOpti * Runtime-checked narrowing for dynamically looked-up models: * * ```ts - * const model = await models.getModel("anthropic", "claude-opus-4-7"); + * const model = models.getModel("anthropic", "claude-opus-4-7"); * if (model && hasApi(model, "anthropic-messages")) { * // model: Model<"anthropic-messages">, stream options fully typed * } diff --git a/packages/ai/test/lazy-module-load.test.ts b/packages/ai/test/lazy-module-load.test.ts index 6f5464ff..dd516962 100644 --- a/packages/ai/test/lazy-module-load.test.ts +++ b/packages/ai/test/lazy-module-load.test.ts @@ -72,7 +72,7 @@ describe("lazy provider module loading", () => { const result = runProbe(` const all = await import(${JSON.stringify(providersAllUrl)}); const models = all.builtinModels(); - await models.getModels(); + models.getModels(); `); expect(result.loadedSpecifiers).toEqual([]); }); diff --git a/packages/ai/test/models-runtime.test.ts b/packages/ai/test/models-runtime.test.ts index 7f94ccb7..cafcb17f 100644 --- a/packages/ai/test/models-runtime.test.ts +++ b/packages/ai/test/models-runtime.test.ts @@ -55,7 +55,8 @@ function testProvider(input: { id: string; models?: Model[]; auth?: ProviderAuth; - getModels?: () => Promise[]>; + getModels?: () => readonly Model[]; + refreshModels?: () => Promise; calls?: ProviderCall[]; }): Provider { const models = input.models ?? [testModel(input.id, "model-a")]; @@ -72,7 +73,8 @@ function testProvider(input: { id: input.id, name: input.id, auth: input.auth ?? { apiKey: ambientAuth }, - getModels: input.getModels ?? (async () => models), + getModels: input.getModels ?? (() => models), + refreshModels: input.refreshModels, stream: (model, _context, options) => respond(model, options as StreamOptions | undefined), streamSimple: (model, _context, options) => respond(model, options as SimpleStreamOptions | undefined), }; @@ -127,14 +129,14 @@ describe("Models runtime", () => { models.setProvider(testProvider({ id: "p1", models: [testModel("p1", "m1"), testModel("p1", "m2")] })); models.setProvider(testProvider({ id: "p2", models: [testModel("p2", "m3")] })); - expect((await models.getModels()).map((m) => m.id)).toEqual(["m1", "m2", "m3"]); - expect((await models.getModels("p1")).map((m) => m.id)).toEqual(["m1", "m2"]); - expect((await models.getModels("nope")).length).toBe(0); - expect((await models.getModel("p2", "m3"))?.id).toBe("m3"); - expect(await models.getModel("p2", "missing")).toBeUndefined(); + expect(models.getModels().map((m) => m.id)).toEqual(["m1", "m2", "m3"]); + expect(models.getModels("p1").map((m) => m.id)).toEqual(["m1", "m2"]); + expect(models.getModels("nope").length).toBe(0); + expect(models.getModel("p2", "m3")?.id).toBe("m3"); + expect(models.getModel("p2", "missing")).toBeUndefined(); // hasApi() narrows dynamically looked-up models with a runtime check - const found = await models.getModel("p2", "m3"); + const found = models.getModel("p2", "m3"); expect(found && hasApi(found, "openai-completions")).toBe(false); expect(found && hasApi(found, "test-api")).toBe(true); if (found && hasApi(found, "test-api")) { @@ -143,48 +145,63 @@ describe("Models runtime", () => { } }); - it("swallows provider source failures for both all-provider and single-provider listing", async () => { + it("swallows provider source failures for both all-provider and single-provider listing", () => { const models = createModels(); models.setProvider( testProvider({ id: "broken", - getModels: async () => { + getModels: () => { throw new Error("boom"); }, }), ); models.setProvider(testProvider({ id: "ok", models: [testModel("ok", "m1")] })); - expect((await models.getModels()).map((m) => m.id)).toEqual(["m1"]); - expect(await models.getModels("broken")).toEqual([]); + expect(models.getModels().map((m) => m.id)).toEqual(["m1"]); + expect(models.getModels("broken")).toEqual([]); // precise failures come from the provider directly - await expect(models.getProvider("broken")?.getModels()).rejects.toThrow("boom"); - - // even sync-throwing (non-async) provider implementations are isolated - models.setProvider({ - ...testProvider({ id: "sync-broken" }), - getModels: () => { - throw new Error("sync boom"); - }, - }); - expect((await models.getModels()).map((m) => m.id)).toEqual(["m1"]); + expect(() => models.getProvider("broken")?.getModels()).toThrow("boom"); }); - it("supports getModels(options) without a provider id", async () => { - const seen: ({ forceRefresh?: boolean } | undefined)[] = []; + it("refresh() updates dynamic providers; single-provider refresh failures reject", async () => { + let list = [testModel("dyn", "before")]; + let refreshes = 0; const models = createModels(); - models.setProvider(testProvider({ id: "p1", models: [testModel("p1", "m1")] })); - models.setProvider({ - ...testProvider({ id: "p2" }), - getModels: async (options) => { - seen.push(options); - return [testModel("p2", "m2")]; - }, - }); + models.setProvider( + testProvider({ + id: "dyn", + getModels: () => list, + refreshModels: async () => { + refreshes++; + list = [testModel("dyn", "after")]; + }, + }), + ); + models.setProvider(testProvider({ id: "static", models: [testModel("static", "s1")] })); - const all = await models.getModels({ forceRefresh: true }); - expect(all.map((m) => m.id)).toEqual(["m1", "m2"]); - expect(seen).toEqual([{ forceRefresh: true }]); + expect(models.getModel("dyn", "before")).toBeDefined(); + await models.refresh("dyn"); + expect(refreshes).toBe(1); + expect(models.getModel("dyn", "after")).toBeDefined(); + expect(models.getModel("dyn", "before")).toBeUndefined(); + + // static providers are no-ops; refresh-all is best-effort + await models.refresh("static"); + await models.refresh(); + expect(refreshes).toBe(2); + + // single-provider refresh failures reject with ModelsError + models.setProvider( + testProvider({ + id: "flaky", + refreshModels: async () => { + throw new Error("fetch failed"); + }, + }), + ); + await expect(models.refresh("flaky")).rejects.toMatchObject({ code: "model_source" }); + // refresh-all swallows the same failure + await expect(models.refresh()).resolves.toBeUndefined(); }); it("resolves auth: stored credential owns the provider, ambient only when nothing stored", async () => { diff --git a/packages/ai/test/oauth-auth.test.ts b/packages/ai/test/oauth-auth.test.ts index 579fdb37..c008b18c 100644 --- a/packages/ai/test/oauth-auth.test.ts +++ b/packages/ai/test/oauth-auth.test.ts @@ -99,7 +99,7 @@ describe("OAuth through Models.getAuth (lazy load chain)", () => { const models = createModels({ credentials }); models.setProvider(anthropicProvider()); - const model = (await models.getModels("anthropic"))[0]; + const model = models.getModels("anthropic")[0]; const result = await models.getAuth(model); expect(result?.auth.apiKey).toBe("oauth-access-token"); expect(result?.source).toBe("OAuth"); @@ -117,7 +117,7 @@ describe("OAuth through Models.getAuth (lazy load chain)", () => { const models = createModels({ credentials }); models.setProvider(githubCopilotProvider()); - const model = (await models.getModels("github-copilot"))[0]; + const model = models.getModels("github-copilot")[0]; const result = await models.getAuth(model); expect(result?.auth.apiKey).toBe(access); expect(result?.auth.baseUrl).toBe("https://api.business.githubcopilot.com"); diff --git a/packages/ai/test/providers.test.ts b/packages/ai/test/providers.test.ts index 2b41ba1c..3e6320a9 100644 --- a/packages/ai/test/providers.test.ts +++ b/packages/ai/test/providers.test.ts @@ -26,15 +26,15 @@ describe("builtin providers", () => { expect(providers.length).toBe(builtinProviders().length); expect(providers.map((p) => p.id)).toContain("anthropic"); - const anthropic = await models.getModel("anthropic", "claude-haiku-4-5"); + const anthropic = models.getModel("anthropic", "claude-haiku-4-5"); expect(anthropic?.api).toBe("anthropic-messages"); - const all = await models.getModels(); + const all = models.getModels(); expect(all.length).toBeGreaterThan(500); // every provider lists at least one model and owns its models for (const provider of providers) { - const list = await models.getModels(provider.id); + const list = models.getModels(provider.id); expect(list.length).toBeGreaterThan(0); expect(list.every((m) => m.provider === provider.id)).toBe(true); } @@ -45,7 +45,7 @@ describe("builtin providers", () => { authContext: fakeAuthContext({ ANTHROPIC_API_KEY: "key", ANTHROPIC_OAUTH_TOKEN: "oauth-token" }), }); models.setProvider(anthropicProvider()); - const model = (await models.getModel("anthropic", "claude-haiku-4-5"))!; + const model = models.getModel("anthropic", "claude-haiku-4-5")!; const result = await models.getAuth(model); expect(result?.auth.apiKey).toBe("oauth-token"); @@ -55,7 +55,7 @@ describe("builtin providers", () => { it("reports bedrock as configured from ambient AWS credentials without an api key", async () => { const models = createModels({ authContext: fakeAuthContext({ AWS_PROFILE: "dev" }) }); models.setProvider(amazonBedrockProvider()); - const model = (await models.getModels("amazon-bedrock"))[0]; + const model = models.getModels("amazon-bedrock")[0]; const result = await models.getAuth(model); expect(result?.auth).toEqual({}); @@ -72,7 +72,7 @@ describe("builtin providers", () => { authContext: fakeAuthContext({ GOOGLE_CLOUD_PROJECT: "proj", GOOGLE_CLOUD_LOCATION: "us-central1" }, [adc]), }); configured.setProvider(googleVertexProvider()); - const model = (await configured.getModels("google-vertex"))[0]; + const model = configured.getModels("google-vertex")[0]; const result = await configured.getAuth(model); expect(result?.auth).toEqual({}); @@ -180,15 +180,28 @@ describe("createProvider", () => { expect(result.errorMessage).toContain("no API implementation"); }); - it("supports async model listers", async () => { + it("supports dynamic providers: empty until refreshed, in-flight refreshes deduped", async () => { + let fetches = 0; const provider = createProvider({ id: "dynamic", auth: { apiKey: { name: "Test", resolve: async () => ({ auth: {} }) } }, - models: async () => [testModel("api-a", "listed")], + models: [], + refreshModels: async () => { + fetches++; + await new Promise((resolve) => setTimeout(resolve, 5)); + return [testModel("api-a", "listed")]; + }, api: recordingStreams("a", []), }); - const models = await provider.getModels(); - expect(models.map((m) => m.id)).toEqual(["listed"]); + + expect(provider.getModels()).toEqual([]); + await Promise.all([provider.refreshModels?.(), provider.refreshModels?.()]); + expect(fetches).toBe(1); + expect(provider.getModels().map((m) => m.id)).toEqual(["listed"]); + + // a later refresh fetches again + await provider.refreshModels?.(); + expect(fetches).toBe(2); }); }); @@ -199,7 +212,7 @@ describe("fauxProvider", () => { models.setProvider(faux.provider); faux.setResponses([fauxAssistantMessage("hello from faux")]); - const model = (await models.getModels(faux.provider.id))[0]; + const model = models.getModels(faux.provider.id)[0]; const result = await models.completeSimple(model, context); expect(result.stopReason).toBe("stop"); expect(result.content).toEqual([{ type: "text", text: "hello from faux" }]); diff --git a/packages/ai/test/scratch.ts b/packages/ai/test/scratch.ts index 5e470cbc..c2d83649 100644 --- a/packages/ai/test/scratch.ts +++ b/packages/ai/test/scratch.ts @@ -18,7 +18,7 @@ models.setProvider(anthropicProvider()); // 2. Look up a model and check auth. // --------------------------------------------------------------------------- -const model = await models.getModel("anthropic", "claude-haiku-4-5"); +const model = models.getModel("anthropic", "claude-haiku-4-5"); if (!model) throw new Error("model not found"); const auth = await models.getAuth(model); diff --git a/packages/coding-agent/src/core/auth-storage.ts b/packages/coding-agent/src/core/auth-storage.ts index 6b9f2f3a..b30d7832 100644 --- a/packages/coding-agent/src/core/auth-storage.ts +++ b/packages/coding-agent/src/core/auth-storage.ts @@ -198,7 +198,6 @@ export class InMemoryAuthStorageBackend implements AuthStorageBackend { export class AuthStorage { private data: AuthStorageData = {}; private runtimeOverrides: Map = new Map(); - private fallbackResolver?: (provider: string) => string | undefined; private loadError: Error | null = null; private errors: Error[] = []; private storage: AuthStorageBackend; @@ -237,14 +236,6 @@ export class AuthStorage { this.runtimeOverrides.delete(provider); } - /** - * Set a fallback resolver for API keys not found in auth.json or env vars. - * Used for custom provider keys from models.json. - */ - setFallbackResolver(resolver: (provider: string) => string | undefined): void { - this.fallbackResolver = resolver; - } - private recordError(error: unknown): void { const normalizedError = error instanceof Error ? error : new Error(String(error)); this.errors.push(normalizedError); @@ -341,7 +332,6 @@ export class AuthStorage { if (this.runtimeOverrides.has(provider)) return true; if (this.data[provider]) return true; if (getEnvApiKey(provider)) return true; - if (this.fallbackResolver?.(provider)) return true; return false; } @@ -362,10 +352,6 @@ export class AuthStorage { return { configured: false, source: "environment", label: envKeys[0] }; } - if (this.fallbackResolver?.(provider)) { - return { configured: false, source: "fallback", label: "custom provider config" }; - } - return { configured: false }; } @@ -459,9 +445,8 @@ export class AuthStorage { * 2. API key from auth.json * 3. OAuth token from auth.json (auto-refreshed with locking) * 4. Environment variable - * 5. Fallback resolver (models.json custom providers) */ - async getApiKey(providerId: string, options?: { includeFallback?: boolean }): Promise { + async getApiKey(providerId: string): Promise { // Runtime override takes highest priority const runtimeKey = this.runtimeOverrides.get(providerId); if (runtimeKey) { @@ -516,11 +501,6 @@ export class AuthStorage { const envKey = getEnvApiKey(providerId); if (envKey) return envKey; - // Fall back to custom resolver (e.g., models.json custom providers) - if (options?.includeFallback !== false) { - return this.fallbackResolver?.(providerId) ?? undefined; - } - return undefined; } diff --git a/packages/coding-agent/src/core/model-registry.ts b/packages/coding-agent/src/core/model-registry.ts index 114cbb00..39246d18 100644 --- a/packages/coding-agent/src/core/model-registry.ts +++ b/packages/coding-agent/src/core/model-registry.ts @@ -757,7 +757,7 @@ export class ModelRegistry { async getApiKeyAndHeaders(model: Model): Promise { try { const providerConfig = this.providerRequestConfigs.get(model.provider); - const apiKeyFromAuthStorage = await this.authStorage.getApiKey(model.provider, { includeFallback: false }); + const apiKeyFromAuthStorage = await this.authStorage.getApiKey(model.provider); const apiKey = apiKeyFromAuthStorage ?? (providerConfig?.apiKey @@ -844,7 +844,7 @@ export class ModelRegistry { * Get API key for a provider. */ async getApiKeyForProvider(provider: string): Promise { - const apiKey = await this.authStorage.getApiKey(provider, { includeFallback: false }); + const apiKey = await this.authStorage.getApiKey(provider); if (apiKey !== undefined) { return apiKey; } From e1283fc17a41ddaea53ad3949c660f35036d63dd Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 10 Jun 2026 23:37:11 +0200 Subject: [PATCH 016/122] docs(ai): rewrite README around the Models API Quick Start, querying, auth, OAuth, custom providers, faux, handoffs, browser usage, and the dev checklist now demonstrate createModels() + provider factories: sync model reads with explicit refresh, provider- owned auth resolution (getAuth, credential store, env tables), OAuthAuth login/refresh with prompt()/notify() callbacks, and createProvider() for custom/dynamic providers (replacing bare Model + global stream). Direct API implementation calls documented under the canonical ./api/ subpaths with the legacy aliases noted. The old global API is documented once, in a migration section pointing at /compat with an old-to-new mapping table. Image generation is documented as living on compat for now. All import/code claims verified with a compile-and-run smoke script. --- packages/ai/README.md | 1163 +++++++++++++++++++---------------------- 1 file changed, 550 insertions(+), 613 deletions(-) diff --git a/packages/ai/README.md b/packages/ai/README.md index 4190fcb8..72ab339b 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -1,6 +1,6 @@ # @earendil-works/pi-ai -Unified LLM API with automatic model discovery, provider configuration, token and cost tracking, and simple context persistence and hand-off to other models mid-session. +Unified LLM API with provider collections, automatic auth resolution, token and cost tracking, and simple context persistence and hand-off to other models mid-session. **Note**: This library only includes models that support tool calling (function calling), as this is essential for agentic workflows. @@ -9,6 +9,16 @@ Unified LLM API with automatic model discovery, provider configuration, token an - [Supported Providers](#supported-providers) - [Installation](#installation) - [Quick Start](#quick-start) +- [Providers and Models](#providers-and-models) + - [Provider Factories](#provider-factories) + - [All Built-in Providers](#all-built-in-providers) + - [Querying Models](#querying-models) + - [Static Catalog Reads](#static-catalog-reads) + - [Dynamic Providers](#dynamic-providers) +- [Auth](#auth) + - [How Auth Resolves](#how-auth-resolves) + - [Credential Store](#credential-store) + - [Environment Variables](#environment-variables) - [Tools](#tools) - [Defining Tools](#defining-tools) - [Handling Tool Calls](#handling-tool-calls) @@ -17,8 +27,6 @@ Unified LLM API with automatic model discovery, provider configuration, token an - [Complete Event Reference](#complete-event-reference) - [Image Input](#image-input) - [Image Generation](#image-generation) - - [Basic Image Generation](#basic-image-generation) - - [Notes and Limitations](#notes-and-limitations) - [Thinking/Reasoning](#thinkingreasoning) - [Unified Interface](#unified-interface-streamsimplecompletesimple) - [Provider-Specific Options](#provider-specific-options-streamcomplete) @@ -27,25 +35,21 @@ Unified LLM API with automatic model discovery, provider configuration, token an - [Error Handling](#error-handling) - [Aborting Requests](#aborting-requests) - [Continuing After Abort](#continuing-after-abort) -- [APIs, Models, and Providers](#apis-models-and-providers) - - [Providers and Models](#providers-and-models) - - [Querying Providers and Models](#querying-providers-and-models) - - [Custom Models](#custom-models) + - [Debugging Provider Payloads](#debugging-provider-payloads) +- [Custom Providers](#custom-providers) + - [createProvider()](#createprovider) + - [Calling API Implementations Directly](#calling-api-implementations-directly) - [OpenAI Compatibility Settings](#openai-compatibility-settings) - - [Type Safety](#type-safety) +- [Faux Provider for Tests](#faux-provider-for-tests) - [Cross-Provider Handoffs](#cross-provider-handoffs) - [Context Serialization](#context-serialization) - [Browser Usage](#browser-usage) - - [Browser Compatibility Notes](#browser-compatibility-notes) - - [Environment Variables](#environment-variables-nodejs-only) - - [Checking Environment Variables](#checking-environment-variables) - [OAuth Providers](#oauth-providers) - [Vertex AI](#vertex-ai) - [CLI Login](#cli-login) - [Programmatic OAuth](#programmatic-oauth) - - [Login Flow Example](#login-flow-example) - - [Using OAuth Tokens](#using-oauth-tokens) - - [Provider Notes](#provider-notes) +- [Migrating from the Old Global API](#migrating-from-the-old-global-api) +- [Development](#development) - [License](#license) ## Supported Providers @@ -89,11 +93,17 @@ TypeBox exports are re-exported from `@earendil-works/pi-ai`: `Type`, `Static`, ## Quick Start -```typescript -import { Type, getModel, stream, complete, Context, Tool, StringEnum } from '@earendil-works/pi-ai'; +You build a `Models` collection, register the providers you want, and stream through it. Importing a provider pulls only that provider's catalog; SDKs load lazily on first request. -// Fully typed with auto-complete support for both providers and models -const model = getModel('openai', 'gpt-4o-mini'); +```typescript +import { Type, createModels, type Context, type Tool } from '@earendil-works/pi-ai'; +import { openaiProvider } from '@earendil-works/pi-ai/providers/openai'; + +const models = createModels(); +models.setProvider(openaiProvider()); + +// Sync lookup against the collection +const model = models.getModel('openai', 'gpt-4o-mini')!; // Define tools with TypeBox schemas for type safety and validation const tools: Tool[] = [{ @@ -107,46 +117,25 @@ const tools: Tool[] = [{ // Build a conversation context (easily serializable and transferable between models) const context: Context = { systemPrompt: 'You are a helpful assistant.', - messages: [{ role: 'user', content: 'What time is it?' }], + messages: [{ role: 'user', content: 'What time is it?', timestamp: Date.now() }], tools }; -// Option 1: Streaming with all event types -const s = stream(model, context); +// Option 1: Streaming with all event types. +// Auth resolves through the provider (OPENAI_API_KEY from the environment here). +const s = models.stream(model, context); for await (const event of s) { switch (event.type) { case 'start': console.log(`Starting with ${event.partial.model}`); break; - case 'text_start': - console.log('\n[Text started]'); - break; case 'text_delta': process.stdout.write(event.delta); break; - case 'text_end': - console.log('\n[Text ended]'); - break; - case 'thinking_start': - console.log('[Model is thinking...]'); - break; case 'thinking_delta': process.stdout.write(event.delta); break; - case 'thinking_end': - console.log('[Thinking complete]'); - break; - case 'toolcall_start': - console.log(`\n[Tool call started: index ${event.contentIndex}]`); - break; - case 'toolcall_delta': - // Partial tool arguments are being streamed - const partialCall = event.partial.content[event.contentIndex]; - if (partialCall.type === 'toolCall') { - console.log(`[Streaming args for ${partialCall.name}]`); - } - break; case 'toolcall_end': console.log(`\nTool called: ${event.toolCall.name}`); console.log(`Arguments: ${JSON.stringify(event.toolCall.arguments)}`); @@ -155,7 +144,7 @@ for await (const event of s) { console.log(`\nFinished: ${event.reason}`); break; case 'error': - console.error(`Error: ${event.error}`); + console.error(`Error: ${event.error.errorMessage}`); break; } } @@ -167,7 +156,6 @@ context.messages.push(finalMessage); // Handle tool calls if any const toolCalls = finalMessage.content.filter(b => b.type === 'toolCall'); for (const call of toolCalls) { - // Execute the tool const result = call.name === 'get_time' ? new Date().toLocaleString('en-US', { timeZone: call.arguments.timezone || 'UTC', @@ -189,7 +177,7 @@ for (const call of toolCalls) { // Continue if there were tool calls if (toolCalls.length > 0) { - const continuation = await complete(model, context); + const continuation = await models.complete(model, context); context.messages.push(continuation); console.log('After tool execution:', continuation.content); } @@ -198,7 +186,7 @@ console.log(`Total tokens: ${finalMessage.usage.input} in, ${finalMessage.usage. console.log(`Cost: $${finalMessage.usage.cost.total.toFixed(4)}`); // Option 2: Get complete response without streaming -const response = await complete(model, context); +const response = await models.complete(model, context); for (const block of response.content) { if (block.type === 'text') { @@ -209,6 +197,180 @@ for (const block of response.content) { } ``` +Snippets in the rest of this README assume a `models` collection set up like this (with the relevant provider registered). + +## Providers and Models + +A **provider** is the runtime unit: it owns its model catalog, its auth (API key resolution, OAuth flows), and its stream behavior. A `Models` collection holds providers and routes every request to the provider that owns the model. + +Providers internally share **API implementations** (the wire protocols): Anthropic models use `anthropic-messages`, OpenAI uses `openai-responses`, while xAI, Groq, Cerebras, OpenRouter, and most others share `openai-completions`. Mixed-API providers (GitHub Copilot, OpenCode Zen) dispatch per model. + +### Provider Factories + +One factory per built-in provider, each a subpath import that pulls only that provider's catalog: + +```typescript +import { anthropicProvider } from '@earendil-works/pi-ai/providers/anthropic'; +import { openaiProvider } from '@earendil-works/pi-ai/providers/openai'; +import { openrouterProvider } from '@earendil-works/pi-ai/providers/openrouter'; +import { amazonBedrockProvider } from '@earendil-works/pi-ai/providers/amazon-bedrock'; +// ...one module per provider in the Supported Providers list + +const models = createModels(); +models.setProvider(anthropicProvider()); +models.setProvider(openrouterProvider()); +``` + +Provider SDKs (`@anthropic-ai/sdk`, `openai`, `@google/genai`, AWS) are **not** imported by registering a provider — they load lazily on the first request to a model of that API. + +### All Built-in Providers + +For apps that want everything: + +```typescript +import { builtinModels } from '@earendil-works/pi-ai/providers/all'; + +const models = builtinModels(); // a Models collection with every built-in provider registered +``` + +This imports all catalogs (it is the heavy, explicit entrypoint) but still no SDKs. + +### Querying Models + +Reads are synchronous and return the last-known lists: + +```typescript +const providers = models.getProviders(); // registered Provider objects +const provider = models.getProvider('anthropic'); // one provider + +const all = models.getModels(); // every model across providers +const anthropicModels = models.getModels('anthropic'); +const model = models.getModel('anthropic', 'claude-sonnet-4-5'); + +for (const m of anthropicModels) { + console.log(`${m.id}: ${m.name}`); + console.log(` API: ${m.api}`); + console.log(` Context: ${m.contextWindow} tokens`); + console.log(` Vision: ${m.input.includes('image')}`); + console.log(` Reasoning: ${m.reasoning}`); +} +``` + +Dynamically listed models are typed `Model`. Narrow with the `hasApi()` guard when you need API-specific option typing: + +```typescript +import { hasApi } from '@earendil-works/pi-ai'; + +const m = models.getModel('anthropic', 'claude-sonnet-4-5'); +if (m && hasApi(m, 'anthropic-messages')) { + // m: Model<'anthropic-messages'> — stream options fully typed + models.stream(m, context, { thinkingEnabled: true, thinkingBudgetTokens: 2048 }); +} +``` + +### Static Catalog Reads + +For tooling that wants the generated built-in catalog with full literal typing (provider and model IDs auto-complete), independent of any collection: + +```typescript +import { getBuiltinModel, getBuiltinModels, getBuiltinProviders } from '@earendil-works/pi-ai/providers/all'; + +const model = getBuiltinModel('openai', 'gpt-4o-mini'); // typed Model<'openai-responses'> +const providers = getBuiltinProviders(); +const anthropic = getBuiltinModels('anthropic'); +``` + +### Dynamic Providers + +Providers may have dynamic model lists (a llama.cpp server, a live OpenRouter listing). Reads stay sync; fetching is an explicit async verb: + +```typescript +// getModels() returns the last-known list (empty before the first refresh) +await models.refresh('llamacpp'); // fetch one provider's list; rejects on failure +await models.refresh(); // refresh all providers concurrently, best-effort +const fresh = models.getModel('llamacpp', 'qwen3-30b'); +``` + +Static built-in providers are no-ops for `refresh()`. See [createProvider()](#createprovider) for building a dynamic provider. + +## Auth + +Every provider owns its auth: how API keys resolve (stored credentials, environment variables, ambient sources like AWS profiles or gcloud ADC) and, where supported, OAuth login/refresh flows. + +### How Auth Resolves + +When you call `models.stream()`, the collection resolves auth through the owning provider and merges it into the request. Explicit per-request values always win: + +```typescript +// Resolved through the provider (env var, stored credential, OAuth token): +await models.complete(model, context); + +// Explicit key wins over anything the provider would resolve: +await models.complete(model, context, { apiKey: 'sk-explicit' }); +``` + +You can inspect resolution without making a request — useful for status UIs: + +```typescript +const auth = await models.getAuth(model); +if (auth) { + console.log(`configured via ${auth.source}`); // e.g. "ANTHROPIC_API_KEY", "OAuth", "stored credential" +} else { + console.log('not configured'); +} +``` + +`getAuth()` resolves `undefined` for unconfigured providers and rejects with `ModelsError` when something is actually broken (`"oauth"`: token refresh failed, credential preserved for re-login; `"auth"`: key resolution or credential store failure). Request paths surface the same failures as stream errors. + +### Credential Store + +Stored credentials (API keys entered interactively, OAuth tokens) live in a `CredentialStore` — one type-tagged credential per provider. pi-ai ships an in-memory default; apps inject persistent storage: + +```typescript +import { createModels, type CredentialStore } from '@earendil-works/pi-ai'; + +const models = createModels({ credentials: myFileBackedStore }); +``` + +The contract is small: `read(providerId)`, `modify(providerId, fn)` (the only write path — a serialized read-modify-write), and `delete(providerId)`. OAuth token refresh runs inside `modify`, so concurrent requests and processes cannot double-refresh a rotated token. A stored credential *owns* its provider: environment variables are only consulted when nothing is stored, and a failed refresh never silently falls back to an env key. + +### Environment Variables + +Built-in providers resolve these env vars (Node.js; in browsers pass `apiKey` explicitly): + +| Provider | Environment Variable(s) | +|----------|------------------------| +| OpenAI | `OPENAI_API_KEY` | +| Ant Ling | `ANT_LING_API_KEY` | +| Azure OpenAI | `AZURE_OPENAI_API_KEY` + `AZURE_OPENAI_BASE_URL` (e.g. `https://{resource}.openai.azure.com`) or `AZURE_OPENAI_RESOURCE_NAME`. Supports `*.openai.azure.com` and `*.cognitiveservices.azure.com`; root endpoints auto-normalize to `/openai/v1`. Optional: `AZURE_OPENAI_API_VERSION` (default `v1`), `AZURE_OPENAI_DEPLOYMENT_NAME_MAP`. | +| Anthropic | `ANTHROPIC_API_KEY` or `ANTHROPIC_OAUTH_TOKEN` | +| DeepSeek | `DEEPSEEK_API_KEY` | +| NVIDIA NIM | `NVIDIA_API_KEY` | +| Google | `GEMINI_API_KEY` | +| Vertex AI | `GOOGLE_CLOUD_API_KEY` or `GOOGLE_CLOUD_PROJECT` (or `GCLOUD_PROJECT`) + `GOOGLE_CLOUD_LOCATION` + ADC | +| Mistral | `MISTRAL_API_KEY` | +| Groq | `GROQ_API_KEY` | +| Cerebras | `CEREBRAS_API_KEY` | +| Cloudflare AI Gateway | `CLOUDFLARE_API_KEY` + `CLOUDFLARE_ACCOUNT_ID` + `CLOUDFLARE_GATEWAY_ID` | +| Cloudflare Workers AI | `CLOUDFLARE_API_KEY` + `CLOUDFLARE_ACCOUNT_ID` | +| xAI | `XAI_API_KEY` | +| Fireworks | `FIREWORKS_API_KEY` | +| Together AI | `TOGETHER_API_KEY` | +| OpenRouter | `OPENROUTER_API_KEY` | +| Vercel AI Gateway | `AI_GATEWAY_API_KEY` | +| zAI | `ZAI_API_KEY` | +| ZAI Coding Plan (China) | `ZAI_CODING_CN_API_KEY` | +| MiniMax | `MINIMAX_API_KEY` | +| OpenCode Zen / OpenCode Go | `OPENCODE_API_KEY` | +| Kimi For Coding | `KIMI_API_KEY` | +| Xiaomi MiMo (API billing) | `XIAOMI_API_KEY` | +| Xiaomi MiMo Token Plan (China) | `XIAOMI_TOKEN_PLAN_CN_API_KEY` | +| Xiaomi MiMo Token Plan (Amsterdam) | `XIAOMI_TOKEN_PLAN_AMS_API_KEY` | +| Xiaomi MiMo Token Plan (Singapore) | `XIAOMI_TOKEN_PLAN_SGP_API_KEY` | +| GitHub Copilot | `COPILOT_GITHUB_TOKEN` | + +Amazon Bedrock resolves ambient AWS credentials (`AWS_PROFILE`, access key pairs, `AWS_BEARER_TOKEN_BEDROCK`, ECS task roles, web identity tokens). Vertex AI resolves either an explicit key or gcloud Application Default Credentials plus project/location. + ## Tools Tools enable LLMs to interact with external systems. This library uses TypeBox schemas for type-safe tool definitions with automatic validation using TypeBox's built-in validator and value conversion utilities. TypeBox schemas can be serialized and deserialized as plain JSON, making them ideal for distributed systems. @@ -216,7 +378,7 @@ Tools enable LLMs to interact with external systems. This library uses TypeBox s ### Defining Tools ```typescript -import { Type, Tool, StringEnum } from '@earendil-works/pi-ai'; +import { Type, type Tool, StringEnum } from '@earendil-works/pi-ai'; // Define tool parameters with TypeBox const weatherTool: Tool = { @@ -251,11 +413,11 @@ Tool results use content blocks and can include both text and images: import { readFileSync } from 'fs'; const context: Context = { - messages: [{ role: 'user', content: 'What is the weather in London?' }], + messages: [{ role: 'user', content: 'What is the weather in London?', timestamp: Date.now() }], tools: [weatherTool] }; -const response = await complete(model, context); +const response = await models.complete(model, context); // Check for tool calls in the response for (const block of response.content) { @@ -296,7 +458,7 @@ context.messages.push({ During streaming, tool call arguments are progressively parsed as they arrive. This enables real-time UI updates before the complete arguments are available: ```typescript -const s = stream(model, context); +const s = models.stream(model, context); for await (const event of s) { if (event.type === 'toolcall_delta') { @@ -337,15 +499,13 @@ for await (const event of s) { ### Validating Tool Arguments -When using `agentLoop`, tool arguments are automatically validated against your TypeBox schemas before execution. If validation fails, the error is returned to the model as a tool result, allowing it to retry. - -When implementing your own tool execution loop with `stream()` or `complete()`, use `validateToolCall` to validate arguments before passing them to your tools: +When implementing your own tool execution loop, use `validateToolCall` to validate arguments before passing them to your tools: ```typescript -import { stream, validateToolCall, Tool } from '@earendil-works/pi-ai'; +import { validateToolCall, type Tool } from '@earendil-works/pi-ai'; const tools: Tool[] = [weatherTool, calculatorTool]; -const s = stream(model, { messages, tools }); +const s = models.stream(model, { messages, tools }); for await (const event of s) { if (event.type === 'toolcall_end') { @@ -398,9 +558,8 @@ Models with vision capabilities can process images. You can check if a model sup ```typescript import { readFileSync } from 'fs'; -import { getModel, complete } from '@earendil-works/pi-ai'; -const model = getModel('openai', 'gpt-4o-mini'); +const model = models.getModel('openai', 'gpt-4o-mini')!; // Check if model supports images if (model.input.includes('image')) { @@ -410,13 +569,14 @@ if (model.input.includes('image')) { const imageBuffer = readFileSync('image.png'); const base64Image = imageBuffer.toString('base64'); -const response = await complete(model, { +const response = await models.complete(model, { messages: [{ role: 'user', content: [ { type: 'text', text: 'What is in this image?' }, { type: 'image', data: base64Image, mimeType: 'image/png' } - ] + ], + timestamp: Date.now() }] }); @@ -430,14 +590,10 @@ for (const block of response.content) { ## Image Generation -Image generation uses a separate API surface from text/chat generation. Use `getImageModel()` / `getImageModels()` / `getImageProviders()` to discover image-generation models, and `generateImages()` to get the final result. - -Do not use `stream()` or `complete()` for image generation. Image generation is a one-shot API: `generateImages()` waits for the provider response and returns the final `AssistantImages` result. - -### Basic Image Generation +Image generation uses a separate API surface from text/chat generation and currently lives on the [compat entrypoint](#migrating-from-the-old-global-api). Use `getImageModel()` / `getImageModels()` / `getImageProviders()` to discover image-generation models, and `generateImages()` to get the final result. ```typescript -import { getImageModel, generateImages } from '@mariozechner/pi-ai'; +import { getImageModel, generateImages } from '@earendil-works/pi-ai/compat'; const model = getImageModel('openrouter', 'google/gemini-2.5-flash-image'); @@ -457,39 +613,11 @@ for (const block of result.output) { } ``` -Some models also support image input: +Notes: -```typescript -import { readFileSync } from 'fs'; - -const imageBuffer = readFileSync('input.png'); -const result = await generateImages(model, { - input: [ - { type: 'text', text: 'Create a variation of this image with a blue background.' }, - { type: 'image', data: imageBuffer.toString('base64'), mimeType: 'image/png' } - ] -}, { - apiKey: process.env.OPENROUTER_API_KEY -}); -``` - -Check capabilities on the model metadata: - -```typescript -console.log(model.input); // ['text', 'image'] -console.log(model.output); // ['image'] or ['image', 'text'] -``` - -### Notes and Limitations - -- Use `getImageModel(...)`, not `getModel(...)`. -- Use `generateImages()`, not `stream()` / `complete()`. -- Image-generation models do not participate in tool calling. -- Outputs are returned in `AssistantImages.output` and can include both base64-encoded `ImageContent` blocks and `TextContent` blocks. -- Some models return only images, others return images plus text. Check `model.output`. -- Some models accept image input, others are text-to-image only. Check `model.input`. -- Like the streaming APIs, image generation supports options such as `apiKey`, `signal`, `headers`, `onPayload`, and `onResponse`, and results may include `stopReason`, `responseId`, and `usage`. -- If you want a model to analyze images in a conversation or call tools, use the regular `stream()` / `complete()` APIs with a model that supports image input. +- Use `getImageModel(...)` and `generateImages()`; image-generation models do not work with the chat/stream APIs and do not participate in tool calling. +- Outputs are returned in `AssistantImages.output` and can include both base64-encoded `ImageContent` blocks and `TextContent` blocks. Check `model.output` and `model.input` for capabilities. +- Options such as `apiKey`, `signal`, `headers`, `onPayload`, and `onResponse` are supported; results may include `stopReason`, `responseId`, and `usage`. - At the moment, image generation is available through only one provider, OpenRouter. ## Thinking/Reasoning @@ -499,16 +627,11 @@ Many models support thinking/reasoning capabilities where they can show their in ### Unified Interface (streamSimple/completeSimple) ```typescript -import { getModel, streamSimple, completeSimple } from '@earendil-works/pi-ai'; - // Many models across providers support thinking/reasoning -const model = getModel('anthropic', 'claude-sonnet-4-20250514'); -// or getModel('openai', 'gpt-5-mini'); -// or getModel('google', 'gemini-2.5-flash'); -// or getModel('xai', 'grok-code-fast-1'); -// or getModel('groq', 'openai/gpt-oss-20b'); -// or getModel('cerebras', 'gpt-oss-120b'); -// or getModel('openrouter', 'z-ai/glm-4.5v'); +const model = models.getModel('anthropic', 'claude-sonnet-4-5')!; +// or models.getModel('openai', 'gpt-5-mini'); +// or models.getModel('google', 'gemini-2.5-flash'); +// or models.getModel('xai', 'grok-code-fast-1'); // Check if model supports reasoning if (model.reasoning) { @@ -516,8 +639,8 @@ if (model.reasoning) { } // Use the simplified reasoning option -const response = await completeSimple(model, { - messages: [{ role: 'user', content: 'Solve: 2x + 5 = 13' }] +const response = await models.completeSimple(model, { + messages: [{ role: 'user', content: 'Solve: 2x + 5 = 13', timestamp: Date.now() }] }, { reasoning: 'medium' // 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' }); @@ -534,33 +657,39 @@ for (const block of response.content) { ### Provider-Specific Options (stream/complete) -For fine-grained control, use the provider-specific options: +`models.stream()`/`complete()` accept the owning API's full option set. Use `hasApi()` to narrow a dynamically looked-up model to its API for full option typing: ```typescript -import { getModel, complete } from '@earendil-works/pi-ai'; +import { hasApi } from '@earendil-works/pi-ai'; // OpenAI Reasoning (o1, o3, gpt-5) -const openaiModel = getModel('openai', 'gpt-5-mini'); -await complete(openaiModel, context, { - reasoningEffort: 'medium', - reasoningSummary: 'detailed' // OpenAI Responses API only -}); +const openaiModel = models.getModel('openai', 'gpt-5-mini')!; +if (hasApi(openaiModel, 'openai-responses')) { + await models.complete(openaiModel, context, { + reasoningEffort: 'medium', + reasoningSummary: 'detailed' // OpenAI Responses API only + }); +} -// Anthropic Thinking (Claude Sonnet 4) -const anthropicModel = getModel('anthropic', 'claude-sonnet-4-20250514'); -await complete(anthropicModel, context, { - thinkingEnabled: true, - thinkingBudgetTokens: 8192 // Optional token limit -}); +// Anthropic Thinking +const anthropicModel = models.getModel('anthropic', 'claude-sonnet-4-5')!; +if (hasApi(anthropicModel, 'anthropic-messages')) { + await models.complete(anthropicModel, context, { + thinkingEnabled: true, + thinkingBudgetTokens: 8192 // Optional token limit + }); +} // Google Gemini Thinking -const googleModel = getModel('google', 'gemini-2.5-flash'); -await complete(googleModel, context, { - thinking: { - enabled: true, - budgetTokens: 8192 // -1 for dynamic, 0 to disable - } -}); +const googleModel = models.getModel('google', 'gemini-2.5-flash')!; +if (hasApi(googleModel, 'google-generative-ai')) { + await models.complete(googleModel, context, { + thinking: { + enabled: true, + budgetTokens: 8192 // -1 for dynamic, 0 to disable + } + }); +} ``` ### Streaming Thinking Content @@ -568,7 +697,7 @@ await complete(googleModel, context, { When streaming, thinking content is delivered through specific events: ```typescript -const s = streamSimple(model, context, { reasoning: 'high' }); +const s = models.streamSimple(model, context, { reasoning: 'high' }); for await (const event of s) { switch (event.type) { @@ -599,11 +728,11 @@ Every `AssistantMessage` includes a `stopReason` field that indicates how the ge ## Error Handling -When a request ends with an error (including aborts and tool call validation errors), the streaming API emits an error event: +Request failures never throw out of the stream functions: when a request ends with an error (including aborts and tool call validation errors), the streaming API emits an error event and the final message carries the details: ```typescript // In streaming -for await (const event of stream) { +for await (const event of s) { if (event.type === 'error') { // event.reason is either "error" or "aborted" // event.error is the AssistantMessage with partial content @@ -613,7 +742,7 @@ for await (const event of stream) { } // The final message will have the error details -const message = await stream.result(); +const message = await s.result(); if (message.stopReason === 'error' || message.stopReason === 'aborted') { console.error('Request failed:', message.errorMessage); // message.content contains any partial content received before the error @@ -621,21 +750,20 @@ if (message.stopReason === 'error' || message.stopReason === 'aborted') { } ``` +Auth failures (no key configured, OAuth refresh failed, unknown provider) surface the same way: as a stream error with `stopReason: "error"`. + ### Aborting Requests The abort signal allows you to cancel in-progress requests. Aborted requests have `stopReason === 'aborted'`: ```typescript -import { getModel, stream } from '@earendil-works/pi-ai'; - -const model = getModel('openai', 'gpt-4o-mini'); const controller = new AbortController(); // Abort after 2 seconds setTimeout(() => controller.abort(), 2000); -const s = stream(model, { - messages: [{ role: 'user', content: 'Write a long story' }] +const s = models.stream(model, { + messages: [{ role: 'user', content: 'Write a long story', timestamp: Date.now() }] }, { signal: controller.signal }); @@ -665,7 +793,7 @@ Aborted messages can be added to the conversation context and continued in subse ```typescript const context = { messages: [ - { role: 'user', content: 'Explain quantum computing in detail' } + { role: 'user', content: 'Explain quantum computing in detail', timestamp: Date.now() } ] }; @@ -673,14 +801,14 @@ const context = { const controller1 = new AbortController(); setTimeout(() => controller1.abort(), 2000); -const partial = await complete(model, context, { signal: controller1.signal }); +const partial = await models.complete(model, context, { signal: controller1.signal }); // Add the partial response to context context.messages.push(partial); -context.messages.push({ role: 'user', content: 'Please continue' }); +context.messages.push({ role: 'user', content: 'Please continue', timestamp: Date.now() }); // Continue the conversation -const continuation = await complete(model, context); +const continuation = await models.complete(model, context); ``` ### Debugging Provider Payloads @@ -688,7 +816,7 @@ const continuation = await complete(model, context); Use the `onPayload` callback to inspect the request payload sent to the provider. This is useful for debugging request formatting issues or provider validation errors. ```typescript -const response = await complete(model, context, { +const response = await models.complete(model, context, { onPayload: (payload) => { console.log('Provider payload:', JSON.stringify(payload, null, 2)); } @@ -697,147 +825,16 @@ const response = await complete(model, context, { The callback is supported by `stream`, `complete`, `streamSimple`, and `completeSimple`. -## APIs, Models, and Providers +## Custom Providers -The library uses a registry of API implementations. Built-in APIs include: +### createProvider() -- **`anthropic-messages`**: Anthropic Messages API (`streamAnthropic`, `AnthropicOptions`) -- **`google-generative-ai`**: Google Generative AI API (`streamGoogle`, `GoogleOptions`) -- **`google-vertex`**: Google Vertex AI API (`streamGoogleVertex`, `GoogleVertexOptions`) -- **`mistral-conversations`**: Mistral Conversations API (`streamMistral`, `MistralOptions`) -- **`openai-completions`**: OpenAI Chat Completions API (`streamOpenAICompletions`, `OpenAICompletionsOptions`) -- **`openai-responses`**: OpenAI Responses API (`streamOpenAIResponses`, `OpenAIResponsesOptions`) -- **`openai-codex-responses`**: OpenAI Codex Responses API (`streamOpenAICodexResponses`, `OpenAICodexResponsesOptions`) -- **`azure-openai-responses`**: Azure OpenAI Responses API (`streamAzureOpenAIResponses`, `AzureOpenAIResponsesOptions`) -- **`bedrock-converse-stream`**: Amazon Bedrock Converse API (`streamBedrock`, `BedrockOptions`) - -### Faux provider for tests - -`registerFauxProvider()` registers a temporary in-memory provider for tests and demos. It is opt-in and not part of the built-in provider set. +`createProvider()` builds a provider from parts: identity, auth, a model list, and an API implementation. Use it for local inference servers, proxies, or any OpenAI/Anthropic-compatible endpoint: ```typescript -import { - complete, - fauxAssistantMessage, - fauxText, - fauxThinking, - fauxToolCall, - registerFauxProvider, - stream, -} from '@earendil-works/pi-ai'; +import { createModels, createProvider, envApiKeyAuth, type Model } from '@earendil-works/pi-ai'; +import { openAICompletionsApi } from '@earendil-works/pi-ai/api/openai-completions.lazy'; -const registration = registerFauxProvider({ - tokensPerSecond: 50 // optional -}); - -const model = registration.getModel(); -const context = { - messages: [{ role: 'user', content: 'Summarize package.json and then call echo', timestamp: Date.now() }] -}; - -registration.setResponses([ - fauxAssistantMessage([ - fauxThinking('Need to inspect package metadata first.'), - fauxToolCall('echo', { text: 'package.json' }) - ], { stopReason: 'toolUse' }) -]); - -const first = await complete(model, context, { - sessionId: 'session-1', - cacheRetention: 'short' -}); -context.messages.push(first); - -context.messages.push({ - role: 'toolResult', - toolCallId: first.content.find((block) => block.type === 'toolCall')!.id, - toolName: 'echo', - content: [{ type: 'text', text: 'package.json contents here' }], - isError: false, - timestamp: Date.now() -}); - -registration.setResponses([ - fauxAssistantMessage([ - fauxThinking('Now I can summarize the tool output.'), - fauxText('Here is the summary.') - ]) -]); - -const s = stream(model, context); -for await (const event of s) { - console.log(event.type); -} - -// Optional: register multiple faux models for model-switching tests -const multiModel = registerFauxProvider({ - models: [ - { id: 'faux-fast', reasoning: false }, - { id: 'faux-thinker', reasoning: true } - ] -}); -const thinker = multiModel.getModel('faux-thinker'); - -console.log(thinker?.reasoning); -console.log(registration.getPendingResponseCount()); -console.log(registration.state.callCount); -registration.unregister(); -multiModel.unregister(); -``` - -Notes: -- Responses are consumed from a queue in request start order. -- If the queue is empty, the faux provider returns an assistant error message with `errorMessage: "No more faux responses queued"`. -- Use `registration.setResponses([...])` to replace the remaining queue and `registration.appendResponses([...])` to add more responses. -- `registration.models` exposes all registered faux models. `registration.getModel()` returns the first one, and `registration.getModel(id)` returns a specific one. -- Use `fauxAssistantMessage(...)` for scripted assistant replies. Use `fauxText(...)`, `fauxThinking(...)`, and `fauxToolCall(...)` to build content blocks without filling in low-level fields manually. -- `registration.unregister()` removes the temporary provider from the global API registry. -- Usage is estimated at roughly 1 token per 4 characters. When `sessionId` is present and `cacheRetention` is not `"none"`, prompt cache reads and writes are simulated automatically. -- Tool call arguments stream incrementally via `toolcall_delta` chunks. -- By default, each streamed chunk is emitted on its own microtask. Set `tokensPerSecond` to pace chunk delivery in real time. -- The intended use is one deterministic scripted flow per registration. If you need independent concurrent flows, register separate faux providers. - -### Providers and Models - -A **provider** offers models through a specific API. For example: -- **Anthropic** models use the `anthropic-messages` API -- **Google** models use the `google-generative-ai` API -- **OpenAI** models use the `openai-responses` API -- **Mistral** models use the `mistral-conversations` API -- **xAI, Cerebras, Groq, NVIDIA NIM, Together AI, etc.** models use the `openai-completions` API (OpenAI-compatible) - -### Querying Providers and Models - -```typescript -import { getProviders, getModels, getModel } from '@earendil-works/pi-ai'; - -// Get all available providers -const providers = getProviders(); -console.log(providers); // ['openai', 'anthropic', 'google', 'xai', 'groq', ...] - -// Get all models from a provider (fully typed) -const anthropicModels = getModels('anthropic'); -for (const model of anthropicModels) { - console.log(`${model.id}: ${model.name}`); - console.log(` API: ${model.api}`); // 'anthropic-messages' - console.log(` Context: ${model.contextWindow} tokens`); - console.log(` Vision: ${model.input.includes('image')}`); - console.log(` Reasoning: ${model.reasoning}`); -} - -// Get a specific model (both provider and model ID are auto-completed in IDEs) -const model = getModel('openai', 'gpt-4o-mini'); -console.log(`Using ${model.name} via ${model.api} API`); -``` - -### Custom Models - -You can create custom models for local inference servers or custom endpoints: - -```typescript -import { Model, stream } from '@earendil-works/pi-ai'; - -// Example: Ollama using OpenAI-compatible API const ollamaModel: Model<'openai-completions'> = { id: 'llama-3.1-8b', name: 'Llama 3.1 8B (Ollama)', @@ -851,53 +848,71 @@ const ollamaModel: Model<'openai-completions'> = { maxTokens: 32000 }; -// Example: LiteLLM proxy with explicit compat settings -const litellmModel: Model<'openai-completions'> = { - id: 'gpt-4o', - name: 'GPT-4o (via LiteLLM)', - api: 'openai-completions', - provider: 'litellm', - baseUrl: 'http://localhost:4000/v1', - reasoning: false, - input: ['text', 'image'], - cost: { input: 2.5, output: 10, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: 16384, - compat: { - supportsStore: false, // LiteLLM doesn't support the store field - } -}; +const ollama = createProvider({ + id: 'ollama', + name: 'Ollama', + baseUrl: 'http://localhost:11434/v1', + // Every provider declares auth; keyless local servers resolve as configured with no key. + auth: { apiKey: { name: 'Ollama', resolve: async () => ({ auth: {} }) } }, + models: [ollamaModel], + api: openAICompletionsApi(), +}); -// Example: Custom endpoint with headers (bypassing Cloudflare bot detection) -const proxyModel: Model<'anthropic-messages'> = { - id: 'claude-sonnet-4', - name: 'Claude Sonnet 4 (Proxied)', - api: 'anthropic-messages', - provider: 'custom-proxy', - baseUrl: 'https://proxy.example.com/v1', - reasoning: true, - input: ['text', 'image'], - cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, - contextWindow: 200000, - maxTokens: 8192, - headers: { - 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', - 'X-Custom-Auth': 'bearer-token-here' - } -}; +const models = createModels(); +models.setProvider(ollama); -// Use the custom model -const response = await stream(ollamaModel, context, { - apiKey: 'dummy' // Ollama doesn't need a real key +await models.complete(models.getModel('ollama', 'llama-3.1-8b')!, context); +``` + +For providers with real keys, `envApiKeyAuth(displayName, envVars)` gives the standard behavior (stored credential wins, then the first set env var): + +```typescript +const proxy = createProvider({ + id: 'my-proxy', + auth: { apiKey: envApiKeyAuth('My proxy API key', ['MY_PROXY_API_KEY']) }, + models: [/* ... */], + api: openAICompletionsApi(), }); ``` -Some OpenAI-compatible servers do not understand the `developer` role used for reasoning-capable models. For those providers, set `compat.supportsDeveloperRole` to `false` so the system prompt is sent as a `system` message instead. If the server also does not support `reasoning_effort`, set `compat.supportsReasoningEffort` to `false` too. +Mixed-API providers pass a map keyed by `model.api`; each model dispatches to its API's implementation: + +```typescript +import { anthropicMessagesApi } from '@earendil-works/pi-ai/api/anthropic-messages.lazy'; +import { openAIResponsesApi } from '@earendil-works/pi-ai/api/openai-responses.lazy'; + +const gateway = createProvider({ + id: 'my-gateway', + auth: { apiKey: envApiKeyAuth('Gateway key', ['GATEWAY_API_KEY']) }, + models: [/* models with api: 'anthropic-messages' or 'openai-responses' */], + api: { + 'anthropic-messages': anthropicMessagesApi(), + 'openai-responses': openAIResponsesApi(), + }, +}); +``` + +Dynamic model lists use `refreshModels`; the provider lists empty until the first `models.refresh()`: + +```typescript +const llamacpp = createProvider({ + id: 'llamacpp', + auth: { apiKey: { name: 'llama.cpp', resolve: async () => ({ auth: {} }) } }, + models: [], + refreshModels: async () => fetchModelsFromServer('http://localhost:8080'), + api: openAICompletionsApi(), +}); + +models.setProvider(llamacpp); +await models.refresh('llamacpp'); +``` + +Custom models can carry `headers` (e.g. proxies behind bot detection) and `compat` flags — see [OpenAI Compatibility Settings](#openai-compatibility-settings). + +Some OpenAI-compatible servers do not understand the `developer` role used for reasoning-capable models. For those providers, set `compat.supportsDeveloperRole` to `false` so the system prompt is sent as a `system` message instead. If the server also does not support `reasoning_effort`, set `compat.supportsReasoningEffort` to `false` too. This commonly applies to Ollama, vLLM, SGLang, and similar OpenAI-compatible servers. Use model-level `thinkingLevelMap` to describe model-specific thinking controls. Keys are pi thinking levels (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`). Missing keys use provider defaults, string values are sent to the provider, and `null` marks a level unsupported. -This commonly applies to Ollama, vLLM, SGLang, and similar OpenAI-compatible servers. You can set `compat` at the provider level or per model. - ```typescript const ollamaReasoningModel: Model<'openai-completions'> = { id: 'gpt-oss:20b', @@ -924,6 +939,36 @@ const ollamaReasoningModel: Model<'openai-completions'> = { }; ``` +### Calling API Implementations Directly + +The API implementations are importable on their own. Each module exports exactly `stream` and `streamSimple` with that API's full option typing. Direct calls bypass provider auth — pass `apiKey` explicitly: + +```typescript +import { stream } from '@earendil-works/pi-ai/api/anthropic-messages'; + +const s = stream(claudeModel, context, { + apiKey: process.env.ANTHROPIC_API_KEY, + thinkingEnabled: true, + thinkingBudgetTokens: 2048, +}); +``` + +Built-in API implementations live under `./api/`: + +| API id | Options type | +|--------|--------------| +| `anthropic-messages` | `AnthropicOptions` | +| `openai-completions` | `OpenAICompletionsOptions` | +| `openai-responses` | `OpenAIResponsesOptions` | +| `openai-codex-responses` | `OpenAICodexResponsesOptions` | +| `azure-openai-responses` | `AzureOpenAIResponsesOptions` | +| `google-generative-ai` | `GoogleOptions` | +| `google-vertex` | `GoogleVertexOptions` | +| `mistral-conversations` | `MistralOptions` | +| `bedrock-converse-stream` | `BedrockOptions` | + +Importing an implementation module loads its SDK. The `./api/.lazy` wrappers (used by the provider factories) defer that load to the first request. Legacy subpaths from older releases (`./anthropic`, `./google`, `./mistral`, `./openai-completions`, ...) still resolve to the corresponding API implementation modules. + ### OpenAI Compatibility Settings The `openai-completions` API is implemented by many providers with minor differences. By default, the library auto-detects compatibility settings based on `baseUrl` for a small set of known OpenAI-compatible providers (Cerebras, xAI, Chutes, DeepSeek, NVIDIA NIM, Together AI, zAi, OpenCode, Cloudflare Workers AI, etc.). For custom proxies or unknown endpoints, you can override these settings via the `compat` field. For `openai-responses` models, the compat field supports Responses-specific flags. @@ -960,30 +1005,97 @@ If `compat` is not set, the library falls back to URL-based detection. If `compa - **Custom inference servers**: May use non-standard field names - **Self-hosted endpoints**: May have different feature support -### Type Safety +## Faux Provider for Tests -Models are typed by their API, which keeps the model metadata accurate. Provider-specific option types are enforced when you call the provider functions directly. The generic `stream` and `complete` functions accept `StreamOptions` with additional provider fields. +`fauxProvider()` builds an in-memory provider with scripted responses for tests and demos: ```typescript -import { streamAnthropic, type AnthropicOptions } from '@earendil-works/pi-ai'; +import { + createModels, + fauxAssistantMessage, + fauxProvider, + fauxText, + fauxThinking, + fauxToolCall, +} from '@earendil-works/pi-ai'; -// TypeScript knows this is an Anthropic model -const claude = getModel('anthropic', 'claude-sonnet-4-20250514'); +const faux = fauxProvider({ + tokensPerSecond: 50 // optional +}); -const options: AnthropicOptions = { - thinkingEnabled: true, - thinkingBudgetTokens: 2048 +const models = createModels(); +models.setProvider(faux.provider); + +const model = faux.getModel(); +const context = { + messages: [{ role: 'user', content: 'Summarize package.json and then call echo', timestamp: Date.now() }] }; -await streamAnthropic(claude, context, options); +faux.setResponses([ + fauxAssistantMessage([ + fauxThinking('Need to inspect package metadata first.'), + fauxToolCall('echo', { text: 'package.json' }) + ], { stopReason: 'toolUse' }) +]); + +const first = await models.complete(model, context, { + sessionId: 'session-1', + cacheRetention: 'short' +}); +context.messages.push(first); + +context.messages.push({ + role: 'toolResult', + toolCallId: first.content.find((block) => block.type === 'toolCall')!.id, + toolName: 'echo', + content: [{ type: 'text', text: 'package.json contents here' }], + isError: false, + timestamp: Date.now() +}); + +faux.setResponses([ + fauxAssistantMessage([ + fauxThinking('Now I can summarize the tool output.'), + fauxText('Here is the summary.') + ]) +]); + +const s = models.stream(model, context); +for await (const event of s) { + console.log(event.type); +} + +// Optional: multiple faux models for model-switching tests +const multiModel = fauxProvider({ + provider: 'faux-multi', + models: [ + { id: 'faux-fast', reasoning: false }, + { id: 'faux-thinker', reasoning: true } + ] +}); +models.setProvider(multiModel.provider); +const thinker = multiModel.getModel('faux-thinker'); + +console.log(thinker?.reasoning); +console.log(faux.getPendingResponseCount()); +console.log(faux.state.callCount); ``` +Notes: +- Responses are consumed from a queue in request start order. +- If the queue is empty, the faux provider returns an assistant error message with `errorMessage: "No more faux responses queued"`. +- Use `faux.setResponses([...])` to replace the remaining queue and `faux.appendResponses([...])` to add more responses. +- `faux.models` exposes all faux models. `faux.getModel()` returns the first one, and `faux.getModel(id)` returns a specific one. +- Use `fauxAssistantMessage(...)` for scripted assistant replies. Use `fauxText(...)`, `fauxThinking(...)`, and `fauxToolCall(...)` to build content blocks without filling in low-level fields manually. +- Usage is estimated at roughly 1 token per 4 characters. When `sessionId` is present and `cacheRetention` is not `"none"`, prompt cache reads and writes are simulated automatically. +- Tool call arguments stream incrementally via `toolcall_delta` chunks. +- By default, each streamed chunk is emitted on its own microtask. Set `tokensPerSecond` to pace chunk delivery in real time. +- The intended use is one deterministic scripted flow per handle. If you need independent concurrent flows, create separate faux providers with distinct `provider` ids. + ## Cross-Provider Handoffs The library supports seamless handoffs between different LLM providers within the same conversation. This allows you to switch models mid-conversation while preserving context, including thinking blocks, tool calls, and tool results. -### How It Works - When messages from one provider are sent to a different provider, the library automatically transforms them for compatibility: - **User and tool result messages** are passed through unchanged @@ -991,98 +1103,86 @@ When messages from one provider are sent to a different provider, the library au - **Assistant messages from different providers** have their thinking blocks converted to text with `` tags - **Tool calls and regular text** are preserved unchanged -### Example: Multi-Provider Conversation - ```typescript -import { getModel, complete, Context } from '@earendil-works/pi-ai'; +import { createModels, type Context } from '@earendil-works/pi-ai'; +import { anthropicProvider } from '@earendil-works/pi-ai/providers/anthropic'; +import { openaiProvider } from '@earendil-works/pi-ai/providers/openai'; +import { googleProvider } from '@earendil-works/pi-ai/providers/google'; + +const models = createModels(); +models.setProvider(anthropicProvider()); +models.setProvider(openaiProvider()); +models.setProvider(googleProvider()); + +const context: Context = { messages: [] }; // Start with Claude -const claude = getModel('anthropic', 'claude-sonnet-4-20250514'); -const context: Context = { - messages: [] -}; - -context.messages.push({ role: 'user', content: 'What is 25 * 18?' }); -const claudeResponse = await complete(claude, context, { - thinkingEnabled: true -}); -context.messages.push(claudeResponse); +const claude = models.getModel('anthropic', 'claude-sonnet-4-5')!; +context.messages.push({ role: 'user', content: 'What is 25 * 18?', timestamp: Date.now() }); +context.messages.push(await models.completeSimple(claude, context, { reasoning: 'medium' })); // Switch to GPT-5 - it will see Claude's thinking as tagged text -const gpt5 = getModel('openai', 'gpt-5-mini'); -context.messages.push({ role: 'user', content: 'Is that calculation correct?' }); -const gptResponse = await complete(gpt5, context); -context.messages.push(gptResponse); +const gpt5 = models.getModel('openai', 'gpt-5-mini')!; +context.messages.push({ role: 'user', content: 'Is that calculation correct?', timestamp: Date.now() }); +context.messages.push(await models.complete(gpt5, context)); // Switch to Gemini -const gemini = getModel('google', 'gemini-2.5-flash'); -context.messages.push({ role: 'user', content: 'What was the original question?' }); -const geminiResponse = await complete(gemini, context); +const gemini = models.getModel('google', 'gemini-2.5-flash')!; +context.messages.push({ role: 'user', content: 'What was the original question?', timestamp: Date.now() }); +const geminiResponse = await models.complete(gemini, context); ``` -### Provider Compatibility - -All providers can handle messages from other providers, including: -- Text content -- Tool calls and tool results (including images in tool results) -- Thinking/reasoning blocks (transformed to tagged text for cross-provider compatibility) -- Aborted messages with partial content - -This enables flexible workflows where you can: -- Start with a fast model for initial responses -- Switch to a more capable model for complex reasoning -- Use specialized models for specific tasks -- Maintain conversation continuity across provider outages +All providers can handle messages from other providers — text, tool calls and results (including images), thinking blocks (transformed to tagged text), and aborted messages with partial content. This enables flexible workflows: start with a fast model, switch to a more capable one for complex reasoning, or maintain continuity across provider outages. ## Context Serialization The `Context` object can be easily serialized and deserialized using standard JSON methods, making it simple to persist conversations, implement chat history, or transfer contexts between services: ```typescript -import { Context, getModel, complete } from '@earendil-works/pi-ai'; - -// Create and use a context const context: Context = { systemPrompt: 'You are a helpful assistant.', messages: [ - { role: 'user', content: 'What is TypeScript?' } + { role: 'user', content: 'What is TypeScript?', timestamp: Date.now() } ] }; -const model = getModel('openai', 'gpt-4o-mini'); -const response = await complete(model, context); +const model = models.getModel('openai', 'gpt-4o-mini')!; +const response = await models.complete(model, context); context.messages.push(response); // Serialize the entire context const serialized = JSON.stringify(context); -console.log('Serialized context size:', serialized.length, 'bytes'); // Save to database, localStorage, file, etc. localStorage.setItem('conversation', serialized); // Later: deserialize and continue the conversation const restored: Context = JSON.parse(localStorage.getItem('conversation')!); -restored.messages.push({ role: 'user', content: 'Tell me more about its type system' }); +restored.messages.push({ role: 'user', content: 'Tell me more about its type system', timestamp: Date.now() }); // Continue with any model -const newModel = getModel('anthropic', 'claude-3-5-haiku-20241022'); -const continuation = await complete(newModel, restored); +const newModel = models.getModel('anthropic', 'claude-3-5-haiku-20241022')!; +const continuation = await models.complete(newModel, restored); ``` +Models are plain serializable data too — no functions or implementations attached — so persisting "which model was this conversation using" is a `JSON.stringify` away. + > **Note**: If the context contains images (encoded as base64 as shown in the Image Input section), those will also be serialized. ## Browser Usage -The library supports browser environments. You must pass the API key explicitly since environment variables are not available in browsers: +The library supports browser environments. The core entrypoint and provider factories are side-effect free and bundle cleanly. Pass API keys explicitly since environment variables are not available in browsers: ```typescript -import { getModel, complete } from '@earendil-works/pi-ai'; +import { createModels } from '@earendil-works/pi-ai'; +import { anthropicProvider } from '@earendil-works/pi-ai/providers/anthropic'; -// API key must be passed explicitly in browser -const model = getModel('anthropic', 'claude-3-5-haiku-20241022'); +const models = createModels(); +models.setProvider(anthropicProvider()); -const response = await complete(model, { - messages: [{ role: 'user', content: 'Hello!' }] +const model = models.getModel('anthropic', 'claude-3-5-haiku-20241022')!; +const response = await models.complete(model, { + messages: [{ role: 'user', content: 'Hello!', timestamp: Date.now() }] }, { apiKey: 'your-api-key' }); @@ -1090,79 +1190,53 @@ const response = await complete(model, { > **Security Warning**: Exposing API keys in frontend code is dangerous. Anyone can extract and abuse your keys. Only use this approach for internal tools or demos. For production applications, use a backend proxy that keeps your API keys secure. -### Browser Compatibility Notes +Browser compatibility notes: -- Amazon Bedrock (`bedrock-converse-stream`) is not supported in browser environments. -- OAuth login flows are not supported in browser environments. Use the `@earendil-works/pi-ai/oauth` entry point in Node.js. -- In browser builds, Bedrock can still appear in model lists. Calls to Bedrock models fail at runtime. +- Amazon Bedrock (`bedrock-converse-stream`) is not supported in browser environments. It can still appear in model lists; calls fail at runtime. +- OAuth login flows are Node-only. They are lazy-loaded behind bundler-opaque imports, so registering an OAuth-capable provider does not pull Node-only code into a browser bundle — only actually logging in would. - Use a server-side proxy or backend service if you need Bedrock or OAuth-based auth from a web app. -### Environment Variables (Node.js only) - -In Node.js environments, you can set environment variables to avoid passing API keys: - -| Provider | Environment Variable(s) | -|----------|------------------------| -| OpenAI | `OPENAI_API_KEY` | -| Ant Ling | `ANT_LING_API_KEY` | -| Azure OpenAI | `AZURE_OPENAI_API_KEY` + `AZURE_OPENAI_BASE_URL` (e.g. `https://{resource}.openai.azure.com`) or `AZURE_OPENAI_RESOURCE_NAME`. Supports `*.openai.azure.com` and `*.cognitiveservices.azure.com`; root endpoints auto-normalize to `/openai/v1`. Optional: `AZURE_OPENAI_API_VERSION` (default `v1`), `AZURE_OPENAI_DEPLOYMENT_NAME_MAP`. | -| Anthropic | `ANTHROPIC_API_KEY` or `ANTHROPIC_OAUTH_TOKEN` | -| DeepSeek | `DEEPSEEK_API_KEY` | -| NVIDIA NIM | `NVIDIA_API_KEY` | -| Google | `GEMINI_API_KEY` | -| Vertex AI | `GOOGLE_CLOUD_API_KEY` or `GOOGLE_CLOUD_PROJECT` (or `GCLOUD_PROJECT`) + `GOOGLE_CLOUD_LOCATION` + ADC | -| Mistral | `MISTRAL_API_KEY` | -| Groq | `GROQ_API_KEY` | -| Cerebras | `CEREBRAS_API_KEY` | -| Cloudflare AI Gateway | `CLOUDFLARE_API_KEY` + `CLOUDFLARE_ACCOUNT_ID` + `CLOUDFLARE_GATEWAY_ID` | -| Cloudflare Workers AI | `CLOUDFLARE_API_KEY` + `CLOUDFLARE_ACCOUNT_ID` | -| xAI | `XAI_API_KEY` | -| Fireworks | `FIREWORKS_API_KEY` | -| Together AI | `TOGETHER_API_KEY` | -| OpenRouter | `OPENROUTER_API_KEY` | -| Vercel AI Gateway | `AI_GATEWAY_API_KEY` | -| zAI | `ZAI_API_KEY` | -| ZAI Coding Plan (China) | `ZAI_CODING_CN_API_KEY` | -| MiniMax | `MINIMAX_API_KEY` | -| OpenCode Zen / OpenCode Go | `OPENCODE_API_KEY` | -| Kimi For Coding | `KIMI_API_KEY` | -| Xiaomi MiMo (API billing) | `XIAOMI_API_KEY` | -| Xiaomi MiMo Token Plan (China) | `XIAOMI_TOKEN_PLAN_CN_API_KEY` | -| Xiaomi MiMo Token Plan (Amsterdam) | `XIAOMI_TOKEN_PLAN_AMS_API_KEY` | -| Xiaomi MiMo Token Plan (Singapore) | `XIAOMI_TOKEN_PLAN_SGP_API_KEY` | -| GitHub Copilot | `COPILOT_GITHUB_TOKEN` | - -When set, the library automatically uses these keys: - -```typescript -// Uses OPENAI_API_KEY from environment -const model = getModel('openai', 'gpt-4o-mini'); -const response = await complete(model, context); - -// Or override with explicit key -const response = await complete(model, context, { - apiKey: 'sk-different-key' -}); -``` - -### Checking Environment Variables - -```typescript -import { getEnvApiKey } from '@earendil-works/pi-ai'; - -// Check if an API key is set in environment variables -const key = getEnvApiKey('openai'); // checks OPENAI_API_KEY -``` - ## OAuth Providers -Several providers require OAuth authentication instead of static API keys: +Several providers support OAuth authentication instead of static API keys: - **Anthropic** (Claude Pro/Max subscription) - **OpenAI Codex** (ChatGPT Plus/Pro subscription, access to GPT-5.x Codex models) - **GitHub Copilot** (Copilot subscription) -For paid Cloud Code Assist subscriptions, set `GOOGLE_CLOUD_PROJECT` or `GOOGLE_CLOUD_PROJECT_ID` to your project ID. +Each of these providers carries an `OAuthAuth` on `provider.auth.oauth` with three operations: `login(callbacks)` runs the interactive flow and returns a credential, `refresh(credential)` exchanges the refresh token, and `toAuth(credential)` derives request auth (GitHub Copilot's per-account base URL comes from here). Refresh is automatic: `models.getAuth()` and the request paths refresh expired tokens under a credential-store lock, so concurrent requests and processes cannot double-refresh. + +```typescript +import { createModels } from '@earendil-works/pi-ai'; +import { anthropicProvider } from '@earendil-works/pi-ai/providers/anthropic'; + +const models = createModels({ credentials: myStore }); // persistent CredentialStore +models.setProvider(anthropicProvider()); + +// Login: drive the flow with prompt()/notify() callbacks, persist the credential +const provider = models.getProvider('anthropic')!; +const credential = await provider.auth.oauth!.login({ + prompt: async (p) => { + // p.type: 'text' | 'secret' | 'select' | 'manual_code' + // manual_code prompts race a local callback server; p.signal aborts them when the server wins + return await askUser(p.message); + }, + notify: (event) => { + // event.type: 'auth_url' | 'device_code' | 'progress' + if (event.type === 'auth_url') console.log(`Open: ${event.url}`); + if (event.type === 'device_code') console.log(`Code: ${event.userCode} at ${event.verificationUri}`); + if (event.type === 'progress') console.log(event.message); + }, +}); +await myStore.modify('anthropic', async () => credential); + +// From here on, requests resolve and refresh the token automatically +const model = models.getModel('anthropic', 'claude-sonnet-4-5')!; +await models.complete(model, context); + +// Logout +await myStore.delete('anthropic'); +``` ### Vertex AI @@ -1174,8 +1248,6 @@ Vertex AI models support either a Google Cloud API key or Application Default Cr When using ADC, also set `GOOGLE_CLOUD_PROJECT` (or `GCLOUD_PROJECT`) and `GOOGLE_CLOUD_LOCATION`. You can also pass `project`/`location` in the call options. When using `GOOGLE_CLOUD_API_KEY`, `project` and `location` are not required. -Example: - ```bash # Local (uses your user credentials) gcloud auth application-default login @@ -1186,23 +1258,6 @@ export GOOGLE_CLOUD_LOCATION="us-central1" export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json" ``` -```typescript -import { getModel, complete } from '@earendil-works/pi-ai'; - -(async () => { - const model = getModel('google-vertex', 'gemini-2.5-flash'); - const response = await complete(model, { - messages: [{ role: 'user', content: 'Hello from Vertex AI' }] - }, { - apiKey: process.env.GOOGLE_CLOUD_API_KEY, - }); - - for (const block of response.content) { - if (block.type === 'text') console.log(block.text); - } -})().catch(console.error); -``` - Official docs: [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials) ### CLI Login @@ -1219,76 +1274,9 @@ Credentials are saved to `auth.json` in the current directory. ### Programmatic OAuth -The library provides login and token refresh functions via the `@earendil-works/pi-ai/oauth` entry point. Credential storage is the caller's responsibility. +The legacy flow functions remain available via the `@earendil-works/pi-ai/oauth` entry point (`loginAnthropic`, `loginOpenAICodex`, `loginGitHubCopilot`, `refreshOAuthToken`, `getOAuthApiKey`); credential storage is the caller's responsibility there. New code should prefer the provider-owned `OAuthAuth` shown above — it composes with the credential store and gets locked auto-refresh for free. -```typescript -import { - // Login functions (return credentials, do not store) - loginAnthropic, - loginOpenAICodex, - loginGitHubCopilot, - loginGeminiCli, - - // Token management - refreshOAuthToken, // (provider, credentials) => new credentials - getOAuthApiKey, // (provider, credentialsMap) => { newCredentials, apiKey } | null - - // Types - type OAuthProvider, - type OAuthCredentials, -} from '@earendil-works/pi-ai/oauth'; -``` - -### Login Flow Example - -```typescript -import { loginGitHubCopilot } from '@earendil-works/pi-ai/oauth'; -import { writeFileSync } from 'fs'; - -const credentials = await loginGitHubCopilot({ - onAuth: (url, instructions) => { - console.log(`Open: ${url}`); - if (instructions) console.log(instructions); - }, - onPrompt: async (prompt) => { - return await getUserInput(prompt.message); - }, - onProgress: (message) => console.log(message) -}); - -// Store credentials yourself -const auth = { 'github-copilot': { type: 'oauth', ...credentials } }; -writeFileSync('auth.json', JSON.stringify(auth, null, 2)); -``` - -### Using OAuth Tokens - -Use `getOAuthApiKey()` to get an API key, automatically refreshing if expired: - -```typescript -import { getModel, complete } from '@earendil-works/pi-ai'; -import { getOAuthApiKey } from '@earendil-works/pi-ai/oauth'; -import { readFileSync, writeFileSync } from 'fs'; - -// Load your stored credentials -const auth = JSON.parse(readFileSync('auth.json', 'utf-8')); - -// Get API key (refreshes if expired) -const result = await getOAuthApiKey('github-copilot', auth); -if (!result) throw new Error('Not logged in'); - -// Save refreshed credentials -auth['github-copilot'] = { type: 'oauth', ...result.newCredentials }; -writeFileSync('auth.json', JSON.stringify(auth, null, 2)); - -// Use the API key -const model = getModel('github-copilot', 'gpt-4o'); -const response = await complete(model, { - messages: [{ role: 'user', content: 'Hello!' }] -}, { apiKey: result.apiKey }); -``` - -### Provider Notes +Provider notes: **OpenAI Codex**: Requires a ChatGPT Plus or Pro subscription. Provides access to GPT-5.x Codex models with extended context windows and reasoning capabilities. The library automatically handles session-based prompt caching when `sessionId` is provided in stream options. You can set `transport` in stream options to `"sse"`, `"websocket"`, or `"auto"` for Codex Responses transport selection. When using WebSocket with a `sessionId`, connections are reused per session and expire after 5 minutes of inactivity. @@ -1296,95 +1284,44 @@ const response = await complete(model, { **GitHub Copilot**: If you get "The requested model is not supported" error, enable the model manually in VS Code: open Copilot Chat, click the model selector, select the model (warning icon), and click "Enable". +## Migrating from the Old Global API + +Older versions exposed a global API: `stream()`/`complete()` dispatching on `model.api` via a global registry, sync `getModel()`/`getModels()`/`getProviders()` catalog reads, `registerApiProvider()`, `getEnvApiKey()`, and per-API lazy stream functions. That surface lives unchanged on the **compat entrypoint**: + +```typescript +// Before +import { getModel, complete } from '@earendil-works/pi-ai'; + +// After (verbatim behavior, one import-path change) +import { getModel, complete } from '@earendil-works/pi-ai/compat'; +``` + +Compat is a strict superset of the root entrypoint, so a file can switch its import path wholesale. It will be removed in a future release; migrate to `createModels()` + provider factories: + +| Old | New | +|-----|-----| +| `getModel('openai', 'gpt-4o-mini')` | `models.getModel('openai', 'gpt-4o-mini')` or `getBuiltinModel()` from `providers/all` | +| `getModels('anthropic')` / `getProviders()` | `models.getModels('anthropic')` / `models.getProviders()` or `getBuiltin*` | +| `stream(model, ctx, opts)` (env-key injection) | `models.stream(model, ctx, opts)` (provider auth resolution) | +| `registerApiProvider({ api, stream, streamSimple })` | `createProvider({ id, auth, models, api })` + `models.setProvider()` | +| `getEnvApiKey('openai')` | `await models.getAuth(model)` | +| `streamAnthropic(model, ctx, opts)` | `stream` from `@earendil-works/pi-ai/anthropic`, or a provider in a collection | +| `registerFauxProvider()` | `fauxProvider()` + `models.setProvider()` | + ## Development ### Adding a New Provider -Adding a new LLM provider requires changes across multiple files. This checklist covers all necessary steps: +The layered layout: API implementations live in `src/api/`, provider factories in `src/providers/`, generated catalogs in `src/providers/.models.ts`. -#### 1. Core Types (`src/types.ts`) - -- Add the API identifier to `KnownApi` (for example `"bedrock-converse-stream"`) -- Create an options interface extending `StreamOptions` (for example `BedrockOptions`) -- Add the provider name to `KnownProvider` (for example `"amazon-bedrock"`) - -#### 2. Provider Implementation (`src/providers/`) - -Create a new provider file (for example `amazon-bedrock.ts`) that exports: - -- `stream()` function returning `AssistantMessageEventStream` -- `streamSimple()` for `SimpleStreamOptions` mapping -- Provider-specific options interface -- Message conversion functions to transform `Context` to provider format -- Tool conversion if the provider supports tools -- Response parsing to emit standardized events (`text`, `tool_call`, `thinking`, `usage`, `stop`) - -#### 3. API Registry Integration (`src/providers/register-builtins.ts`) - -- Register the API with `registerApiProvider()` -- Add a package subpath export in `package.json` for the provider module (`./dist/providers/.js`) -- Add lazy loader wrappers in `src/providers/register-builtins.ts`, do not statically import provider implementation modules there -- Add any root-level `export type` re-exports in `src/index.ts` that should remain available from `@earendil-works/pi-ai` -- Add credential detection in `env-api-keys.ts` for the new provider -- Ensure `streamSimple` handles auth lookup via `getEnvApiKey()` or provider-specific auth - -#### 4. Model Generation (`scripts/generate-models.ts`, `scripts/generate-image-models.ts`) - -- Add logic to fetch and parse models from the provider's source (e.g., models.dev API) -- Map chat/tool-capable provider model data to the standardized `Model` interface via `scripts/generate-models.ts` -- Map image-generation provider model data to the standardized `ImagesModel` interface via `scripts/generate-image-models.ts` -- Handle provider-specific quirks (pricing format, capability flags, model ID transformations) - -#### 5. Tests (`test/`) - -Create or update test files to cover the new provider: - -- `stream.test.ts` - Basic streaming and tool use -- `tokens.test.ts` - Token usage reporting -- `abort.test.ts` - Request cancellation -- `empty.test.ts` - Empty message handling -- `context-overflow.test.ts` - Context limit errors -- `image-limits.test.ts` - Image support (if applicable) -- `unicode-surrogate.test.ts` - Unicode handling -- `tool-call-without-result.test.ts` - Orphaned tool calls -- `image-tool-result.test.ts` - Images in tool results -- `total-tokens.test.ts` - Token counting accuracy -- `cross-provider-handoff.test.ts` - Cross-provider context replay - -For `cross-provider-handoff.test.ts`, add at least one provider/model pair. If the provider exposes multiple model families (for example GPT and Claude), add at least one pair per family. - -For providers with non-standard auth (AWS, Google Vertex), create a utility like `bedrock-utils.ts` with credential detection helpers. - -#### 6. Coding Agent Integration (`../coding-agent/`) - -Update `src/core/model-resolver.ts`: - -- Add a default model ID for the provider in `DEFAULT_MODELS` - -Update `src/cli/args.ts`: - -- Add environment variable documentation in the help text - -Update `README.md`: - -- Add the provider to the providers section with setup instructions - -#### 7. Documentation - -Update `packages/ai/README.md`: - -- Add to the Supported Providers table -- Document any provider-specific options or authentication requirements -- Add environment variable to the Environment Variables section - -#### 8. Changelog - -Add an entry to `packages/ai/CHANGELOG.md` under `## [Unreleased]`: - -```markdown -### Added -- Added support for [Provider Name] provider ([#PR](link) by [@author](link)) -``` +1. **Core types** (`src/types.ts`): add the API id to `KnownApi` (if it is a new API), the provider id to `KnownProvider`, and the options type to `ApiOptionsMap`. +2. **API implementation** (`src/api/.ts`, only for a new API): export exactly `stream` and `streamSimple`, plus the options interface extending `StreamOptions`. Add a lazy wrapper `src/api/.lazy.ts` (`Api()` via `lazyApi()`). +3. **Catalog** (`scripts/generate-models.ts`): add fetching/mapping for the provider's models (e.g. from models.dev); regeneration emits `src/providers/.models.ts` and the aggregator. +4. **Provider factory** (`src/providers/.ts`): `createProvider()` wiring catalog + auth (`envApiKeyAuth` for standard key providers, custom `ApiKeyAuth` for ambient auth, `lazyOAuth` where OAuth exists) + the lazy API wrapper. Register it in `src/providers/all.ts`. +5. **Compat**: if it is a new API, register it in the builtin list in `src/compat.ts` and add the legacy subpath in `package.json` if warranted. +6. **Tests** (`test/`): cover streaming/tools/abort/tokens for new APIs (`stream.test.ts` and friends, env-gated), `cross-provider-handoff.test.ts` pairs, and provider listing/auth in `providers.test.ts`. +7. **Docs**: this README (Supported Providers, env var table) and `CHANGELOG.md` under `## [Unreleased]`. +8. **coding-agent**: default model id in `src/core/model-resolver.ts`, env var docs in `src/cli/args.ts`. ## License From 827fe1e2c48bcb6534807ce0eba5241fd5438fcb Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 11 Jun 2026 00:27:43 +0200 Subject: [PATCH 017/122] feat(ai): ImagesModels collections mirroring the chat-side design createImagesModels()/ImagesProvider/createImagesProvider() give image generation the same shape as chat: sync model reads, explicit async refresh(provider?) with in-flight dedupe, provider-resolved auth, and never-rejecting generateImages() (failures return AssistantImages with stopReason error). Auth resolution is shared with the chat side via the free-standing resolveProviderAuth() in auth/resolve.ts, which also owns ModelsError; both collections pass their store/context as arguments. The OpenRouter implementation moves to api/openrouter-images.ts with a lazy wrapper; openrouterImagesProvider() factory plus builtinImagesProviders()/builtinImagesModels() land in providers/all. The ImagesProvider id type alias is renamed to ImagesProviderId (mirror of Provider -> ProviderId). The old global image API (getImageModel*, generateImages, registerImagesApiProvider) stays on /compat, its registration shim repointed at the moved implementation. README: Quick Start uses builtinModels(), the full streaming event switch, image generation and the development checklist are restored in full, image generation documents the new collections with compat noted for the old API, plus the review fixes (builtinModels options, credential-store mention for browsers, ImagesModels notes). --- packages/agent/docs/models.md | 16 +- packages/ai/CHANGELOG.md | 1 + packages/ai/README.md | 201 ++++++++++++-- packages/ai/src/api/openrouter-images.lazy.ts | 10 + .../openrouter-images.ts} | 8 +- packages/ai/src/auth/resolve.ts | 117 ++++++++ packages/ai/src/auth/types.ts | 7 +- packages/ai/src/images-models.ts | 262 ++++++++++++++++++ packages/ai/src/index.ts | 1 + packages/ai/src/models.ts | 104 +------ packages/ai/src/providers/all.ts | 16 ++ .../src/providers/images/register-builtins.ts | 8 +- .../ai/src/providers/openrouter-images.ts | 14 + packages/ai/src/types.ts | 20 +- packages/ai/test/images-models.test.ts | 168 +++++++++++ 15 files changed, 804 insertions(+), 149 deletions(-) create mode 100644 packages/ai/src/api/openrouter-images.lazy.ts rename packages/ai/src/{providers/images/openrouter.ts => api/openrouter-images.ts} (95%) create mode 100644 packages/ai/src/auth/resolve.ts create mode 100644 packages/ai/src/images-models.ts create mode 100644 packages/ai/src/providers/openrouter-images.ts create mode 100644 packages/ai/test/images-models.test.ts diff --git a/packages/agent/docs/models.md b/packages/agent/docs/models.md index cfa524e0..05307be5 100644 --- a/packages/agent/docs/models.md +++ b/packages/agent/docs/models.md @@ -18,8 +18,8 @@ Non-goals for the immediate `pi-ai` pass: - Do not migrate coding-agent `ModelRegistry` yet. - Do not keep the stream/API registry inside `Models`. -- Do not implement web OAuth flows yet (the factory option is reserved). -- Images (`images.ts`, `images-api-registry.ts`) are out of scope; leave untouched. +- Do not implement web OAuth flows yet. +- Image generation mirrors the chat-side design (`ImagesModels`/`ImagesProvider` in `images-models.ts`); the old global image API (`images.ts`, `images-api-registry.ts`) lives on compat. ## Package layout @@ -28,9 +28,10 @@ Target source layout: ```txt packages/ai/src/ index.ts # core exports only; no built-in provider imports - models.ts # Models runtime, Provider, auth types + models.ts # Models runtime, Provider + images-models.ts # ImagesModels runtime, ImagesProvider (mirrors models.ts) compat.ts # temporary old-API compatibility entrypoint - auth/ # auth method types, helpers, login callbacks + auth/ # auth method types, helpers, shared resolveProviderAuth(), login callbacks api/ # API implementations and lazy wrappers openai-completions.ts # real implementation, imports SDKs, exports stream/streamSimple openai-completions.lazy.ts @@ -50,6 +51,8 @@ packages/ai/src/ mistral-conversations.lazy.ts bedrock-converse-stream.ts bedrock-converse-stream.lazy.ts + openrouter-images.ts # image-generation API implementation + openrouter-images.lazy.ts lazy.ts # lazyStream()/lazyApi() helpers (shared helpers: openai-responses-shared, google-shared, transform-messages, ...) providers/ # concrete provider factories and per-provider catalogs @@ -62,8 +65,9 @@ packages/ai/src/ google.ts google.models.ts ...one pair per built-in provider... + openrouter-images.ts # image-generation provider factory faux.ts # test provider factory - all.ts # explicit aggregate: builtinModels(), getBuiltin*() + all.ts # explicit aggregate: builtinModels(), builtinImagesModels(), getBuiltin*() utils/oauth/ # OAuth flow implementations (node), lazy-loaded ``` @@ -892,7 +896,7 @@ Ordering: ### Deferred / follow-ups - [ ] Web OAuth implementations (sitegeist-style) as an alternative `OAuthAuth`. -- [ ] Images API registry redesign (untouched in this pass). +- [x] Images API redesign: `ImagesModels`/`ImagesProvider`/`createImagesProvider` mirror the chat-side design (sync reads, explicit refresh, never-reject generation); auth resolution shared with the chat side via the free-standing `resolveProviderAuth()` in `auth/resolve.ts` (which also owns `ModelsError`; both collections pass their store/context as arguments — no resolver object). `openrouterImagesProvider()` factory + `builtinImagesProviders()`/`builtinImagesModels()` in `providers/all`; impl moved to `api/openrouter-images.ts` with a lazy wrapper. The old global image API (registry + `getImageModel*` + `generateImages`) stays on compat; `ImagesProvider` id alias in types.ts renamed to `ImagesProviderId` (mirror of `Provider` -> `ProviderId`). ## Error behavior diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 0c0d3bd6..c450e028 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -15,6 +15,7 @@ - One provider factory per built-in provider under `@earendil-works/pi-ai/providers/*` (e.g. `anthropicProvider()`, `openrouterProvider()`), plus `@earendil-works/pi-ai/providers/all` with `builtinProviders()`/`builtinModels()` and typed `getBuiltin*` catalog reads. Generated catalogs are split per provider, so importing one provider pulls one catalog; `sideEffects` metadata makes the package tree-shakeable. - OAuth flows (Anthropic, OpenAI Codex, GitHub Copilot) gained `OAuthAuth` adapters (`login`/`refresh`/`toAuth`) on unified `prompt()`/`notify()` login callbacks; Copilot's per-credential base URL is derived in `toAuth()`. - `fauxProvider()` returns a faux `Provider` for tests built on explicit `Models` collections. +- Image generation mirrors the chat-side design: `createImagesModels()`/`ImagesProvider`/`createImagesProvider()` with sync model reads, explicit `refresh()`, provider-resolved auth, and never-rejecting `generateImages()`; `openrouterImagesProvider()` factory plus `builtinImagesProviders()`/`builtinImagesModels()` in `providers/all`. The `ImagesProvider` id type alias is renamed to `ImagesProviderId`; the old global image API stays on `/compat`. - When Amazon Bedrock rejects an unsupported data retention mode, the error now links the AWS data retention documentation ([#5561](https://github.com/earendil-works/pi/pull/5561) by [@unexge](https://github.com/unexge)). diff --git a/packages/ai/README.md b/packages/ai/README.md index 72ab339b..735e6c18 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -93,14 +93,14 @@ TypeBox exports are re-exported from `@earendil-works/pi-ai`: `Type`, `Static`, ## Quick Start -You build a `Models` collection, register the providers you want, and stream through it. Importing a provider pulls only that provider's catalog; SDKs load lazily on first request. +You build a `Models` collection of providers and stream through it. The quickest start registers every built-in provider; apps that care about bundle size register individual providers instead (see [Provider Factories](#provider-factories)). Either way, provider SDKs load lazily on first request. ```typescript -import { Type, createModels, type Context, type Tool } from '@earendil-works/pi-ai'; -import { openaiProvider } from '@earendil-works/pi-ai/providers/openai'; +import { Type, type Context, type Tool } from '@earendil-works/pi-ai'; +import { builtinModels } from '@earendil-works/pi-ai/providers/all'; -const models = createModels(); -models.setProvider(openaiProvider()); +// A Models collection with every built-in provider registered +const models = builtinModels(); // Sync lookup against the collection const model = models.getModel('openai', 'gpt-4o-mini')!; @@ -130,12 +130,34 @@ for await (const event of s) { case 'start': console.log(`Starting with ${event.partial.model}`); break; + case 'text_start': + console.log('\n[Text started]'); + break; case 'text_delta': process.stdout.write(event.delta); break; + case 'text_end': + console.log('\n[Text ended]'); + break; + case 'thinking_start': + console.log('[Model is thinking...]'); + break; case 'thinking_delta': process.stdout.write(event.delta); break; + case 'thinking_end': + console.log('[Thinking complete]'); + break; + case 'toolcall_start': + console.log(`\n[Tool call started: index ${event.contentIndex}]`); + break; + case 'toolcall_delta': + // Partial tool arguments are being streamed + const partialCall = event.partial.content[event.contentIndex]; + if (partialCall.type === 'toolCall') { + console.log(`[Streaming args for ${partialCall.name}]`); + } + break; case 'toolcall_end': console.log(`\nTool called: ${event.toolCall.name}`); console.log(`Arguments: ${JSON.stringify(event.toolCall.arguments)}`); @@ -197,7 +219,7 @@ for (const block of response.content) { } ``` -Snippets in the rest of this README assume a `models` collection set up like this (with the relevant provider registered). +Snippets in the rest of this README assume a `models` collection set up like this (with the relevant providers registered). ## Providers and Models @@ -207,7 +229,7 @@ Providers internally share **API implementations** (the wire protocols): Anthrop ### Provider Factories -One factory per built-in provider, each a subpath import that pulls only that provider's catalog: +For apps that only need specific providers, there is one factory per built-in provider, each a subpath import that pulls only that provider's catalog: ```typescript import { anthropicProvider } from '@earendil-works/pi-ai/providers/anthropic'; @@ -225,7 +247,7 @@ Provider SDKs (`@anthropic-ai/sdk`, `openai`, `@google/genai`, AWS) are **not** ### All Built-in Providers -For apps that want everything: +For apps that want everything (as in Quick Start): ```typescript import { builtinModels } from '@earendil-works/pi-ai/providers/all'; @@ -233,7 +255,7 @@ import { builtinModels } from '@earendil-works/pi-ai/providers/all'; const models = builtinModels(); // a Models collection with every built-in provider registered ``` -This imports all catalogs (it is the heavy, explicit entrypoint) but still no SDKs. +This imports all catalogs (it is the heavy, explicit entrypoint) but still no SDKs. `builtinModels()` accepts the same options as `createModels()` (`credentials`, `authContext`); `builtinProviders()` returns the provider array if you want to register them on your own collection. ### Querying Models @@ -330,6 +352,8 @@ Stored credentials (API keys entered interactively, OAuth tokens) live in a `Cre import { createModels, type CredentialStore } from '@earendil-works/pi-ai'; const models = createModels({ credentials: myFileBackedStore }); +// builtinModels() takes the same options: +// const models = builtinModels({ credentials: myFileBackedStore }); ``` The contract is small: `read(providerId)`, `modify(providerId, fn)` (the only write path — a serialized read-modify-write), and `delete(providerId)`. OAuth token refresh runs inside `modify`, so concurrent requests and processes cannot double-refresh a rotated token. A stored credential *owns* its provider: environment variables are only consulted when nothing is stored, and a failed refresh never silently falls back to an env key. @@ -590,17 +614,21 @@ for (const block of response.content) { ## Image Generation -Image generation uses a separate API surface from text/chat generation and currently lives on the [compat entrypoint](#migrating-from-the-old-global-api). Use `getImageModel()` / `getImageModels()` / `getImageProviders()` to discover image-generation models, and `generateImages()` to get the final result. +Image generation uses a separate API surface from text/chat generation, mirroring the chat-side design: an `ImagesModels` collection holds `ImagesProvider`s, reads are sync, and auth resolves through the owning provider. Image generation is a one-shot API: `generateImages()` waits for the provider response and returns the final `AssistantImages` result — do not use the chat/stream APIs for it. + +### Basic Image Generation ```typescript -import { getImageModel, generateImages } from '@earendil-works/pi-ai/compat'; +import { builtinImagesModels } from '@earendil-works/pi-ai/providers/all'; -const model = getImageModel('openrouter', 'google/gemini-2.5-flash-image'); +// Every built-in image-generation provider; accepts the same options as createModels() +const imagesModels = builtinImagesModels(); -const result = await generateImages(model, { +const model = imagesModels.getModel('openrouter', 'google/gemini-2.5-flash-image')!; + +// Auth resolves through the provider (OPENROUTER_API_KEY here); explicit apiKey wins +const result = await imagesModels.generateImages(model, { input: [{ type: 'text', text: 'Generate a red circle on a plain white background.' }] -}, { - apiKey: process.env.OPENROUTER_API_KEY }); for (const block of result.output) { @@ -613,11 +641,52 @@ for (const block of result.output) { } ``` -Notes: +Like the chat side, you can build the collection from parts: `createImagesModels({ credentials?, authContext? })`, the `openrouterImagesProvider()` factory from `@earendil-works/pi-ai/providers/openrouter-images`, and `createImagesProvider({ id, auth, models, refreshModels?, api })` for custom image providers (with `imagesModels.refresh(provider?)` for dynamic lists). Failures never reject — they return an `AssistantImages` with `stopReason: "error"`. The collection's `getAuth(model)` works exactly like the chat-side one. -- Use `getImageModel(...)` and `generateImages()`; image-generation models do not work with the chat/stream APIs and do not participate in tool calling. -- Outputs are returned in `AssistantImages.output` and can include both base64-encoded `ImageContent` blocks and `TextContent` blocks. Check `model.output` and `model.input` for capabilities. -- Options such as `apiKey`, `signal`, `headers`, `onPayload`, and `onResponse` are supported; results may include `stopReason`, `responseId`, and `usage`. +The old global API (`getImageModel()` / `getImageModels()` / `getImageProviders()` / `generateImages()`) remains available on the [compat entrypoint](#migrating-from-the-old-global-api): + +```typescript +import { getImageModel, generateImages } from '@earendil-works/pi-ai/compat'; + +const model = getImageModel('openrouter', 'google/gemini-2.5-flash-image'); +const result = await generateImages(model, { + input: [{ type: 'text', text: 'Generate a red circle on a plain white background.' }] +}, { + apiKey: process.env.OPENROUTER_API_KEY +}); +``` + +Some models also support image input: + +```typescript +import { readFileSync } from 'fs'; + +const imageBuffer = readFileSync('input.png'); +const result = await imagesModels.generateImages(model, { + input: [ + { type: 'text', text: 'Create a variation of this image with a blue background.' }, + { type: 'image', data: imageBuffer.toString('base64'), mimeType: 'image/png' } + ] +}); +``` + +Check capabilities on the model metadata: + +```typescript +console.log(model.input); // ['text', 'image'] +console.log(model.output); // ['image'] or ['image', 'text'] +``` + +### Notes and Limitations + +- Image models live in `ImagesModels` collections, chat models in `Models` collections; the two are separate surfaces. +- Use `generateImages()`, not the chat/stream APIs. +- Image-generation models do not participate in tool calling. +- Outputs are returned in `AssistantImages.output` and can include both base64-encoded `ImageContent` blocks and `TextContent` blocks. +- Some models return only images, others return images plus text. Check `model.output`. +- Some models accept image input, others are text-to-image only. Check `model.input`. +- Like the streaming APIs, image generation supports options such as `apiKey`, `signal`, `headers`, `onPayload`, and `onResponse`, and results may include `stopReason`, `responseId`, and `usage`. +- If you want a model to analyze images in a conversation or call tools, use the regular chat APIs with a model that supports image input. - At the moment, image generation is available through only one provider, OpenRouter. ## Thinking/Reasoning @@ -1171,7 +1240,7 @@ Models are plain serializable data too — no functions or implementations attac ## Browser Usage -The library supports browser environments. The core entrypoint and provider factories are side-effect free and bundle cleanly. Pass API keys explicitly since environment variables are not available in browsers: +The library supports browser environments. The core entrypoint and provider factories are side-effect free and bundle cleanly. Environment variables are not available in browsers, so pass API keys explicitly — or inject a `CredentialStore` (e.g. localStorage-backed) and let provider auth resolve from stored credentials: ```typescript import { createModels } from '@earendil-works/pi-ai'; @@ -1312,16 +1381,90 @@ Compat is a strict superset of the root entrypoint, so a file can switch its imp ### Adding a New Provider -The layered layout: API implementations live in `src/api/`, provider factories in `src/providers/`, generated catalogs in `src/providers/.models.ts`. +Adding a new LLM provider requires changes across multiple files. The layered layout: API implementations live in `src/api/`, provider factories in `src/providers/`, generated catalogs in `src/providers/.models.ts`. This checklist covers all necessary steps: -1. **Core types** (`src/types.ts`): add the API id to `KnownApi` (if it is a new API), the provider id to `KnownProvider`, and the options type to `ApiOptionsMap`. -2. **API implementation** (`src/api/.ts`, only for a new API): export exactly `stream` and `streamSimple`, plus the options interface extending `StreamOptions`. Add a lazy wrapper `src/api/.lazy.ts` (`Api()` via `lazyApi()`). -3. **Catalog** (`scripts/generate-models.ts`): add fetching/mapping for the provider's models (e.g. from models.dev); regeneration emits `src/providers/.models.ts` and the aggregator. -4. **Provider factory** (`src/providers/.ts`): `createProvider()` wiring catalog + auth (`envApiKeyAuth` for standard key providers, custom `ApiKeyAuth` for ambient auth, `lazyOAuth` where OAuth exists) + the lazy API wrapper. Register it in `src/providers/all.ts`. -5. **Compat**: if it is a new API, register it in the builtin list in `src/compat.ts` and add the legacy subpath in `package.json` if warranted. -6. **Tests** (`test/`): cover streaming/tools/abort/tokens for new APIs (`stream.test.ts` and friends, env-gated), `cross-provider-handoff.test.ts` pairs, and provider listing/auth in `providers.test.ts`. -7. **Docs**: this README (Supported Providers, env var table) and `CHANGELOG.md` under `## [Unreleased]`. -8. **coding-agent**: default model id in `src/core/model-resolver.ts`, env var docs in `src/cli/args.ts`. +#### 1. Core Types (`src/types.ts`) + +- Add the API identifier to `KnownApi` (for example `"bedrock-converse-stream"`), if it is a new API +- Add the provider name to `KnownProvider` (for example `"amazon-bedrock"`) +- Add the options type to `ApiOptionsMap` + +#### 2. API Implementation (`src/api/.ts`, only for a new API) + +Create a new API implementation file (for example `bedrock-converse-stream.ts`) that exports exactly `stream` and `streamSimple`, plus: + +- An options interface extending `StreamOptions` (for example `BedrockOptions`) +- Message conversion functions to transform `Context` to provider format +- Tool conversion if the provider supports tools +- Response parsing to emit standardized events (`text`, `tool_call`, `thinking`, `usage`, `stop`) + +Add a lazy wrapper `src/api/.lazy.ts` (`Api()` via `lazyApi()`) so providers can reference the implementation without importing its SDK. Add any root-level `export type` re-exports in `src/index.ts` that should remain available from `@earendil-works/pi-ai`. + +#### 3. Model Generation (`scripts/generate-models.ts`, `scripts/generate-image-models.ts`) + +- Add logic to fetch and parse models from the provider's source (e.g., models.dev API) +- Map chat/tool-capable provider model data to the standardized `Model` interface via `scripts/generate-models.ts`; regeneration emits `src/providers/.models.ts` and the aggregator +- Map image-generation provider model data to the standardized `ImagesModel` interface via `scripts/generate-image-models.ts` +- Handle provider-specific quirks (pricing format, capability flags, model ID transformations) + +#### 4. Provider Factory (`src/providers/.ts`) + +- `createProvider()` wiring catalog + auth + the lazy API wrapper +- Auth: `envApiKeyAuth` for standard key providers, a custom `ApiKeyAuth` for ambient auth (AWS profiles, ADC), `lazyOAuth` where an OAuth flow exists +- Register the factory in `src/providers/all.ts` +- If it is a new API: register it in the builtin list in `src/compat.ts` and add the package subpath export in `package.json` + +#### 5. Tests (`test/`) + +Create or update test files to cover the new provider: + +- `stream.test.ts` - Basic streaming and tool use +- `tokens.test.ts` - Token usage reporting +- `abort.test.ts` - Request cancellation +- `empty.test.ts` - Empty message handling +- `context-overflow.test.ts` - Context limit errors +- `image-limits.test.ts` - Image support (if applicable) +- `unicode-surrogate.test.ts` - Unicode handling +- `tool-call-without-result.test.ts` - Orphaned tool calls +- `image-tool-result.test.ts` - Images in tool results +- `total-tokens.test.ts` - Token counting accuracy +- `cross-provider-handoff.test.ts` - Cross-provider context replay +- `providers.test.ts` - Provider listing and auth resolution + +For `cross-provider-handoff.test.ts`, add at least one provider/model pair. If the provider exposes multiple model families (for example GPT and Claude), add at least one pair per family. + +For providers with non-standard auth (AWS, Google Vertex), create a utility like `bedrock-utils.ts` with credential detection helpers. + +#### 6. Coding Agent Integration (`../coding-agent/`) + +Update `src/core/model-resolver.ts`: + +- Add a default model ID for the provider in `DEFAULT_MODELS` + +Update `src/cli/args.ts`: + +- Add environment variable documentation in the help text + +Update `README.md`: + +- Add the provider to the providers section with setup instructions + +#### 7. Documentation + +Update `packages/ai/README.md`: + +- Add to the Supported Providers table +- Document any provider-specific options or authentication requirements +- Add environment variable to the Environment Variables section + +#### 8. Changelog + +Add an entry to `packages/ai/CHANGELOG.md` under `## [Unreleased]`: + +```markdown +### Added +- Added support for [Provider Name] provider ([#PR](link) by [@author](link)) +``` ## License diff --git a/packages/ai/src/api/openrouter-images.lazy.ts b/packages/ai/src/api/openrouter-images.lazy.ts new file mode 100644 index 00000000..362d50a0 --- /dev/null +++ b/packages/ai/src/api/openrouter-images.lazy.ts @@ -0,0 +1,10 @@ +import type { ImagesModel, ProviderImages } from "../types.ts"; + +export const openrouterImagesApi = (): ProviderImages => ({ + generateImages: async (model, context, options) => + (await import("./openrouter-images.ts")).generateImages( + model as ImagesModel<"openrouter-images">, + context, + options, + ), +}); diff --git a/packages/ai/src/providers/images/openrouter.ts b/packages/ai/src/api/openrouter-images.ts similarity index 95% rename from packages/ai/src/providers/images/openrouter.ts rename to packages/ai/src/api/openrouter-images.ts index 54caeaf0..c96be7c8 100644 --- a/packages/ai/src/providers/images/openrouter.ts +++ b/packages/ai/src/api/openrouter-images.ts @@ -14,9 +14,9 @@ import type { ImagesModel, ImagesOptions, TextContent, -} from "../../types.ts"; -import { headersToRecord } from "../../utils/headers.ts"; -import { sanitizeSurrogates } from "../../utils/sanitize-unicode.ts"; +} from "../types.ts"; +import { headersToRecord } from "../utils/headers.ts"; +import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; interface OpenRouterGeneratedImage { image_url?: string | { url?: string }; @@ -34,7 +34,7 @@ type OpenRouterImageGenerationResponse = ChatCompletion & { choices: OpenRouterImageGenerationChoice[]; }; -export const generateImagesOpenRouter: ImagesFunction<"openrouter-images", ImagesOptions> = async ( +export const generateImages: ImagesFunction<"openrouter-images", ImagesOptions> = async ( model: ImagesModel<"openrouter-images">, context: ImagesContext, options?: ImagesOptions, diff --git a/packages/ai/src/auth/resolve.ts b/packages/ai/src/auth/resolve.ts new file mode 100644 index 00000000..3adec6b4 --- /dev/null +++ b/packages/ai/src/auth/resolve.ts @@ -0,0 +1,117 @@ +import type { Api, ImagesApi, ImagesModel, Model } from "../types.ts"; +import type { + ApiKeyAuth, + ApiKeyCredential, + AuthContext, + AuthResult, + Credential, + CredentialStore, + OAuthAuth, + OAuthCredential, + ProviderAuth, +} from "./types.ts"; + +export type ModelsErrorCode = "model_source" | "model_validation" | "provider" | "stream" | "auth" | "oauth"; + +export class ModelsError extends Error { + readonly code: ModelsErrorCode; + + constructor(code: ModelsErrorCode, message: string, options?: { cause?: unknown }) { + super(message, options); + this.name = "ModelsError"; + this.code = code; + } +} + +/** Model shape auth resolution receives: chat or image-generation models. */ +export type AuthModel = Model | ImagesModel; + +/** + * Auth resolution shared by the `Models` and `ImagesModels` collections. + * A stored credential owns the provider: ambient/env is consulted only when + * nothing is stored. No silent env fallback after a failed refresh or for a + * credential type without a matching handler. + */ +export async function resolveProviderAuth( + provider: { id: string; auth: ProviderAuth }, + model: AuthModel, + credentials: CredentialStore, + authContext: AuthContext, +): Promise { + const stored = await readCredential(credentials, provider.id); + if (stored) { + if (stored.type === "oauth" && provider.auth.oauth) { + return resolveStoredOAuth(credentials, provider.id, provider.auth.oauth, stored); + } + if (stored.type === "api-key" && provider.auth.apiKey) { + return resolveApiKey(authContext, provider.auth.apiKey, model, stored); + } + return undefined; + } + + // Ambient (env vars, AWS profiles, ADC files). + return provider.auth.apiKey ? resolveApiKey(authContext, provider.auth.apiKey, model, undefined) : undefined; +} + +/** + * OAuth resolution with double-checked locking (same pattern as today's + * AuthStorage): valid tokens cost zero locks; expired tokens lock, re-check + * expiry under the lock, refresh once globally, and persist the rotated + * credential before release. + */ +async function resolveStoredOAuth( + credentials: CredentialStore, + providerId: string, + oauth: OAuthAuth, + stored: OAuthCredential, +): Promise { + let credential = stored; + + if (Date.now() >= credential.expires) { + // Optimistic check said expired; the authoritative check runs under the lock. + let post: Credential | undefined; + try { + post = await credentials.modify(providerId, async (current) => { + if (current?.type !== "oauth") return undefined; // logged out meanwhile + if (Date.now() < current.expires) return undefined; // another process/request refreshed + try { + return await oauth.refresh(current); + } catch (error) { + throw new ModelsError("oauth", `OAuth refresh failed for ${providerId}`, { cause: error }); + } + }); + } catch (error) { + if (error instanceof ModelsError) throw error; + throw new ModelsError("auth", `Credential store modify failed for ${providerId}`, { cause: error }); + } + if (post?.type !== "oauth") return undefined; // logged out meanwhile + credential = post; + } + + try { + return { auth: await oauth.toAuth(credential), source: "OAuth" }; + } catch (error) { + throw new ModelsError("oauth", `OAuth auth derivation failed for ${providerId}`, { cause: error }); + } +} + +async function resolveApiKey( + authContext: AuthContext, + apiKey: ApiKeyAuth, + model: AuthModel, + credential: ApiKeyCredential | undefined, +): Promise { + try { + return await apiKey.resolve({ model, ctx: authContext, credential }); + } catch (error) { + throw new ModelsError("auth", `API key auth failed for provider ${model.provider}`, { cause: error }); + } +} + +async function readCredential(credentials: CredentialStore, providerId: string): Promise { + try { + return await credentials.read(providerId); + } catch (error) { + throw new ModelsError("auth", `Credential store read failed for ${providerId}`, { cause: error }); + } +} diff --git a/packages/ai/src/auth/types.ts b/packages/ai/src/auth/types.ts index b308b90d..74bb45bf 100644 --- a/packages/ai/src/auth/types.ts +++ b/packages/ai/src/auth/types.ts @@ -1,4 +1,4 @@ -import type { Api, Model } from "../types.ts"; +import type { Api, ImagesApi, ImagesModel, Model } from "../types.ts"; import type { OAuthCredentials } from "../utils/oauth/types.ts"; /** @@ -134,10 +134,11 @@ export interface ApiKeyAuth { /** * Resolve auth from the stored credential and/or ambient sources, merging * per field (`credential.key ?? env("...")`, `metadata.accountId ?? env("...")`). - * undefined = not configured. + * undefined = not configured. Receives the chat or image-generation model + * the request is for (both carry `provider` and `baseUrl`). */ resolve(input: { - model: Model; + model: Model | ImagesModel; ctx: AuthContext; credential?: ApiKeyCredential; }): Promise; diff --git a/packages/ai/src/images-models.ts b/packages/ai/src/images-models.ts new file mode 100644 index 00000000..67449b03 --- /dev/null +++ b/packages/ai/src/images-models.ts @@ -0,0 +1,262 @@ +import { defaultProviderAuthContext as defaultAuthContext } from "./auth/context.ts"; +import { InMemoryCredentialStore } from "./auth/credential-store.ts"; +import { ModelsError, resolveProviderAuth } from "./auth/resolve.ts"; +import type { AuthContext, AuthResult, CredentialStore, ProviderAuth } from "./auth/types.ts"; +import type { CreateModelsOptions } from "./models.ts"; +import type { AssistantImages, ImagesApi, ImagesContext, ImagesModel, ImagesOptions, ProviderImages } from "./types.ts"; + +/** + * An image-generation provider: the image-side counterpart of `Provider`. + * Owns id/name metadata, auth, model listing, and generation behavior. + */ +export interface ImagesProvider { + readonly id: string; + readonly name: string; + + /** + * Required: at least one of `apiKey`/`oauth`. Same semantics as chat + * providers; `ImagesModels.getAuth()` returns undefined when the provider + * is unconfigured. + */ + readonly auth: ProviderAuth; + + /** + * Current known models, sync. Static providers return their catalog; + * dynamic providers return the list as of the last `refreshModels()` + * (empty before the first). Must not throw; `ImagesModels` treats a + * throwing implementation as having no models. + */ + getModels(): readonly ImagesModel[]; + + /** + * Dynamic providers only: fetch and update the model list. May reject + * (network); on rejection the model list stays at its last-known state + * and a later call retries. + */ + refreshModels?(): Promise; + + generateImages( + model: ImagesModel, + context: ImagesContext, + options?: ImagesOptions, + ): Promise; +} + +/** + * Runtime collection of image-generation providers plus auth application and + * generation convenience: the image-side counterpart of `Models`. + */ +export interface ImagesModels { + getProviders(): readonly ImagesProvider[]; + getProvider(id: string): ImagesProvider | undefined; + + /** + * Sync read of last-known models from one provider or all providers. + * Best-effort: a provider whose `getModels()` throws yields no models. + */ + getModels(provider?: string): readonly ImagesModel[]; + + /** Sync runtime model lookup against last-known lists. */ + getModel(provider: string, id: string): ImagesModel | undefined; + + /** + * Ask dynamic providers to re-fetch their model lists. With a provider id, + * rejects with `ModelsError` ("model_source") on that provider's fetch + * failure; without one, refreshes all providers concurrently best-effort. + * Static providers (no `refreshModels`) are no-ops. + */ + refresh(provider?: string): Promise; + + /** + * Resolve request auth for an image model. Same contract as + * `Models.getAuth()`: undefined when unknown/unconfigured, rejects with + * `ModelsError` ("oauth"/"auth") on real failures. + */ + getAuth(model: ImagesModel): Promise; + + /** + * Generate images through the owning provider with auth resolved and + * merged (explicit options win per field). Never rejects; failures are + * returned as an `AssistantImages` with `stopReason: "error"`. + */ + generateImages( + model: ImagesModel, + context: ImagesContext, + options?: ImagesOptions, + ): Promise; +} + +export interface MutableImagesModels extends ImagesModels { + /** Upsert/replace by provider.id. Provider ids are unique. */ + setProvider(provider: ImagesProvider): void; + deleteProvider(id: string): void; + clearProviders(): void; +} + +class ImagesModelsImpl implements MutableImagesModels { + private providers = new Map(); + private credentials: CredentialStore; + private authContext: AuthContext; + + constructor(options?: CreateModelsOptions) { + this.credentials = options?.credentials ?? new InMemoryCredentialStore(); + this.authContext = options?.authContext ?? defaultAuthContext(); + } + + setProvider(provider: ImagesProvider): void { + this.providers.set(provider.id, provider); + } + + deleteProvider(id: string): void { + this.providers.delete(id); + } + + clearProviders(): void { + this.providers.clear(); + } + + getProviders(): readonly ImagesProvider[] { + return Array.from(this.providers.values()); + } + + getProvider(id: string): ImagesProvider | undefined { + return this.providers.get(id); + } + + getModels(provider?: string): readonly ImagesModel[] { + if (provider !== undefined) { + const entry = this.providers.get(provider); + if (!entry) return []; + try { + return entry.getModels(); + } catch { + return []; + } + } + + const models: ImagesModel[] = []; + for (const entry of this.providers.values()) { + try { + models.push(...entry.getModels()); + } catch { + // Best-effort: ill-behaved providers yield no models. + } + } + return models; + } + + getModel(provider: string, id: string): ImagesModel | undefined { + return this.getModels(provider).find((model) => model.id === id); + } + + async refresh(provider?: string): Promise { + if (provider !== undefined) { + const entry = this.providers.get(provider); + if (!entry?.refreshModels) return; + try { + await entry.refreshModels(); + } catch (error) { + if (error instanceof ModelsError) throw error; + throw new ModelsError("model_source", `Model refresh failed for ${provider}`, { cause: error }); + } + return; + } + + // Cannot reject: the async mapper turns even sync throws from ill-behaved + // providers into rejections, and allSettled captures all of them. + await Promise.allSettled(Array.from(this.providers.values(), async (entry) => entry.refreshModels?.())); + } + + async getAuth(model: ImagesModel): Promise { + const provider = this.providers.get(model.provider); + if (!provider) return undefined; + return resolveProviderAuth(provider, model, this.credentials, this.authContext); + } + + async generateImages( + model: ImagesModel, + context: ImagesContext, + options?: ImagesOptions, + ): Promise { + try { + const provider = this.providers.get(model.provider); + if (!provider) { + throw new ModelsError("provider", `Unknown provider: ${model.provider}`); + } + + const resolution = await this.getAuth(model); + const auth = resolution?.auth; + if (!auth) { + return provider.generateImages(model, context, options); + } + + const requestModel = auth.baseUrl ? { ...model, baseUrl: auth.baseUrl } : model; + + // Explicit request options win per-field; headers merge per header. + const apiKey = options?.apiKey ?? auth.apiKey; + const headers = auth.headers || options?.headers ? { ...auth.headers, ...options?.headers } : undefined; + + return await provider.generateImages(requestModel, context, { ...options, apiKey, headers }); + } catch (error) { + return { + api: model.api, + provider: model.provider, + model: model.id, + output: [], + stopReason: "error", + errorMessage: error instanceof Error ? error.message : String(error), + timestamp: Date.now(), + }; + } + } +} + +export function createImagesModels(options?: CreateModelsOptions): MutableImagesModels { + return new ImagesModelsImpl(options); +} + +export interface CreateImagesProviderOptions { + id: string; + /** Display name. Default: `id`. */ + name?: string; + /** Required — every provider has auth semantics, even ambient/keyless ones. */ + auth: ProviderAuth; + /** Initial model list (empty for purely dynamic providers). */ + models: readonly ImagesModel[]; + /** + * Dynamic providers: fetch the current list. Stored on success; concurrent + * calls share one in-flight fetch. May reject: the stored list then stays + * at its last-known state, the rejection propagates to the caller of + * `refreshModels()` (wrapped as ModelsError "model_source" by + * `ImagesModels.refresh(provider)`), and a later call retries. + */ + refreshModels?: () => Promise[]>; + api: ProviderImages; +} + +/** Builds an image-generation provider from parts. */ +export function createImagesProvider(input: CreateImagesProviderOptions): ImagesProvider { + let models = input.models; + let inflightRefresh: Promise | undefined; + const refreshModels = input.refreshModels; + + return { + id: input.id, + name: input.name ?? input.id, + auth: input.auth, + getModels: () => models, + refreshModels: refreshModels + ? () => { + inflightRefresh ??= (async () => { + try { + models = await refreshModels(); + } finally { + inflightRefresh = undefined; + } + })(); + return inflightRefresh; + } + : undefined, + generateImages: (model, context, options) => input.api.generateImages(model, context, options), + }; +} diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 53a6f719..6c3f9675 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -21,6 +21,7 @@ export * from "./auth/context.ts"; export * from "./auth/credential-store.ts"; export * from "./auth/helpers.ts"; export * from "./auth/types.ts"; +export * from "./images-models.ts"; export * from "./models.ts"; export * from "./providers/faux.ts"; export * from "./session-resources.ts"; diff --git a/packages/ai/src/models.ts b/packages/ai/src/models.ts index 37d33841..e540db69 100644 --- a/packages/ai/src/models.ts +++ b/packages/ai/src/models.ts @@ -1,17 +1,8 @@ import { lazyStream } from "./api/lazy.ts"; import { defaultProviderAuthContext as defaultAuthContext } from "./auth/context.ts"; import { InMemoryCredentialStore } from "./auth/credential-store.ts"; -import type { - ApiKeyAuth, - ApiKeyCredential, - AuthContext, - AuthResult, - Credential, - CredentialStore, - OAuthAuth, - OAuthCredential, - ProviderAuth, -} from "./auth/types.ts"; +import { ModelsError, resolveProviderAuth } from "./auth/resolve.ts"; +import type { AuthContext, AuthResult, CredentialStore, ProviderAuth } from "./auth/types.ts"; import type { Api, ApiStreamOptions, @@ -26,17 +17,7 @@ import type { Usage, } from "./types.ts"; -export type ModelsErrorCode = "model_source" | "model_validation" | "provider" | "stream" | "auth" | "oauth"; - -export class ModelsError extends Error { - readonly code: ModelsErrorCode; - - constructor(code: ModelsErrorCode, message: string, options?: { cause?: unknown }) { - super(message, options); - this.name = "ModelsError"; - this.code = code; - } -} +export { type AuthModel, ModelsError, type ModelsErrorCode } from "./auth/resolve.ts"; /** * A provider is the concrete runtime unit. It owns id/name/base metadata, @@ -234,84 +215,7 @@ class ModelsImpl implements MutableModels { async getAuth(model: Model): Promise { const provider = this.providers.get(model.provider); if (!provider) return undefined; - - // A stored credential owns the provider: ambient/env is consulted only - // when nothing is stored. No silent env fallback after a failed refresh - // or for a credential type without a matching handler. - const stored = await this.readCredential(provider.id); - if (stored) { - if (stored.type === "oauth" && provider.auth.oauth) { - return this.resolveOAuth(provider.id, provider.auth.oauth, stored); - } - if (stored.type === "api-key" && provider.auth.apiKey) { - return this.resolveApiKey(provider.auth.apiKey, model, stored); - } - return undefined; - } - - // Ambient (env vars, AWS profiles, ADC files). - return provider.auth.apiKey ? this.resolveApiKey(provider.auth.apiKey, model, undefined) : undefined; - } - - /** - * OAuth resolution with double-checked locking (same pattern as today's - * AuthStorage): valid tokens cost zero locks; expired tokens lock, - * re-check expiry under the lock, refresh once globally, and persist the - * rotated credential before release. - */ - private async resolveOAuth( - providerId: string, - oauth: OAuthAuth, - stored: OAuthCredential, - ): Promise { - let credential = stored; - - if (Date.now() >= credential.expires) { - // Optimistic check said expired; the authoritative check runs under the lock. - let post: Credential | undefined; - try { - post = await this.credentials.modify(providerId, async (current) => { - if (current?.type !== "oauth") return undefined; // logged out meanwhile - if (Date.now() < current.expires) return undefined; // another process/request refreshed - try { - return await oauth.refresh(current); - } catch (error) { - throw new ModelsError("oauth", `OAuth refresh failed for ${providerId}`, { cause: error }); - } - }); - } catch (error) { - if (error instanceof ModelsError) throw error; - throw new ModelsError("auth", `Credential store modify failed for ${providerId}`, { cause: error }); - } - if (post?.type !== "oauth") return undefined; // logged out meanwhile - credential = post; - } - - try { - return { auth: await oauth.toAuth(credential), source: "OAuth" }; - } catch (error) { - throw new ModelsError("oauth", `OAuth auth derivation failed for ${providerId}`, { cause: error }); - } - } - - private async resolveApiKey( - apiKey: ApiKeyAuth, - model: Model, - credential: ApiKeyCredential | undefined, - ): Promise { - try { - return await apiKey.resolve({ model, ctx: this.authContext, credential }); - } catch (error) { - throw new ModelsError("auth", `API key auth failed for provider ${model.provider}`, { cause: error }); - } - } - - private async readCredential(providerId: string): Promise { - try { - return await this.credentials.read(providerId); - } catch (error) { - throw new ModelsError("auth", `Credential store read failed for ${providerId}`, { cause: error }); - } + return resolveProviderAuth(provider, model, this.credentials, this.authContext); } private requireProvider(model: Model): Provider { diff --git a/packages/ai/src/providers/all.ts b/packages/ai/src/providers/all.ts index 1c4c70b1..85ba0301 100644 --- a/packages/ai/src/providers/all.ts +++ b/packages/ai/src/providers/all.ts @@ -1,3 +1,4 @@ +import { createImagesModels, type ImagesProvider, type MutableImagesModels } from "../images-models.ts"; import { MODELS } from "../models.generated.ts"; import { type CreateModelsOptions, createModels, type MutableModels, type Provider } from "../models.ts"; import type { Api, KnownProvider, Model } from "../types.ts"; @@ -27,6 +28,7 @@ import { openaiCodexProvider } from "./openai-codex.ts"; import { opencodeProvider } from "./opencode.ts"; import { opencodeGoProvider } from "./opencode-go.ts"; import { openrouterProvider } from "./openrouter.ts"; +import { openrouterImagesProvider } from "./openrouter-images.ts"; import { togetherProvider } from "./together.ts"; import { vercelAIGatewayProvider } from "./vercel-ai-gateway.ts"; import { xaiProvider } from "./xai.ts"; @@ -113,3 +115,17 @@ export function builtinModels(options?: CreateModelsOptions): MutableModels { } return models; } + +/** All built-in image-generation providers, freshly constructed. */ +export function builtinImagesProviders(): ImagesProvider[] { + return [openrouterImagesProvider()]; +} + +/** An `ImagesModels` collection with every built-in image-generation provider registered. */ +export function builtinImagesModels(options?: CreateModelsOptions): MutableImagesModels { + const models = createImagesModels(options); + for (const provider of builtinImagesProviders()) { + models.setProvider(provider); + } + return models; +} diff --git a/packages/ai/src/providers/images/register-builtins.ts b/packages/ai/src/providers/images/register-builtins.ts index e3decbb9..a5c901fa 100644 --- a/packages/ai/src/providers/images/register-builtins.ts +++ b/packages/ai/src/providers/images/register-builtins.ts @@ -1,9 +1,9 @@ +import type { generateImages as generateImagesOpenRouterFunction } from "../../api/openrouter-images.ts"; import { registerImagesApiProvider } from "../../images-api-registry.ts"; import type { AssistantImages, ImagesContext, ImagesFunction, ImagesModel, ImagesOptions } from "../../types.ts"; -import type { generateImagesOpenRouter as generateImagesOpenRouterFunction } from "./openrouter.ts"; interface OpenRouterImagesProviderModule { - generateImagesOpenRouter: typeof generateImagesOpenRouterFunction; + generateImages: typeof generateImagesOpenRouterFunction; } let openRouterImagesProviderModulePromise: Promise | undefined; @@ -21,7 +21,7 @@ function createLazyLoadErrorImages(model: ImagesModel<"openrouter-images">, erro } function loadOpenRouterImagesProviderModule(): Promise { - openRouterImagesProviderModulePromise ||= import("./openrouter.ts").then( + openRouterImagesProviderModulePromise ||= import("../../api/openrouter-images.ts").then( (module) => module as OpenRouterImagesProviderModule, ); return openRouterImagesProviderModulePromise; @@ -34,7 +34,7 @@ export const generateImagesOpenRouter: ImagesFunction<"openrouter-images", Image ) => { try { const module = await loadOpenRouterImagesProviderModule(); - return await module.generateImagesOpenRouter(model, context, options); + return await module.generateImages(model, context, options); } catch (error) { return createLazyLoadErrorImages(model, error); } diff --git a/packages/ai/src/providers/openrouter-images.ts b/packages/ai/src/providers/openrouter-images.ts new file mode 100644 index 00000000..7047cf0e --- /dev/null +++ b/packages/ai/src/providers/openrouter-images.ts @@ -0,0 +1,14 @@ +import { openrouterImagesApi } from "../api/openrouter-images.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { IMAGE_MODELS } from "../image-models.generated.ts"; +import { createImagesProvider, type ImagesProvider } from "../images-models.ts"; + +export function openrouterImagesProvider(): ImagesProvider { + return createImagesProvider({ + id: "openrouter", + name: "OpenRouter", + auth: { apiKey: envApiKeyAuth("OpenRouter API key", ["OPENROUTER_API_KEY"]) }, + models: Object.values(IMAGE_MODELS.openrouter), + api: openrouterImagesApi(), + }); +} diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index 2bce49bc..bbfbd5d6 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -69,7 +69,7 @@ export type ProviderId = KnownProvider | string; export type KnownImagesProvider = "openrouter"; -export type ImagesProvider = KnownImagesProvider | string; +export type ImagesProviderId = KnownImagesProvider | string; export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh"; export type ModelThinkingLevel = "off" | ThinkingLevel; @@ -204,6 +204,20 @@ export interface ProviderStreams { streamSimple(model: Model, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream; } +/** + * The uniform contract of an image-generation API implementation module: + * every image API module under `src/api/` exports exactly `generateImages`, + * so the module itself satisfies this interface. Lazy wrappers and image + * provider factories pass these around as values. + */ +export interface ProviderImages { + generateImages( + model: ImagesModel, + context: ImagesContext, + options?: ImagesOptions, + ): Promise; +} + export interface ImagesOptions { signal?: AbortSignal; apiKey?: string; @@ -370,7 +384,7 @@ export type ImagesStopReason = "stop" | "error" | "aborted"; export interface AssistantImages { api: ImagesApi; - provider: ImagesProvider; + provider: ImagesProviderId; model: string; output: ImagesOutputContent[]; responseId?: string; @@ -647,6 +661,6 @@ export interface Model { export interface ImagesModel extends Omit, "api" | "provider" | "reasoning" | "contextWindow" | "maxTokens" | "compat"> { api: TApi; - provider: ImagesProvider; + provider: ImagesProviderId; output: ("text" | "image")[]; } diff --git a/packages/ai/test/images-models.test.ts b/packages/ai/test/images-models.test.ts new file mode 100644 index 00000000..1b2e08da --- /dev/null +++ b/packages/ai/test/images-models.test.ts @@ -0,0 +1,168 @@ +import { describe, expect, it } from "vitest"; +import type { AuthContext } from "../src/auth/types.ts"; +import { createImagesModels, createImagesProvider, type ImagesProvider } from "../src/images-models.ts"; +import { builtinImagesModels } from "../src/providers/all.ts"; +import type { AssistantImages, ImagesApi, ImagesContext, ImagesModel, ImagesOptions } from "../src/types.ts"; + +function fakeAuthContext(env: Record): AuthContext { + return { + env: async (name) => env[name], + fileExists: async () => false, + }; +} + +function testImageModel(provider: string, id: string): ImagesModel { + return { + id, + name: id, + api: "test-images", + provider, + baseUrl: "https://example.test/v1", + input: ["text"], + output: ["image"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + }; +} + +function okResult(model: ImagesModel): AssistantImages { + return { + api: model.api, + provider: model.provider, + model: model.id, + output: [{ type: "image", data: "aGk=", mimeType: "image/png" }], + stopReason: "stop", + timestamp: Date.now(), + }; +} + +interface GenerateCall { + model: ImagesModel; + options: ImagesOptions | undefined; +} + +function testProvider(input: { + id: string; + models?: ImagesModel[]; + envVar?: string; + calls?: GenerateCall[]; +}): ImagesProvider { + return createImagesProvider({ + id: input.id, + auth: { + apiKey: { + name: "Test key", + resolve: async ({ ctx }) => { + if (!input.envVar) return { auth: {} }; + const key = await ctx.env(input.envVar); + return key ? { auth: { apiKey: key }, source: input.envVar } : undefined; + }, + }, + }, + models: input.models ?? [testImageModel(input.id, "model-a")], + api: { + generateImages: async (model, _context, options) => { + input.calls?.push({ model, options }); + return okResult(model); + }, + }, + }); +} + +const context: ImagesContext = { input: [{ type: "text", text: "a red circle" }] }; + +describe("ImagesModels", () => { + it("registers providers and reads models synchronously", () => { + const models = createImagesModels(); + models.setProvider(testProvider({ id: "p1", models: [testImageModel("p1", "m1"), testImageModel("p1", "m2")] })); + models.setProvider(testProvider({ id: "p2", models: [testImageModel("p2", "m3")] })); + + expect(models.getProviders().map((p) => p.id)).toEqual(["p1", "p2"]); + expect(models.getModels().map((m) => m.id)).toEqual(["m1", "m2", "m3"]); + expect(models.getModels("p1").map((m) => m.id)).toEqual(["m1", "m2"]); + expect(models.getModel("p2", "m3")?.id).toBe("m3"); + expect(models.getModel("p2", "missing")).toBeUndefined(); + + models.deleteProvider("p1"); + expect(models.getProvider("p1")).toBeUndefined(); + }); + + it("resolves auth through the provider and merges it into requests; explicit options win", async () => { + const calls: GenerateCall[] = []; + const models = createImagesModels({ authContext: fakeAuthContext({ TEST_KEY: "env-key" }) }); + models.setProvider(testProvider({ id: "p1", envVar: "TEST_KEY", calls })); + const model = models.getModel("p1", "model-a")!; + + expect((await models.getAuth(model))?.auth.apiKey).toBe("env-key"); + + const result = await models.generateImages(model, context); + expect(result.stopReason).toBe("stop"); + expect(calls[0].options?.apiKey).toBe("env-key"); + + await models.generateImages(model, context, { apiKey: "explicit" }); + expect(calls[1].options?.apiKey).toBe("explicit"); + }); + + it("returns an error result for unknown providers and unconfigured auth rejections", async () => { + const models = createImagesModels({ authContext: fakeAuthContext({}) }); + const ghost = await models.generateImages(testImageModel("ghost", "m"), context); + expect(ghost.stopReason).toBe("error"); + expect(ghost.errorMessage).toContain("Unknown provider: ghost"); + + // unconfigured (resolve -> undefined) still dispatches; provider decides what to do + const calls: GenerateCall[] = []; + models.setProvider(testProvider({ id: "p1", envVar: "MISSING", calls })); + const model = models.getModel("p1", "model-a")!; + expect(await models.getAuth(model)).toBeUndefined(); + await models.generateImages(model, context); + expect(calls[0].options?.apiKey).toBeUndefined(); + }); + + it("supports dynamic providers via refresh with in-flight dedupe", async () => { + let fetches = 0; + const provider = createImagesProvider({ + id: "dyn", + auth: { apiKey: { name: "Test", resolve: async () => ({ auth: {} }) } }, + models: [], + refreshModels: async () => { + fetches++; + await new Promise((resolve) => setTimeout(resolve, 5)); + return [testImageModel("dyn", "listed")]; + }, + api: { generateImages: async (model) => okResult(model) }, + }); + const models = createImagesModels(); + models.setProvider(provider); + + expect(models.getModels("dyn")).toEqual([]); + await Promise.all([models.refresh("dyn"), models.refresh("dyn")]); + expect(fetches).toBe(1); + expect(models.getModel("dyn", "listed")).toBeDefined(); + + // failures reject with ModelsError for a single provider + models.setProvider( + createImagesProvider({ + id: "flaky", + auth: { apiKey: { name: "Test", resolve: async () => ({ auth: {} }) } }, + models: [], + refreshModels: async () => { + throw new Error("fetch failed"); + }, + api: { generateImages: async (model) => okResult(model) }, + }), + ); + await expect(models.refresh("flaky")).rejects.toMatchObject({ code: "model_source" }); + await expect(models.refresh()).resolves.toBeUndefined(); + }); + + it("builtinImagesModels registers the openrouter provider with its catalog", async () => { + const models = builtinImagesModels({ authContext: fakeAuthContext({ OPENROUTER_API_KEY: "or-key" }) }); + const providers = models.getProviders(); + expect(providers.map((p) => p.id)).toEqual(["openrouter"]); + + const list = models.getModels("openrouter"); + expect(list.length).toBeGreaterThan(0); + expect(list.every((m) => m.api === "openrouter-images")).toBe(true); + + expect((await models.getAuth(list[0]))?.auth.apiKey).toBe("or-key"); + }); +}); From ebdf9b65e722076f52539170f398b70923185335 Mon Sep 17 00:00:00 2001 From: Sviatoslav Abakumov Date: Tue, 16 Jun 2026 00:20:04 +0400 Subject: [PATCH 018/122] fix(coding-agent): sort threaded sessions by latest activity in the subtree --- .../components/session-selector.ts | 20 ++++++++-- .../test/session-selector-path-delete.test.ts | 39 +++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/src/modes/interactive/components/session-selector.ts b/packages/coding-agent/src/modes/interactive/components/session-selector.ts index 74141e5e..77b5a2ba 100644 --- a/packages/coding-agent/src/modes/interactive/components/session-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/session-selector.ts @@ -190,6 +190,7 @@ class SessionSelectorHeader implements Component { interface SessionTreeNode { session: SessionInfo; children: SessionTreeNode[]; + latestActivity: number; } /** Flattened node for display with tree structure info */ @@ -210,7 +211,7 @@ function buildSessionTree(sessions: SessionInfo[]): SessionTreeNode[] { for (const session of sessions) { const sessionPath = canonicalizePath(session.path) ?? session.path; - byPath.set(sessionPath, { session, children: [] }); + byPath.set(sessionPath, { session, children: [], latestActivity: session.modified.getTime() }); } const roots: SessionTreeNode[] = []; @@ -227,9 +228,22 @@ function buildSessionTree(sessions: SessionInfo[]): SessionTreeNode[] { } } - // Sort children and roots by modified date (descending) + const updateLatestActivity = (node: SessionTreeNode): number => { + let latestActivity = node.session.modified.getTime(); + for (const child of node.children) { + latestActivity = Math.max(latestActivity, updateLatestActivity(child)); + } + node.latestActivity = latestActivity; + return latestActivity; + }; + + for (const root of roots) { + updateLatestActivity(root); + } + + // Sort children and roots by latest activity in each subtree (descending) const sortNodes = (nodes: SessionTreeNode[]): void => { - nodes.sort((a, b) => b.session.modified.getTime() - a.session.modified.getTime()); + nodes.sort((a, b) => b.latestActivity - a.latestActivity); for (const node of nodes) { sortNodes(node.children); } diff --git a/packages/coding-agent/test/session-selector-path-delete.test.ts b/packages/coding-agent/test/session-selector-path-delete.test.ts index 52cae486..cb702d5e 100644 --- a/packages/coding-agent/test/session-selector-path-delete.test.ts +++ b/packages/coding-agent/test/session-selector-path-delete.test.ts @@ -282,6 +282,45 @@ describe("session selector path/delete interactions", () => { expect(output).toContain("└─ Child"); }); + it("sorts threaded sessions by latest activity in their subtree", async () => { + const parentOne = makeSession({ + id: "parent-one", + name: "Parent one", + modified: new Date("2026-01-02T00:00:00.000Z"), + }); + const parentTwo = makeSession({ + id: "parent-two", + name: "Parent two", + modified: new Date("2026-01-01T00:00:00.000Z"), + }); + const childTwo = makeSession({ + id: "child-two", + name: "Child two", + parentSessionPath: parentTwo.path, + modified: new Date("2026-01-03T00:00:00.000Z"), + }); + + const selector = new SessionSelectorComponent( + async () => [parentOne, parentTwo, childTwo], + async () => [], + () => {}, + () => {}, + () => {}, + () => {}, + { keybindings }, + ); + await flushPromises(); + + const output = stripAnsi(selector.render(120).join("\n")); + const parentTwoIndex = output.indexOf("Parent two"); + const childTwoIndex = output.indexOf("└─ Child two"); + const parentOneIndex = output.indexOf("Parent one"); + + expect(parentTwoIndex).toBeGreaterThanOrEqual(0); + expect(childTwoIndex).toBeGreaterThan(parentTwoIndex); + expect(parentOneIndex).toBeGreaterThan(childTwoIndex); + }); + it("treats the current session as active across symlink aliases", async () => { const paths = createSymlinkedSessionPaths(); tempDirs.push(paths.baseDir); From bc93655e1c07627d40b32c978d3208eff688dafb Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 16:26:01 +0200 Subject: [PATCH 019/122] meta: Added report template --- .github/ISSUE_TEMPLATE/package-report.yml | 51 +++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/package-report.yml diff --git a/.github/ISSUE_TEMPLATE/package-report.yml b/.github/ISSUE_TEMPLATE/package-report.yml new file mode 100644 index 00000000..c2b4e76a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/package-report.yml @@ -0,0 +1,51 @@ +name: Package Report +description: Report a problematic Pi package listed on pi.dev +labels: ["package-report"] +body: + - type: markdown + attributes: + value: | + Use this form to report a package listed on pi.dev. For Pi core bugs, use the bug report template instead. + + New issues from new contributors are auto-closed by default. Maintainers review auto-closed issues daily. Issues that do not meet the quality bar in [CONTRIBUTING.md](https://github.com/earendil-works/pi-mono/blob/main/CONTRIBUTING.md) will not be reopened or receive a reply. + + Keep this short. If it doesn't fit on one screen, it's too long. Write in your own voice. + + - type: input + id: package-name + attributes: + label: Package name + description: The npm package name from pi.dev. + placeholder: "@scope/package" + validations: + required: true + + - type: input + id: package-version + attributes: + label: Version + description: The package version shown on pi.dev. + placeholder: "0.1.0" + validations: + required: false + + - type: dropdown + id: report-type + attributes: + label: What are you reporting? + options: + - Malicious or unsafe behavior + - Broken package or install instructions + - Misleading metadata or impersonation + - Spam or low-quality package + - Other + validations: + required: true + + - type: textarea + id: details + attributes: + label: Details + description: Describe the concern and include links, logs, or screenshots if helpful. + validations: + required: true From 908be616f27ac2f97f8049feb94f3d21de6cb7b6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 17:12:03 +0200 Subject: [PATCH 020/122] ref: Remove some options from package reporting --- .github/ISSUE_TEMPLATE/package-report.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/package-report.yml b/.github/ISSUE_TEMPLATE/package-report.yml index c2b4e76a..846e25ee 100644 --- a/.github/ISSUE_TEMPLATE/package-report.yml +++ b/.github/ISSUE_TEMPLATE/package-report.yml @@ -35,10 +35,8 @@ body: label: What are you reporting? options: - Malicious or unsafe behavior - - Broken package or install instructions - - Misleading metadata or impersonation - - Spam or low-quality package - - Other + - Impersonation + - Trademark / TOS Violations validations: required: true From d0b46764b1dde621ce89932f92f6e59a4c286133 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 17:13:47 +0200 Subject: [PATCH 021/122] feat(coding-agent): add automatic theme mode (#5874) --- packages/coding-agent/docs/themes.md | 2 +- .../coding-agent/src/core/settings-manager.ts | 9 +- .../components/settings-selector.ts | 279 ++++++++++++++++-- .../src/modes/interactive/interactive-mode.ts | 71 ++--- .../interactive/theme/theme-controller.ts | 135 +++++++++ .../modes/interactive/theme/theme-schema.json | 3 +- .../src/modes/interactive/theme/theme.ts | 43 ++- .../test/interactive-mode-status.test.ts | 13 + .../test/settings-manager.test.ts | 18 ++ ...-signal-shutdown-extension-cleanup.test.ts | 2 + .../5724-sigterm-signal-exit.test.ts | 2 + .../coding-agent/test/theme-detection.test.ts | 12 + packages/tui/src/index.ts | 7 +- packages/tui/src/terminal-colors.ts | 11 + packages/tui/src/tui.ts | 75 ++++- packages/tui/test/terminal-colors.test.ts | 18 +- 16 files changed, 620 insertions(+), 80 deletions(-) create mode 100644 packages/coding-agent/src/modes/interactive/theme/theme-controller.ts diff --git a/packages/coding-agent/docs/themes.md b/packages/coding-agent/docs/themes.md index c18e954b..11655128 100644 --- a/packages/coding-agent/docs/themes.md +++ b/packages/coding-agent/docs/themes.md @@ -137,7 +137,7 @@ vim ~/.pi/agent/themes/my-theme.json } ``` -- `name` is required and must be unique. +- `name` is required, must be unique, and must not contain `/`. - `vars` is optional. Define reusable colors here, then reference them in `colors`. - `colors` must define all 51 required tokens. diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index 6b54a187..a90916a4 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -714,8 +714,15 @@ export class SettingsManager { this.save(); } + getThemeSetting(): string | undefined { + const value = this.settings.theme; + if (typeof value === "string") return value; + return undefined; + } + getTheme(): string | undefined { - return this.settings.theme; + const theme = this.getThemeSetting(); + return theme?.includes("/") ? undefined : theme; } setTheme(theme: string): void { diff --git a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts index 39d25f80..7cc92614 100644 --- a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts @@ -1,6 +1,7 @@ import type { ThinkingLevel } from "@earendil-works/pi-agent-core"; import type { Transport } from "@earendil-works/pi-ai"; import { + type Component, Container, getCapabilities, type SelectItem, @@ -13,7 +14,13 @@ import { } from "@earendil-works/pi-tui"; import { formatHttpIdleTimeoutMs, HTTP_IDLE_TIMEOUT_CHOICES } from "../../../core/http-dispatcher.ts"; import type { DefaultProjectTrust, WarningSettings } from "../../../core/settings-manager.ts"; -import { getSelectListTheme, getSettingsListTheme, theme } from "../theme/theme.ts"; +import { + getSelectListTheme, + getSettingsListTheme, + parseAutoThemeSetting, + type TerminalTheme, + theme, +} from "../theme/theme.ts"; import { DynamicBorder } from "./dynamic-border.ts"; import { keyDisplayText } from "./keybinding-hints.ts"; @@ -55,6 +62,7 @@ export interface SettingsConfig { thinkingLevel: ThinkingLevel; availableThinkingLevels: ThinkingLevel[]; currentTheme: string; + terminalTheme: TerminalTheme; availableThemes: string[]; hideThinkingBlock: boolean; collapseChangelog: boolean; @@ -210,6 +218,249 @@ class SelectSubmenu extends Container { } } +function themeItems(availableThemes: string[]): SelectItem[] { + return availableThemes.map((name) => ({ value: name, label: name })); +} + +const AUTOMATIC_THEME_VALUE = "/"; + +function singleModeThemeItems(availableThemes: string[]): SelectItem[] { + return [ + { + value: AUTOMATIC_THEME_VALUE, + label: "Automatic", + description: "Use separate themes for light and dark terminal appearance", + }, + ...themeItems(availableThemes), + ]; +} + +function preferredTheme(availableThemes: string[], preferred: string | undefined, fallback: string): string { + if (preferred && availableThemes.includes(preferred)) return preferred; + if (availableThemes.includes(fallback)) return fallback; + return availableThemes[0] ?? fallback; +} + +function defaultAutomaticThemes( + currentThemeSetting: string, + availableThemes: string[], +): { lightTheme: string; darkTheme: string } { + const autoTheme = parseAutoThemeSetting(currentThemeSetting); + if (autoTheme) return autoTheme; + + const currentFixedTheme = currentThemeSetting.includes("/") ? undefined : currentThemeSetting; + const themeName = preferredTheme(availableThemes, currentFixedTheme, "dark"); + return { lightTheme: themeName, darkTheme: themeName }; +} + +class ThemeSubmenu extends Container { + private inputComponent: Component | undefined; + private readonly callbacks: SettingsCallbacks; + private readonly availableThemes: string[]; + private readonly terminalTheme: TerminalTheme; + private readonly onDone: (selectedValue?: string) => void; + private readonly originalThemeSetting: string; + private mode: "single" | "automatic"; + private singleTheme: string; + private lightTheme: string; + private darkTheme: string; + + constructor( + currentThemeSetting: string, + terminalTheme: TerminalTheme, + availableThemes: string[], + callbacks: SettingsCallbacks, + onDone: (selectedValue?: string) => void, + ) { + super(); + this.callbacks = callbacks; + this.availableThemes = availableThemes; + this.terminalTheme = terminalTheme; + this.onDone = onDone; + this.originalThemeSetting = currentThemeSetting; + const autoTheme = parseAutoThemeSetting(currentThemeSetting); + const automaticThemes = defaultAutomaticThemes(currentThemeSetting, availableThemes); + const fixedTheme = autoTheme || currentThemeSetting.includes("/") ? undefined : currentThemeSetting; + this.mode = autoTheme ? "automatic" : "single"; + this.lightTheme = automaticThemes.lightTheme; + this.darkTheme = automaticThemes.darkTheme; + this.singleTheme = preferredTheme( + availableThemes, + fixedTheme ?? (autoTheme ? this.getActiveAutomaticTheme() : undefined), + "dark", + ); + + if (this.mode === "automatic") { + this.showAutomaticMenu(); + } else { + this.showSingleMenu(); + } + } + + handleInput(data: string): void { + this.inputComponent?.handleInput?.(data); + } + + private setContent(renderComponent: Component, inputComponent: Component = renderComponent): void { + this.clear(); + this.addChild(renderComponent); + this.inputComponent = inputComponent; + } + + private showSingleMenu(): void { + this.mode = "single"; + const menu = new SelectSubmenu( + "Theme", + "Select a theme, or choose Automatic to follow terminal appearance.", + singleModeThemeItems(this.availableThemes), + this.singleTheme, + (value) => { + if (value === AUTOMATIC_THEME_VALUE) { + this.mode = "automatic"; + this.callbacks.onThemePreview?.(this.getThemeSetting()); + this.showAutomaticMenu(); + return; + } + + this.singleTheme = value; + this.apply(value); + }, + () => this.cancel(), + (value) => { + this.callbacks.onThemePreview?.(value === AUTOMATIC_THEME_VALUE ? this.getAutomaticThemeSetting() : value); + }, + ); + this.setContent(menu); + } + + private showAutomaticMenu(): void { + this.mode = "automatic"; + const content = new Container(); + content.addChild(new Text(theme.bold(theme.fg("accent", "Automatic Theme")), 0, 0)); + content.addChild(new Spacer(1)); + content.addChild(new Text(theme.fg("muted", "Choose themes for terminal light and dark appearance."), 0, 0)); + content.addChild(new Text(theme.fg("muted", "Light/dark detection requires terminal support."), 0, 0)); + content.addChild(new Spacer(1)); + + const items: SettingItem[] = [ + { + id: "light-theme", + label: "Light theme", + description: "Theme to use in automatic mode when the terminal is light", + currentValue: this.lightTheme, + submenu: (currentValue, done) => + this.createThemeSelect( + "Light Theme", + "Select the theme to use for light terminal appearance", + currentValue, + done, + (value) => { + this.lightTheme = value; + this.callbacks.onThemePreview?.(this.getThemeSetting()); + done(value); + }, + ), + }, + { + id: "dark-theme", + label: "Dark theme", + description: "Theme to use in automatic mode when the terminal is dark", + currentValue: this.darkTheme, + submenu: (currentValue, done) => + this.createThemeSelect( + "Dark Theme", + "Select the theme to use for dark terminal appearance", + currentValue, + done, + (value) => { + this.darkTheme = value; + this.callbacks.onThemePreview?.(this.getThemeSetting()); + done(value); + }, + ), + }, + { + id: "apply", + label: "Apply", + description: "Save and go back", + currentValue: "save and go back", + values: ["save and go back"], + }, + { + id: "single-mode", + label: "Change mode", + description: "Switch to one theme for light and dark", + currentValue: "switch to single theme", + values: ["switch to single theme"], + }, + ]; + + const settingsList = new SettingsList( + items, + Math.min(items.length, 10), + getSettingsListTheme(), + (id) => { + switch (id) { + case "single-mode": + this.mode = "single"; + this.singleTheme = this.getActiveAutomaticTheme(); + this.callbacks.onThemePreview?.(this.singleTheme); + this.showSingleMenu(); + break; + case "apply": + this.apply(this.getAutomaticThemeSetting()); + break; + } + }, + () => this.cancel(), + ); + content.addChild(settingsList); + this.setContent(content, settingsList); + } + + private createThemeSelect( + title: string, + description: string, + currentValue: string, + done: (selectedValue?: string) => void, + onSelect: (value: string) => void, + ): SelectSubmenu { + return new SelectSubmenu( + title, + description, + themeItems(this.availableThemes), + currentValue, + onSelect, + () => { + this.callbacks.onThemePreview?.(this.getThemeSetting()); + done(); + }, + (value) => this.callbacks.onThemePreview?.(value), + ); + } + + private getThemeSetting(): string { + return this.mode === "automatic" ? this.getAutomaticThemeSetting() : this.singleTheme; + } + + private getActiveAutomaticTheme(): string { + return this.terminalTheme === "light" ? this.lightTheme : this.darkTheme; + } + + private getAutomaticThemeSetting(): string { + return `${this.lightTheme}/${this.darkTheme}`; + } + + private apply(themeSetting: string): void { + this.onDone(themeSetting); + } + + private cancel(): void { + this.callbacks.onThemePreview?.(this.originalThemeSetting); + this.onDone(); + } +} + /** * Main settings selector component. */ @@ -353,28 +604,7 @@ export class SettingsSelectorComponent extends Container { description: "Color theme for the interface", currentValue: config.currentTheme, submenu: (currentValue, done) => - new SelectSubmenu( - "Theme", - "Select color theme", - config.availableThemes.map((t) => ({ - value: t, - label: t, - })), - currentValue, - (value) => { - callbacks.onThemeChange(value); - done(value); - }, - () => { - // Restore original theme on cancel - callbacks.onThemePreview?.(currentValue); - done(); - }, - (value) => { - // Preview theme on selection change - callbacks.onThemePreview?.(value); - }, - ), + new ThemeSubmenu(currentValue, config.terminalTheme, config.availableThemes, callbacks, done), }, ]; @@ -561,6 +791,9 @@ export class SettingsSelectorComponent extends Container { case "terminal-progress": callbacks.onShowTerminalProgressChange(newValue === "true"); break; + case "theme": + callbacks.onThemeChange(newValue); + break; } }, callbacks.onCancel, diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index ad9d3d3a..0cde5ee4 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -128,22 +128,19 @@ import { UserMessageComponent } from "./components/user-message.ts"; import { UserMessageSelectorComponent } from "./components/user-message-selector.ts"; import { getModelSearchText } from "./model-search.ts"; import { - detectTerminalBackgroundTheme, getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, - initTheme, onThemeChange, setRegisteredThemes, - setTheme, - setThemeInstance, stopThemeWatcher, Theme, type ThemeColor, theme, } from "./theme/theme.ts"; +import { InteractiveThemeController } from "./theme/theme-controller.ts"; /** Interface for components that can be expanded/collapsed */ interface Expandable { @@ -374,6 +371,7 @@ export class InteractiveMode { private options: InteractiveModeOptions; private autoTrustOnReloadCwd: string | undefined; + private themeController: InteractiveThemeController; // Convenience accessors private get session(): AgentSession { @@ -428,26 +426,12 @@ export class InteractiveMode { // Register themes from resource loader and initialize setRegisteredThemes(this.session.resourceLoader.getThemes().themes); - initTheme(this.settingsManager.getTheme(), true); - } - - private async detectThemeIfUnset(): Promise { - if (this.settingsManager.getTheme()) { - return; - } - - const detection = await detectTerminalBackgroundTheme({ ui: this.ui, timeoutMs: 100 }); - const result = setTheme(detection.theme, true); - if (!result.success) { - return; - } - - if (detection.confidence === "high") { - this.settingsManager.setTheme(detection.theme); - await this.settingsManager.flush(); - } - this.updateEditorBorderColor(); - this.ui.requestRender(); + this.themeController = new InteractiveThemeController( + this.ui, + this.settingsManager, + (message) => this.showError(message), + () => this.updateEditorBorderColor(), + ); } private getAutocompleteSourceTag(sourceInfo?: SourceInfo): string | undefined { @@ -672,7 +656,7 @@ export class InteractiveMode { this.ui.start(); this.isInitialized = true; - await this.detectThemeIfUnset(); + await this.themeController.applyFromSettings(); // Add header with keybindings from config (unless silenced) if (this.options.verbose || !this.settingsManager.getQuietStartup()) { @@ -2080,16 +2064,13 @@ export class InteractiveMode { getTheme: (name) => getThemeByName(name), setTheme: (themeOrName) => { if (themeOrName instanceof Theme) { - setThemeInstance(themeOrName); - this.ui.requestRender(); - return { success: true }; + return this.themeController.setThemeInstance(themeOrName); } - const result = setTheme(themeOrName, true); + const result = this.themeController.setThemeName(themeOrName); if (result.success) { if (this.settingsManager.getTheme() !== themeOrName) { this.settingsManager.setTheme(themeOrName); } - this.ui.requestRender(); } return result; }, @@ -3378,6 +3359,7 @@ export class InteractiveMode { // which the stdout/stderr error handler turns into emergencyTerminalExit; // the render loop is already idle, so this cannot hot-spin (see #4144). await this.runtimeHost.dispose(); + this.themeController.disableAutoSync(); await this.ui.terminal.drainInput(1000); this.stop(); process.exit(0); @@ -3388,6 +3370,7 @@ export class InteractiveMode { // the final frame while the process is exiting. // Drain any in-flight Kitty key release events before stopping. // This prevents escape sequences from leaking to the parent shell over slow SSH. + this.themeController.disableAutoSync(); await this.ui.terminal.drainInput(1000); this.stop(); @@ -3991,7 +3974,8 @@ export class InteractiveMode { httpIdleTimeoutMs: this.settingsManager.getHttpIdleTimeoutMs(), thinkingLevel: this.session.thinkingLevel, availableThinkingLevels: this.session.getAvailableThinkingLevels(), - currentTheme: this.settingsManager.getTheme() || "dark", + currentTheme: this.settingsManager.getThemeSetting() || "dark", + terminalTheme: this.themeController.getTerminalTheme(), availableThemes: getAvailableThemes(), hideThinkingBlock: this.hideThinkingBlock, collapseChangelog: this.settingsManager.getCollapseChangelog(), @@ -4058,21 +4042,11 @@ export class InteractiveMode { this.footer.invalidate(); this.updateEditorBorderColor(); }, - onThemeChange: (themeName) => { - const result = setTheme(themeName, true); - this.settingsManager.setTheme(themeName); - this.ui.invalidate(); - if (!result.success) { - this.showError(`Failed to load theme "${themeName}": ${result.error}\nFell back to dark theme.`); - } - }, - onThemePreview: (themeName) => { - const result = setTheme(themeName, true); - if (result.success) { - this.ui.invalidate(); - this.ui.requestRender(); - } + onThemeChange: (themeSetting) => { + this.settingsManager.setTheme(themeSetting); + void this.themeController.applyFromSettings(); }, + onThemePreview: (themeName) => this.themeController.preview(themeName), onHideThinkingBlockChange: (hidden) => { this.hideThinkingBlock = hidden; this.settingsManager.setHideThinkingBlock(hidden); @@ -5109,11 +5083,7 @@ export class InteractiveMode { } setRegisteredThemes(this.session.resourceLoader.getThemes().themes); this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock(); - const themeName = this.settingsManager.getTheme(); - const themeResult = themeName ? setTheme(themeName, true) : { success: true }; - if (!themeResult.success) { - this.showError(`Failed to load theme "${themeName}": ${themeResult.error}\nFell back to dark theme.`); - } + await this.themeController.applyFromSettings(); const editorPaddingX = this.settingsManager.getEditorPaddingX(); const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible(); this.defaultEditor.setPaddingX(editorPaddingX); @@ -5754,6 +5724,7 @@ export class InteractiveMode { this.loadingAnimation.stop(); this.loadingAnimation = undefined; } + this.themeController.disableAutoSync(); this.clearExtensionTerminalInputListeners(); this.footer.dispose(); this.footerDataProvider.dispose(); diff --git a/packages/coding-agent/src/modes/interactive/theme/theme-controller.ts b/packages/coding-agent/src/modes/interactive/theme/theme-controller.ts new file mode 100644 index 00000000..9fe9e8cc --- /dev/null +++ b/packages/coding-agent/src/modes/interactive/theme/theme-controller.ts @@ -0,0 +1,135 @@ +import type { TUI } from "@earendil-works/pi-tui"; +import type { SettingsManager } from "../../../core/settings-manager.ts"; +import { + detectTerminalBackgroundFromEnv, + detectTerminalBackgroundTheme, + initTheme, + parseAutoThemeSetting, + resolveThemeSetting, + setTheme, + setThemeInstance, + type TerminalTheme, + type Theme, +} from "./theme.ts"; + +type ThemeResult = { success: boolean; error?: string }; + +export class InteractiveThemeController { + private readonly ui: TUI; + private readonly settingsManager: SettingsManager; + private readonly showError: (message: string) => void; + private readonly onChanged: () => void; + private terminalTheme: TerminalTheme = detectTerminalBackgroundFromEnv().theme; + private activeThemeName: string | undefined; + private autoSyncEnabled = false; + + constructor(ui: TUI, settingsManager: SettingsManager, showError: (message: string) => void, onChanged: () => void) { + this.ui = ui; + this.settingsManager = settingsManager; + this.showError = showError; + this.onChanged = onChanged; + this.activeThemeName = resolveThemeSetting(this.settingsManager.getThemeSetting(), this.terminalTheme); + initTheme(this.activeThemeName, true); + this.ui.onTerminalColorSchemeChange((terminalTheme) => this.applyTerminalTheme(terminalTheme)); + } + + async applyFromSettings(): Promise { + const themeSetting = this.settingsManager.getThemeSetting(); + const autoTheme = parseAutoThemeSetting(themeSetting); + if (autoTheme) { + this.terminalTheme = await this.detectTerminalThemeForAuto(); + this.setAutoSync(true); + this.applyThemeName(this.terminalTheme === "light" ? autoTheme.lightTheme : autoTheme.darkTheme, true); + return; + } + + this.setAutoSync(false); + if (themeSetting !== undefined) { + this.applyThemeName(themeSetting, true); + return; + } + + const detection = await detectTerminalBackgroundTheme({ ui: this.ui, timeoutMs: 100 }); + this.terminalTheme = detection.theme; + if (!this.applyThemeName(detection.theme).success) return; + if (detection.confidence === "high") { + this.settingsManager.setTheme(detection.theme); + await this.settingsManager.flush(); + } + } + + setThemeName(themeName: string, showError = false): ThemeResult { + this.setAutoSync(false); + return this.applyThemeName(themeName, showError); + } + + setThemeInstance(themeInstance: Theme): ThemeResult { + this.setAutoSync(false); + setThemeInstance(themeInstance); + this.activeThemeName = ""; + this.notifyChanged(); + return { success: true }; + } + + preview(themeSettingOrName: string): void { + const themeName = resolveThemeSetting(themeSettingOrName, this.terminalTheme) ?? this.activeThemeName; + if (!themeName) return; + if (setTheme(themeName, true).success) { + this.ui.invalidate(); + this.ui.requestRender(); + } + } + + disableAutoSync(): void { + this.setAutoSync(false); + } + + getTerminalTheme(): TerminalTheme { + return this.terminalTheme; + } + + private applyThemeName(themeName: string, showError = false): ThemeResult { + const result = setTheme(themeName, true); + this.activeThemeName = result.success ? themeName : "dark"; + this.notifyChanged(); + if (!result.success && showError) { + this.showError(`Failed to load theme "${themeName}": ${result.error}\nFell back to dark theme.`); + } + return result; + } + + private notifyChanged(): void { + this.ui.invalidate(); + this.onChanged(); + } + + private setAutoSync(enabled: boolean): void { + if (this.autoSyncEnabled === enabled) return; + this.autoSyncEnabled = enabled; + this.ui.setTerminalColorSchemeNotifications(enabled); + } + + private async detectTerminalThemeForAuto(): Promise { + try { + const colorScheme = await this.ui.queryTerminalColorScheme({ timeoutMs: 100 }); + if (colorScheme) return colorScheme; + } catch { + // Fall back to OSC 11 / COLORFGBG detection when color-scheme DSR is unsupported. + } + return (await detectTerminalBackgroundTheme({ ui: this.ui, timeoutMs: 100 })).theme; + } + + private applyTerminalTheme(terminalTheme: TerminalTheme): void { + if (!this.autoSyncEnabled) return; + this.terminalTheme = terminalTheme; + const autoTheme = parseAutoThemeSetting(this.settingsManager.getThemeSetting()); + if (!autoTheme) { + this.setAutoSync(false); + return; + } + const themeName = terminalTheme === "light" ? autoTheme.lightTheme : autoTheme.darkTheme; + if (themeName !== this.activeThemeName) { + this.applyThemeName(themeName); + } + } +} diff --git a/packages/coding-agent/src/modes/interactive/theme/theme-schema.json b/packages/coding-agent/src/modes/interactive/theme/theme-schema.json index 7bc495da..9d94a12a 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme-schema.json +++ b/packages/coding-agent/src/modes/interactive/theme/theme-schema.json @@ -11,7 +11,8 @@ }, "name": { "type": "string", - "description": "Theme name" + "pattern": "^[^/]+$", + "description": "Theme name. Must not contain '/' because it is reserved for automatic light/dark theme settings." }, "vars": { "type": "object", diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index 778f8028..58e5aac3 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -503,6 +503,14 @@ function getCustomThemeInfos(): ThemeInfo[] { return result; } +function assertThemeNameIsValid(name: string): void { + if (name.includes("/")) { + throw new Error( + `Invalid theme name "${name}": theme names cannot contain "/" because it is reserved for automatic light/dark theme settings.`, + ); + } +} + function parseThemeJson(label: string, json: unknown): ThemeJson { if (!validateThemeJson.Check(json)) { const errors = Array.from(validateThemeJson.Errors(json)); @@ -539,7 +547,9 @@ function parseThemeJson(label: string, json: unknown): ThemeJson { throw new Error(errorMessage); } - return json as ThemeJson; + const themeJson = json as ThemeJson; + assertThemeNameIsValid(themeJson.name); + return themeJson; } function parseThemeJsonContent(label: string, content: string): ThemeJson { @@ -625,6 +635,36 @@ export function getThemeByName(name: string): Theme | undefined { export type TerminalTheme = "dark" | "light"; +export function parseAutoThemeSetting( + themeSetting: string | undefined, +): { lightTheme: string; darkTheme: string } | undefined { + if (!themeSetting) return undefined; + const slashIndex = themeSetting.indexOf("/"); + if (slashIndex === -1 || themeSetting.indexOf("/", slashIndex + 1) !== -1) { + return undefined; + } + + const lightTheme = themeSetting.slice(0, slashIndex).trim(); + const darkTheme = themeSetting.slice(slashIndex + 1).trim(); + if (!lightTheme || !darkTheme) { + return undefined; + } + return { lightTheme, darkTheme }; +} + +export function resolveThemeSetting( + themeSetting: string | undefined, + terminalTheme: TerminalTheme, +): string | undefined { + const autoTheme = parseAutoThemeSetting(themeSetting); + if (autoTheme) { + return terminalTheme === "light" ? autoTheme.lightTheme : autoTheme.darkTheme; + } + if (themeSetting?.includes("/")) return undefined; + if (typeof themeSetting === "string") return themeSetting; + return undefined; +} + export interface TerminalThemeDetection { theme: TerminalTheme; source: "terminal background" | "COLORFGBG" | "fallback"; @@ -752,6 +792,7 @@ export function setRegisteredThemes(themes: Theme[]): void { registeredThemes.clear(); for (const theme of themes) { if (theme.name) { + assertThemeNameIsValid(theme.name); registeredThemes.set(theme.name, theme); } } diff --git a/packages/coding-agent/test/interactive-mode-status.test.ts b/packages/coding-agent/test/interactive-mode-status.test.ts index 8f1018b2..6ba7db91 100644 --- a/packages/coding-agent/test/interactive-mode-status.test.ts +++ b/packages/coding-agent/test/interactive-mode-status.test.ts @@ -151,6 +151,13 @@ describe("InteractiveMode.createExtensionUIContext setTheme", () => { const fakeThis: any = { session: { settingsManager }, settingsManager, + themeController: { + setThemeInstance: vi.fn(() => ({ success: true })), + setThemeName: vi.fn(() => { + fakeThis.ui.requestRender(); + return { success: true }; + }), + }, ui: { requestRender: vi.fn() }, }; @@ -158,6 +165,7 @@ describe("InteractiveMode.createExtensionUIContext setTheme", () => { const result = uiContext.setTheme("light"); expect(result.success).toBe(true); + expect(fakeThis.themeController.setThemeName).toHaveBeenCalledWith("light"); expect(settingsManager.setTheme).toHaveBeenCalledWith("light"); expect(currentTheme).toBe("light"); expect(fakeThis.ui.requestRender).toHaveBeenCalledTimes(1); @@ -173,6 +181,10 @@ describe("InteractiveMode.createExtensionUIContext setTheme", () => { const fakeThis: any = { session: { settingsManager }, settingsManager, + themeController: { + setThemeInstance: vi.fn(() => ({ success: true })), + setThemeName: vi.fn(() => ({ success: false, error: "Theme not found" })), + }, ui: { requestRender: vi.fn() }, }; @@ -180,6 +192,7 @@ describe("InteractiveMode.createExtensionUIContext setTheme", () => { const result = uiContext.setTheme("__missing_theme__"); expect(result.success).toBe(false); + expect(fakeThis.themeController.setThemeName).toHaveBeenCalledWith("__missing_theme__"); expect(settingsManager.setTheme).not.toHaveBeenCalled(); expect(fakeThis.ui.requestRender).not.toHaveBeenCalled(); }); diff --git a/packages/coding-agent/test/settings-manager.test.ts b/packages/coding-agent/test/settings-manager.test.ts index 279bece1..d86c3f91 100644 --- a/packages/coding-agent/test/settings-manager.test.ts +++ b/packages/coding-agent/test/settings-manager.test.ts @@ -198,6 +198,24 @@ describe("SettingsManager", () => { }); }); + describe("theme setting", () => { + it("stores slash-separated automatic theme settings separately from fixed theme names", async () => { + const settingsPath = join(agentDir, "settings.json"); + writeFileSync(settingsPath, JSON.stringify({ theme: "light/dark" })); + + const manager = SettingsManager.create(projectDir, agentDir); + + expect(manager.getTheme()).toBeUndefined(); + expect(manager.getThemeSetting()).toBe("light/dark"); + + manager.setTheme("solarized-light/tokyo-night"); + await manager.flush(); + + const savedSettings = JSON.parse(readFileSync(settingsPath, "utf-8")); + expect(savedSettings.theme).toBe("solarized-light/tokyo-night"); + }); + }); + describe("error tracking", () => { it("should collect and clear load errors via drainErrors", () => { const globalSettingsPath = join(agentDir, "settings.json"); diff --git a/packages/coding-agent/test/suite/regressions/5080-signal-shutdown-extension-cleanup.test.ts b/packages/coding-agent/test/suite/regressions/5080-signal-shutdown-extension-cleanup.test.ts index 33a960e2..9ccf77cd 100644 --- a/packages/coding-agent/test/suite/regressions/5080-signal-shutdown-extension-cleanup.test.ts +++ b/packages/coding-agent/test/suite/regressions/5080-signal-shutdown-extension-cleanup.test.ts @@ -21,6 +21,7 @@ type ShutdownThis = { unregisterSignalHandlers: () => void; runtimeHost: { dispose: () => Promise }; ui: { terminal: { drainInput: (ms: number) => Promise } }; + themeController: { disableAutoSync: () => void }; stop: () => void; sessionManager: SessionManager; }; @@ -81,6 +82,7 @@ function createContext(order: string[], sessionManager = createSessionManager()) }), }, }, + themeController: { disableAutoSync: vi.fn() }, stop: vi.fn(() => { order.push("stop"); }), diff --git a/packages/coding-agent/test/suite/regressions/5724-sigterm-signal-exit.test.ts b/packages/coding-agent/test/suite/regressions/5724-sigterm-signal-exit.test.ts index d170f1cf..75e82fb7 100644 --- a/packages/coding-agent/test/suite/regressions/5724-sigterm-signal-exit.test.ts +++ b/packages/coding-agent/test/suite/regressions/5724-sigterm-signal-exit.test.ts @@ -13,6 +13,7 @@ type ShutdownThis = { unregisterSignalHandlers: () => void; runtimeHost: { dispose: () => Promise }; ui: { terminal: { drainInput: (ms: number) => Promise } }; + themeController: { disableAutoSync: () => void }; stop: () => void; }; @@ -73,6 +74,7 @@ describe("InteractiveMode SIGTERM shutdown with signal-exit (#5724)", () => { }), }, }, + themeController: { disableAutoSync: vi.fn() }, stop: vi.fn(() => { order.push("stop"); }), diff --git a/packages/coding-agent/test/theme-detection.test.ts b/packages/coding-agent/test/theme-detection.test.ts index 4ad0cf56..6bdc597b 100644 --- a/packages/coding-agent/test/theme-detection.test.ts +++ b/packages/coding-agent/test/theme-detection.test.ts @@ -5,6 +5,8 @@ import { detectTerminalBackgroundTheme, getThemeByName, getThemeForRgbColor, + parseAutoThemeSetting, + resolveThemeSetting, } from "../src/modes/interactive/theme/theme.ts"; afterEach(() => { @@ -119,3 +121,13 @@ describe("theme detection from RGB", () => { expect(getThemeForRgbColor({ r: 250, g: 250, b: 250 })).toBe("light"); }); }); + +describe("theme setting helpers", () => { + it("parses and resolves automatic theme settings", () => { + expect(parseAutoThemeSetting("light/dark")).toEqual({ lightTheme: "light", darkTheme: "dark" }); + expect(resolveThemeSetting("dark", "light")).toBe("dark"); + expect(resolveThemeSetting("light/dark", "light")).toBe("light"); + expect(resolveThemeSetting("light/dark", "dark")).toBe("dark"); + expect(resolveThemeSetting("light/dark/extra", "dark")).toBeUndefined(); + }); +}); diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index e22bf08a..4e76b107 100644 --- a/packages/tui/src/index.ts +++ b/packages/tui/src/index.ts @@ -62,7 +62,12 @@ export { StdinBuffer, type StdinBufferEventMap, type StdinBufferOptions } from " // Terminal interface and implementations export { ProcessTerminal, type Terminal } from "./terminal.ts"; // Terminal colors -export { parseOsc11BackgroundColor, type RgbColor } from "./terminal-colors.ts"; +export { + parseOsc11BackgroundColor, + parseTerminalColorSchemeReport, + type RgbColor, + type TerminalColorScheme, +} from "./terminal-colors.ts"; // Terminal image support export { allocateImageId, diff --git a/packages/tui/src/terminal-colors.ts b/packages/tui/src/terminal-colors.ts index 1c383a51..cff6dc8e 100644 --- a/packages/tui/src/terminal-colors.ts +++ b/packages/tui/src/terminal-colors.ts @@ -4,6 +4,8 @@ export interface RgbColor { b: number; } +export type TerminalColorScheme = "dark" | "light"; + function hexToRgb(hex: string): RgbColor { const normalized = hex.startsWith("#") ? hex.slice(1) : hex; const r = parseInt(normalized.slice(0, 2), 16); @@ -24,6 +26,7 @@ function parseOscHexChannel(channel: string): number | undefined { } const OSC11_BACKGROUND_COLOR_RESPONSE_PATTERN = /^\x1b\]11;([^\x07\x1b]*)(?:\x07|\x1b\\)$/i; +const COLOR_SCHEME_REPORT_PATTERN = /^\x1b\[\?997;(1|2)n$/; export function isOsc11BackgroundColorResponse(data: string): boolean { return OSC11_BACKGROUND_COLOR_RESPONSE_PATTERN.test(data); @@ -60,3 +63,11 @@ export function parseOsc11BackgroundColor(data: string): RgbColor | undefined { const b = parseOscHexChannel(blue); return r !== undefined && g !== undefined && b !== undefined ? { r, g, b } : undefined; } + +export function parseTerminalColorSchemeReport(data: string): TerminalColorScheme | undefined { + const match = data.match(COLOR_SCHEME_REPORT_PATTERN); + if (!match) { + return undefined; + } + return match[1] === "2" ? "light" : "dark"; +} diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index 1fff6545..a7054888 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -8,7 +8,13 @@ import * as path from "node:path"; import { performance } from "node:perf_hooks"; import { isKeyRelease, matchesKey } from "./keys.ts"; import type { Terminal } from "./terminal.ts"; -import { isOsc11BackgroundColorResponse, parseOsc11BackgroundColor, type RgbColor } from "./terminal-colors.ts"; +import { + isOsc11BackgroundColorResponse, + parseOsc11BackgroundColor, + parseTerminalColorSchemeReport, + type RgbColor, + type TerminalColorScheme, +} from "./terminal-colors.ts"; import { deleteKittyImage, getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.ts"; import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.ts"; @@ -311,6 +317,8 @@ export class TUI extends Container { private stopped = false; private pendingOsc11BackgroundReplies = 0; private pendingOsc11BackgroundQueries: PendingOsc11BackgroundQuery[] = []; + private terminalColorSchemeListeners = new Set<(scheme: TerminalColorScheme) => void>(); + private terminalColorSchemeNotificationsEnabled = false; // Overlay stack for modal components rendered on top of base content private focusOrderCounter = 0; @@ -631,6 +639,9 @@ export class TUI extends Container { () => this.requestRender(), ); this.terminal.hideCursor(); + if (this.terminalColorSchemeNotificationsEnabled) { + this.terminal.write("\x1b[?2031h"); + } this.queryCellSize(); this.requestRender(); } @@ -646,6 +657,23 @@ export class TUI extends Container { this.inputListeners.delete(listener); } + onTerminalColorSchemeChange(listener: (scheme: TerminalColorScheme) => void): () => void { + this.terminalColorSchemeListeners.add(listener); + return () => { + this.terminalColorSchemeListeners.delete(listener); + }; + } + + setTerminalColorSchemeNotifications(enabled: boolean): void { + if (this.terminalColorSchemeNotificationsEnabled === enabled) { + return; + } + this.terminalColorSchemeNotificationsEnabled = enabled; + if (!this.stopped) { + this.terminal.write(enabled ? "\x1b[?2031h" : "\x1b[?2031l"); + } + } + private queryCellSize(): void { // Only query if terminal supports images (cell size is only used for image rendering) if (!getCapabilities().images) { @@ -662,6 +690,9 @@ export class TUI extends Container { clearTimeout(this.renderTimer); this.renderTimer = undefined; } + if (this.terminalColorSchemeNotificationsEnabled) { + this.terminal.write("\x1b[?2031l"); + } // Move cursor to the end of the content to prevent overwriting/artifacts on exit if (this.previousLines.length > 0) { const targetRow = this.previousLines.length; // Line after the last content @@ -731,6 +762,9 @@ export class TUI extends Container { if (this.consumeOsc11BackgroundResponse(data)) { return; } + if (this.consumeTerminalColorSchemeReport(data)) { + return; + } if (this.inputListeners.size > 0) { let current = data; @@ -824,6 +858,18 @@ export class TUI extends Container { return true; } + private consumeTerminalColorSchemeReport(data: string): boolean { + const scheme = parseTerminalColorSchemeReport(data); + if (!scheme) { + return false; + } + + for (const listener of this.terminalColorSchemeListeners) { + listener(scheme); + } + return true; + } + private consumeCellSizeResponse(data: string): boolean { // Response format: ESC [ 6 ; height ; width t const match = data.match(/^\x1b\[6;(\d+);(\d+)t$/); @@ -1638,4 +1684,31 @@ export class TUI extends Container { this.terminal.write("\x1b]11;?\x07"); }); } + + /** + * Query the terminal's color-scheme preference with DSR (`CSI ? 996 n`). + * Terminals that support the color palette notification protocol reply with + * `CSI ? 997 ; 1 n` for dark or `CSI ? 997 ; 2 n` for light. + */ + queryTerminalColorScheme({ timeoutMs }: { timeoutMs: number }): Promise { + return new Promise((resolve) => { + let settled = false; + let timer: NodeJS.Timeout | undefined; + let unsubscribe: () => void = () => {}; + const settle = (scheme: TerminalColorScheme | undefined) => { + if (settled) return; + settled = true; + if (timer) { + clearTimeout(timer); + timer = undefined; + } + unsubscribe(); + resolve(scheme); + }; + + unsubscribe = this.onTerminalColorSchemeChange(settle); + timer = setTimeout(() => settle(undefined), timeoutMs); + this.terminal.write("\x1b[?996n"); + }); + } } diff --git a/packages/tui/test/terminal-colors.test.ts b/packages/tui/test/terminal-colors.test.ts index 9ac2a4ee..d777e061 100644 --- a/packages/tui/test/terminal-colors.test.ts +++ b/packages/tui/test/terminal-colors.test.ts @@ -1,6 +1,12 @@ import assert from "node:assert"; import { describe, it } from "node:test"; -import { type Component, parseOsc11BackgroundColor, type Terminal, TUI } from "../src/index.ts"; +import { + type Component, + parseOsc11BackgroundColor, + parseTerminalColorSchemeReport, + type Terminal, + TUI, +} from "../src/index.ts"; class TestTerminal implements Terminal { private inputHandler?: (data: string) => void; @@ -104,6 +110,16 @@ describe("parseOsc11BackgroundColor", () => { }); }); +describe("parseTerminalColorSchemeReport", () => { + it("parses color scheme reports", () => { + assert.strictEqual(parseTerminalColorSchemeReport("\x1b[?997;1n"), "dark"); + assert.strictEqual(parseTerminalColorSchemeReport("\x1b[?997;2n"), "light"); + assert.strictEqual(parseTerminalColorSchemeReport("\x1b[?997;3n"), undefined); + assert.strictEqual(parseTerminalColorSchemeReport("\x1b[?996n"), undefined); + assert.strictEqual(parseTerminalColorSchemeReport("x\x1b[?997;1n"), undefined); + }); +}); + describe("TUI.queryTerminalBackgroundColor", () => { it("writes OSC 11 query and resolves with the parsed RGB reply", async () => { const terminal = new TestTerminal(); From 2b46f388635f61a3fab2eed241998a61d67e7753 Mon Sep 17 00:00:00 2001 From: Alexey Zaytsev Date: Thu, 18 Jun 2026 10:14:39 -0500 Subject: [PATCH 022/122] feat(coding-agent): Expose edit-diff for extensions (#5756) --- packages/coding-agent/src/core/tools/edit-diff.ts | 3 +-- packages/coding-agent/src/index.ts | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/src/core/tools/edit-diff.ts b/packages/coding-agent/src/core/tools/edit-diff.ts index f280bf19..cc94c202 100644 --- a/packages/coding-agent/src/core/tools/edit-diff.ts +++ b/packages/coding-agent/src/core/tools/edit-diff.ts @@ -1,6 +1,5 @@ /** - * Shared diff computation utilities for the edit tool. - * Used by both edit.ts (for execution) and tool-execution.ts (for preview rendering). + * Shared diff computation utilities for the edit and similar tools. */ import * as Diff from "diff"; diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 70416af1..5830ecda 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -246,6 +246,7 @@ export { type SkillFrontmatter, } from "./core/skills.ts"; export { createSyntheticSourceInfo } from "./core/source-info.ts"; +export { type EditDiffResult, generateDiffString, generateUnifiedPatch } from "./core/tools/edit-diff.ts"; // Tools export { type BashOperations, From aae62dfa87c7935f124c4349fb34216eee84502a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 18:09:03 +0200 Subject: [PATCH 023/122] feat(coding-agent): make bare update self-only --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/README.md | 8 +-- packages/coding-agent/docs/packages.md | 9 ++-- packages/coding-agent/docs/usage.md | 5 +- packages/coding-agent/src/cli/args.ts | 2 +- .../src/modes/interactive/interactive-mode.ts | 2 +- .../coding-agent/src/package-manager-cli.ts | 51 +++++++++++++++---- 7 files changed, 58 insertions(+), 20 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a2fd43e8..e29e19d2 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,7 @@ ### Changed +- Changed bare `pi update` to update only pi, added `pi update --all` for updating pi and extensions together, and clarified extension update prompts. - Updated extension docs, examples, runtime help, trust prompts, and config labels to use the configured project config directory instead of hardcoded `.pi` paths. ### Fixed diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 41a7bbc2..ce7dba2a 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -419,7 +419,8 @@ pi install ssh://git@github.com/user/repo@v1 # tag or commit pi remove npm:@foo/pi-tools pi uninstall npm:@foo/pi-tools # alias for remove pi list -pi update # update pi and packages (skips pinned packages) +pi update # update pi only +pi update --all # update pi and packages pi update --extensions # update packages only pi update --self # update pi only pi update --self --force # reinstall pi even if current @@ -427,7 +428,7 @@ pi update npm:@foo/pi-tools # update one package pi config # enable/disable extensions, skills, prompts, themes ``` -Packages install to `~/.pi/agent/git/` (git) or `~/.pi/agent/npm/` (npm). Use `-l` for project-local installs (`.pi/git/`, `.pi/npm/`). Git `@ref` values are pinned tags or commits; pinned packages are skipped by `pi update`, so use `pi install git:host/user/repo@new-ref` to move an existing package to a new ref. Git packages install dependencies with `npm install --omit=dev` by default, so runtime deps must be listed under `dependencies`; when `npmCommand` is configured, git packages use plain `install` for compatibility with wrappers. If you use a Node version manager and want package installs to reuse a stable npm context, set `npmCommand` in `settings.json`, for example `["mise", "exec", "node@20", "--", "npm"]`. +Packages install to `~/.pi/agent/git/` (git) or `~/.pi/agent/npm/` (npm). Use `-l` for project-local installs (`.pi/git/`, `.pi/npm/`). Git `@ref` values are pinned tags or commits; pinned packages are skipped by `pi update --extensions` and `pi update --all`, so use `pi install git:host/user/repo@new-ref` to move an existing package to a new ref. Git packages install dependencies with `npm install --omit=dev` by default, so runtime deps must be listed under `dependencies`; when `npmCommand` is configured, git packages use plain `install` for compatibility with wrappers. If you use a Node version manager and want package installs to reuse a stable npm context, set `npmCommand` in `settings.json`, for example `["mise", "exec", "node@20", "--", "npm"]`. Create a package by adding a `pi` key to `package.json`: @@ -518,7 +519,8 @@ pi [options] [@files...] [messages...] pi install [-l] # Install package, -l for project-local pi remove [-l] # Remove package pi uninstall [-l] # Alias for remove -pi update [source|self|pi] # Update pi and packages (skips pinned packages) +pi update [source|self|pi] # Update pi only, or one package source +pi update --all # Update pi and packages pi update --extensions # Update packages only pi update --self # Update pi only pi update --self --force # Reinstall pi even if current diff --git a/packages/coding-agent/docs/packages.md b/packages/coding-agent/docs/packages.md index 7009b773..73f2dccb 100644 --- a/packages/coding-agent/docs/packages.md +++ b/packages/coding-agent/docs/packages.md @@ -28,7 +28,8 @@ pi install ./relative/path/to/package pi remove npm:@foo/bar pi list # show installed packages from settings -pi update # update pi, update packages, and reconcile pinned git refs +pi update # update pi only +pi update --all # update pi, update packages, and reconcile pinned git refs pi update --extensions # update packages and reconcile pinned git refs only pi update --self # update pi only pi update --self --force # reinstall pi even if current @@ -36,7 +37,7 @@ pi update npm:@foo/bar # update one package pi update --extension npm:@foo/bar ``` -These commands manage pi packages, not the pi CLI installation. To uninstall pi itself, see [Quickstart](quickstart.md#uninstall). +These commands manage pi packages and `pi update` can update the pi CLI installation. To uninstall pi itself, see [Quickstart](quickstart.md#uninstall). By default, `install` and `remove` write to user settings (`~/.pi/agent/settings.json`). Use `-l` to write to project settings (`.pi/settings.json`) instead. Project settings can be shared with your team, and pi installs any missing packages automatically on startup after the project is trusted. @@ -58,7 +59,7 @@ npm:@scope/pkg@1.2.3 npm:pkg ``` -- Versioned specs are pinned and skipped by package updates (`pi update`, `pi update --extensions`). +- Versioned specs are pinned and skipped by package updates (`pi update --extensions`, `pi update --all`). - User installs go under `~/.pi/agent/npm/`. - Project installs go under `.pi/npm/`. - Set `npmCommand` in `settings.json` to pin npm package lookup and install operations to a specific wrapper command such as `mise` or `asdf`. @@ -85,7 +86,7 @@ ssh://git@github.com/user/repo@v1 - HTTPS and SSH URLs are both supported. - SSH URLs use your configured SSH keys automatically (respects `~/.ssh/config`). - For non-interactive runs (for example CI), you can set `GIT_TERMINAL_PROMPT=0` to disable credential prompts and set `GIT_SSH_COMMAND` (for example `ssh -o BatchMode=yes -o ConnectTimeout=5`) to fail fast. -- Refs are pinned tags or commits. `pi update` and `pi update --extensions` do not move them to newer refs, but they do reconcile an existing clone to the configured ref. +- Refs are pinned tags or commits. `pi update --extensions` and `pi update --all` do not move them to newer refs, but they do reconcile an existing clone to the configured ref. - Use `pi install git:host/user/repo@new-ref` to update settings and move an existing package to a new pinned ref. - Cloned to `~/.pi/agent/git//` (global) or `.pi/git//` (project). - When reconciliation changes the checkout, pi resets and cleans the clone, then runs `npm install` if `package.json` exists. diff --git a/packages/coding-agent/docs/usage.md b/packages/coding-agent/docs/usage.md index 4f8f5954..5ff0cf73 100644 --- a/packages/coding-agent/docs/usage.md +++ b/packages/coding-agent/docs/usage.md @@ -145,7 +145,8 @@ pi [options] [@files...] [messages...] pi install [-l] # Install package, -l for project-local pi remove [-l] # Remove package pi uninstall [-l] # Alias for remove -pi update [source|self|pi] # Update pi and packages; reconcile pinned git refs +pi update [source|self|pi] # Update pi only, or one package source +pi update --all # Update pi and packages; reconcile pinned git refs pi update --extensions # Update packages only; reconcile pinned git refs pi update --self # Update pi only pi update --extension # Update one package @@ -153,7 +154,7 @@ pi list # List installed packages pi config # Enable/disable package resources ``` -These commands manage pi packages, not the pi CLI installation. To uninstall pi itself, see [Quickstart](quickstart.md#uninstall). `pi config` and project package commands accept `--approve`/`--no-approve` to trust or ignore project-local settings for one command. `pi update` never prompts for project trust. +These commands manage pi packages and `pi update` can update the pi CLI installation. To uninstall pi itself, see [Quickstart](quickstart.md#uninstall). `pi config` and project package commands accept `--approve`/`--no-approve` to trust or ignore project-local settings for one command. `pi update` never prompts for project trust. See [Pi Packages](packages.md) for package sources and security notes. diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index 839c60e8..5d3c4af8 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -229,7 +229,7 @@ ${chalk.bold("Commands:")} ${APP_NAME} install [-l] Install extension source and add to settings ${APP_NAME} remove [-l] Remove extension source from settings ${APP_NAME} uninstall [-l] Alias for remove - ${APP_NAME} update [source|self|pi] Update pi and installed extensions + ${APP_NAME} update [source|self|pi] Update pi (use --all for pi and extensions) ${APP_NAME} list List installed extensions from settings ${APP_NAME} config Open TUI to enable/disable package resources ${APP_NAME} --help Show help for install/remove/uninstall/update/list diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 0cde5ee4..00054eb2 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -3740,7 +3740,7 @@ export class InteractiveMode { } showPackageUpdateNotification(packages: string[]): void { - const action = theme.fg("accent", `${APP_NAME} update`); + const action = theme.fg("accent", `${APP_NAME} update --extensions`); const updateInstruction = theme.fg("muted", "Package updates are available. Run ") + action; const packageLines = packages.map((pkg) => `- ${pkg}`).join("\n"); diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index e00de1b4..d47df930 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -52,6 +52,7 @@ interface PackageCommandOptions { command: PackageCommand; source?: string; updateTarget?: UpdateTarget; + showExtensionsSkippedNote: boolean; local: boolean; force: boolean; projectTrustOverride?: boolean; @@ -79,7 +80,7 @@ function getPackageCommandUsage(command: PackageCommand): string { case "remove": return `${APP_NAME} remove [-l] [--approve|--no-approve]`; case "update": - return `${APP_NAME} update [source|self|pi] [--self] [--extensions] [--extension ] [--approve|--no-approve] [--force]`; + return `${APP_NAME} update [source|self|pi] [--self|--extensions|--all] [--extension ] [--approve|--no-approve] [--force]`; case "list": return `${APP_NAME} list [--approve|--no-approve]`; } @@ -133,15 +134,17 @@ Examples: Update pi and installed packages. Options: - --self Update pi only + --self Update pi only (default when no target is given) --extensions Update installed packages only + --all Update pi and installed packages --extension Update one package only -a, --approve Trust project-local files for this command -na, --no-approve Ignore project-local files for this command --force Reinstall pi even if the current version is latest Short forms: - ${APP_NAME} update Update pi and all extensions + ${APP_NAME} update Update pi only + ${APP_NAME} update --all Update pi and all extensions ${APP_NAME} update Update one package ${APP_NAME} update pi Update pi only (self works as alias to pi) `); @@ -184,6 +187,7 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined let source: string | undefined; let selfFlag = false; let extensionsFlag = false; + let allFlag = false; let extensionFlagSource: string | undefined; for (let index = 0; index < rest.length; index++) { @@ -220,6 +224,15 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined continue; } + if (arg === "--all") { + if (command === "update") { + allFlag = true; + } else { + invalidOption = invalidOption ?? arg; + } + continue; + } + if (arg === "--approve" || arg === "-a") { projectTrustOverride = true; continue; @@ -271,10 +284,20 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined } let updateTarget: UpdateTarget | undefined; + let showExtensionsSkippedNote = false; if (command === "update") { + if (allFlag && (selfFlag || extensionsFlag || extensionFlagSource)) { + conflictingOptions = + conflictingOptions ?? "--all cannot be combined with --self, --extensions, or --extension"; + } + if (allFlag && source) { + conflictingOptions = conflictingOptions ?? "--all cannot be combined with a positional source"; + } + if (extensionFlagSource) { - if (selfFlag || extensionsFlag) { - conflictingOptions = conflictingOptions ?? "--extension cannot be combined with --self or --extensions"; + if (selfFlag || extensionsFlag || allFlag) { + conflictingOptions = + conflictingOptions ?? "--extension cannot be combined with --self, --extensions, or --all"; } if (source) { conflictingOptions = conflictingOptions ?? "--extension cannot be combined with a positional source"; @@ -285,12 +308,15 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined if (sourceIsSelf) { updateTarget = extensionsFlag ? { type: "all" } : { type: "self" }; } else { - if (extensionsFlag || selfFlag) { + if (extensionsFlag || selfFlag || allFlag) { conflictingOptions = - conflictingOptions ?? "positional update targets cannot be combined with --self or --extensions"; + conflictingOptions ?? + "positional update targets cannot be combined with --self, --extensions, or --all"; } updateTarget = { type: "extensions", source }; } + } else if (allFlag) { + updateTarget = { type: "all" }; } else if (selfFlag && extensionsFlag) { updateTarget = { type: "all" }; } else if (selfFlag) { @@ -298,7 +324,8 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined } else if (extensionsFlag) { updateTarget = { type: "extensions" }; } else { - updateTarget = { type: "all" }; + updateTarget = { type: "self" }; + showExtensionsSkippedNote = true; } } @@ -306,6 +333,7 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined command, source, updateTarget, + showExtensionsSkippedNote, local, force, projectTrustOverride, @@ -660,7 +688,12 @@ export async function handlePackageCommand( } case "update": { - const target = options.updateTarget ?? { type: "all" }; + const target = options.updateTarget ?? { type: "self" }; + if (options.showExtensionsSkippedNote) { + console.log( + chalk.dim(`Extensions are skipped. Run ${APP_NAME} update --extensions to update extensions.`), + ); + } if (updateTargetIncludesExtensions(target)) { const updateSource = target.type === "extensions" ? target.source : undefined; await packageManager.update(updateSource); From 717494223d41d663224f1b177c8427c05ad62448 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 18:16:48 +0200 Subject: [PATCH 024/122] docs: audit unreleased changelogs --- packages/coding-agent/CHANGELOG.md | 13 ++++++++++++- packages/tui/CHANGELOG.md | 2 ++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e29e19d2..3f953510 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,13 +2,24 @@ ## [Unreleased] +### New Features + +- **Automatic theme mode** - `/settings` can choose separate light and dark themes and follow terminal color-scheme changes. See [Selecting a Theme](docs/themes.md#selecting-a-theme). +- **Self-only updates by default** - `pi update` now updates pi only, with `pi update --all` for updating pi and packages together. See [Install and Manage](docs/packages.md#install-and-manage). +- **Extension API helpers** - extensions can use `CONFIG_DIR_NAME` for project config paths and import edit diff helpers for edit-style diffs. See [`ctx.cwd`](docs/extensions.md#ctxcwd) and [SDK Exports](docs/sdk.md#exports). +- **Warp inline images** - Warp terminals now get inline image rendering through Kitty graphics detection. See [Image](docs/tui.md#image). + ### Added -- Exported `CONFIG_DIR_NAME` from the coding-agent public API so extensions can resolve project config paths without hardcoding `.pi` ([#5869](https://github.com/earendil-works/pi/pulls/5869) by [@xl0](https://github.com/xl0)) +- Added automatic theme mode so `/settings` can use separate light and dark themes and follow terminal color-scheme changes ([#5874](https://github.com/earendil-works/pi/pull/5874)). +- Added inherited Warp terminal image capability detection so inline images render through Warp's Kitty graphics support ([#5841](https://github.com/earendil-works/pi/pull/5841) by [@dodiego](https://github.com/dodiego)). +- Exported `CONFIG_DIR_NAME` from the coding-agent public API so extensions can resolve project config paths without hardcoding `.pi` ([#5869](https://github.com/earendil-works/pi/pull/5869) by [@xl0](https://github.com/xl0)). +- Exported edit diff helpers (`generateDiffString`, `generateUnifiedPatch`, and `EditDiffResult`) from the public API for extensions that need edit-style diffs ([#5756](https://github.com/earendil-works/pi/pull/5756) by [@xl0](https://github.com/xl0)). ### Changed - Changed bare `pi update` to update only pi, added `pi update --all` for updating pi and extensions together, and clarified extension update prompts. +- Reserved `/` in theme names for automatic light/dark theme settings. - Updated extension docs, examples, runtime help, trust prompts, and config labels to use the configured project config directory instead of hardcoded `.pi` paths. ### Fixed diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 9b90a5b1..ca28b0a3 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -4,6 +4,8 @@ ### Added +- Added terminal color-scheme query and notification support for light/dark appearance detection (`TUI.queryTerminalColorScheme()`, `TUI.onTerminalColorSchemeChange()`, and `TUI.setTerminalColorSchemeNotifications()`) ([#5874](https://github.com/earendil-works/pi/pull/5874)). +- Added Warp terminal detection for Kitty graphics inline image support ([#5841](https://github.com/earendil-works/pi/pull/5841) by [@dodiego](https://github.com/dodiego)). - Exported `sliceByColumn` for ANSI-aware horizontal column slicing. ## [0.79.6] - 2026-06-16 From c4ab61dcbf719e75018f0ae90eb900b78a881603 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 18:22:07 +0200 Subject: [PATCH 025/122] Release v0.79.7 --- package-lock.json | 26 ++++---- packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 +- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- packages/ai/src/image-models.generated.ts | 30 +++++++++ packages/ai/src/models.generated.ts | 65 ++++++++++++------- packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/gondolin/package-lock.json | 4 +- .../examples/extensions/gondolin/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/npm-shrinkwrap.json | 24 +++---- packages/coding-agent/package.json | 8 +-- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- 21 files changed, 120 insertions(+), 75 deletions(-) diff --git a/package-lock.json b/package-lock.json index d4811db6..821c9d30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6176,10 +6176,10 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.79.6", + "version": "0.79.7", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.6", + "@earendil-works/pi-ai": "^0.79.7", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -6213,7 +6213,7 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.79.6", + "version": "0.79.7", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -6258,12 +6258,12 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.6", + "version": "0.79.7", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.6", - "@earendil-works/pi-ai": "^0.79.6", - "@earendil-works/pi-tui": "^0.79.6", + "@earendil-works/pi-agent-core": "^0.79.7", + "@earendil-works/pi-ai": "^0.79.7", + "@earendil-works/pi-tui": "^0.79.7", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -6304,32 +6304,32 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.79.6", + "version": "0.79.7", "dependencies": { "@anthropic-ai/sdk": "0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.79.6" + "version": "0.79.7" }, "packages/coding-agent/examples/extensions/gondolin": { "name": "pi-extension-gondolin", - "version": "0.79.6", + "version": "0.79.7", "dependencies": { "@earendil-works/gondolin": "0.12.0" } }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.9.6", + "version": "1.9.7", "dependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.79.6", + "version": "0.79.7", "dependencies": { "ms": "2.1.3" }, @@ -6365,7 +6365,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.79.6", + "version": "0.79.7", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 41203247..76368ff6 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.7] - 2026-06-18 ## [0.79.6] - 2026-06-16 diff --git a/packages/agent/package.json b/packages/agent/package.json index 66ee4847..81d3e356 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.79.6", + "version": "0.79.7", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -29,7 +29,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.79.6", + "@earendil-works/pi-ai": "^0.79.7", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index a49ddf52..f6070588 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.7] - 2026-06-18 ### Added diff --git a/packages/ai/package.json b/packages/ai/package.json index 11e4261b..9c71d156 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.79.6", + "version": "0.79.7", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/ai/src/image-models.generated.ts b/packages/ai/src/image-models.generated.ts index 09c74180..7545a9f1 100644 --- a/packages/ai/src/image-models.generated.ts +++ b/packages/ai/src/image-models.generated.ts @@ -95,6 +95,21 @@ export const IMAGE_MODELS = { cacheWrite: 0.08333333333333334, }, } satisfies ImagesModel<"openrouter-images">, + "google/gemini-3-pro-image": { + id: "google/gemini-3-pro-image", + name: "Google: Nano Banana Pro (Gemini 3 Pro Image)", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["image", "text"], + output: ["image", "text"], + cost: { + input: 2, + output: 12, + cacheRead: 0.19999999999999998, + cacheWrite: 0.375, + }, + } satisfies ImagesModel<"openrouter-images">, "google/gemini-3-pro-image-preview": { id: "google/gemini-3-pro-image-preview", name: "Google: Nano Banana Pro (Gemini 3 Pro Image Preview)", @@ -110,6 +125,21 @@ export const IMAGE_MODELS = { cacheWrite: 0.375, }, } satisfies ImagesModel<"openrouter-images">, + "google/gemini-3.1-flash-image": { + id: "google/gemini-3.1-flash-image", + name: "Google: Nano Banana 2 (Gemini 3.1 Flash Image)", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["image", "text"], + output: ["image", "text"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, "google/gemini-3.1-flash-image-preview": { id: "google/gemini-3.1-flash-image-preview", name: "Google: Nano Banana 2 (Gemini 3.1 Flash Image Preview)", diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 424f8bcd..8e1b776f 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -4566,25 +4566,6 @@ export const MODELS = { contextWindow: 400000, maxTokens: 128000, } satisfies Model<"openai-responses">, - "raptor-mini": { - id: "raptor-mini", - name: "Raptor mini", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, }, "google": { "gemini-2.0-flash": { @@ -11785,6 +11766,23 @@ export const MODELS = { contextWindow: 1048756, maxTokens: 262144, } satisfies Model<"openai-completions">, + "poolside/laguna-m.1": { + id: "poolside/laguna-m.1", + name: "Poolside: Laguna M.1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.2, + output: 0.4, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, "poolside/laguna-m.1:free": { id: "poolside/laguna-m.1:free", name: "Poolside: Laguna M.1 (free)", @@ -11802,6 +11800,23 @@ export const MODELS = { contextWindow: 262144, maxTokens: 32768, } satisfies Model<"openai-completions">, + "poolside/laguna-xs.2": { + id: "poolside/laguna-xs.2", + name: "Poolside: Laguna XS.2", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.1, + output: 0.2, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, "poolside/laguna-xs.2:free": { id: "poolside/laguna-xs.2:free", name: "Poolside: Laguna XS.2 (free)", @@ -12425,11 +12440,11 @@ export const MODELS = { cost: { input: 0.14, output: 1, - cacheRead: 0.05, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 81920, + maxTokens: 262144, } satisfies Model<"openai-completions">, "qwen/qwen3.5-397b-a17b": { id: "qwen/qwen3.5-397b-a17b", @@ -12542,9 +12557,9 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.15, + input: 0.14, output: 1, - cacheRead: 0.05, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, @@ -13054,11 +13069,11 @@ export const MODELS = { cost: { input: 1.4, output: 4.4, - cacheRead: 0.26, + cacheRead: 0.7, cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 262144, + maxTokens: 524288, } satisfies Model<"openai-completions">, "~anthropic/claude-fable-latest": { id: "~anthropic/claude-fable-latest", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 3f953510..89e7f892 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.7] - 2026-06-18 ### New Features diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index bd9ae9f3..c3f35b25 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.79.6", + "version": "0.79.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.79.6", + "version": "0.79.7", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index 7f2d2bb4..c817a94e 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.79.6", + "version": "0.79.7", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index 0478f9c8..12363537 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.79.6", + "version": "0.79.7", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/gondolin/package-lock.json b/packages/coding-agent/examples/extensions/gondolin/package-lock.json index c573e8e0..ea80b459 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package-lock.json +++ b/packages/coding-agent/examples/extensions/gondolin/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-gondolin", - "version": "0.79.6", + "version": "0.79.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-gondolin", - "version": "0.79.6", + "version": "0.79.7", "dependencies": { "@earendil-works/gondolin": "0.12.0" } diff --git a/packages/coding-agent/examples/extensions/gondolin/package.json b/packages/coding-agent/examples/extensions/gondolin/package.json index 71e89d6c..89705d97 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package.json +++ b/packages/coding-agent/examples/extensions/gondolin/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-gondolin", "private": true, - "version": "0.79.6", + "version": "0.79.7", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 24e20622..41b01437 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.9.6", + "version": "1.9.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.9.6", + "version": "1.9.7", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index 7f1fa016..a80abe4a 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.9.6", + "version": "1.9.7", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index fb5d53cd..b45b2214 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.79.6", + "version": "0.79.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.79.6", + "version": "0.79.7", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index b8d759de..02abd397 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.79.6", + "version": "0.79.7", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index 39313f9d..c366f1a5 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -1,17 +1,17 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.6", + "version": "0.79.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.6", + "version": "0.79.7", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.6", - "@earendil-works/pi-ai": "^0.79.6", - "@earendil-works/pi-tui": "^0.79.6", + "@earendil-works/pi-agent-core": "^0.79.7", + "@earendil-works/pi-ai": "^0.79.7", + "@earendil-works/pi-tui": "^0.79.7", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -474,11 +474,11 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.79.6", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.6.tgz", + "version": "0.79.7", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.7.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.6", + "@earendil-works/pi-ai": "^0.79.7", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -488,8 +488,8 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.79.6", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.6.tgz", + "version": "0.79.7", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.7.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -511,8 +511,8 @@ } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.79.6", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.6.tgz", + "version": "0.79.7", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.7.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 22b408ce..c873bb8d 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.6", + "version": "0.79.7", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -36,9 +36,9 @@ "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.6", - "@earendil-works/pi-ai": "^0.79.6", - "@earendil-works/pi-tui": "^0.79.6", + "@earendil-works/pi-agent-core": "^0.79.7", + "@earendil-works/pi-ai": "^0.79.7", + "@earendil-works/pi-tui": "^0.79.7", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index ca28b0a3..28a38ba8 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.7] - 2026-06-18 ### Added diff --git a/packages/tui/package.json b/packages/tui/package.json index 9c73829c..24590a18 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.79.6", + "version": "0.79.7", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", From 788a044483da3d3c1fe1b4603fde7ffac0fbae3f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 18:22:10 +0200 Subject: [PATCH 026/122] Add [Unreleased] section for next cycle --- packages/agent/CHANGELOG.md | 2 ++ packages/ai/CHANGELOG.md | 2 ++ packages/coding-agent/CHANGELOG.md | 2 ++ packages/tui/CHANGELOG.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 76368ff6..3050117f 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.7] - 2026-06-18 ## [0.79.6] - 2026-06-16 diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index f6070588..72851f5f 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.7] - 2026-06-18 ### Added diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 89e7f892..ed277120 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.7] - 2026-06-18 ### New Features diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 28a38ba8..4db80b91 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.7] - 2026-06-18 ### Added From 6b9f3f492f1de0683246e6fb2f86d35f70335f56 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 19:30:14 +0200 Subject: [PATCH 027/122] fix(coding-agent): avoid retrying successful overflow compaction closes #5720 --- packages/coding-agent/CHANGELOG.md | 4 +++ .../coding-agent/src/core/agent-session.ts | 13 ++++++-- .../suite/agent-session-compaction.test.ts | 31 +++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index ed277120..ab9812c5 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed successful overflow-triggered auto-compaction to avoid retrying completed assistant responses ([#5720](https://github.com/earendil-works/pi/issues/5720)). + ## [0.79.7] - 2026-06-18 ### New Features diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 54de3103..23fb5983 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -1824,8 +1824,17 @@ export class AgentSession { return false; } - // Case 1: Overflow - LLM returned context overflow error + // Case 1: Overflow - LLM returned context overflow error, or reported usage exceeded + // the configured window. A successful response over the configured window should compact + // but must not retry: the assistant answer already completed and agent.continue() cannot + // continue from an assistant message. if (sameModel && isContextOverflow(assistantMessage, contextWindow)) { + const willRetry = assistantMessage.stopReason !== "stop"; + + if (!willRetry) { + return await this._runAutoCompaction("overflow", false); + } + if (this._overflowRecoveryAttempted) { this._emit({ type: "compaction_end", @@ -1846,7 +1855,7 @@ export class AgentSession { if (messages.length > 0 && messages[messages.length - 1].role === "assistant") { this.agent.state.messages = messages.slice(0, -1); } - return await this._runAutoCompaction("overflow", true); + return await this._runAutoCompaction("overflow", willRetry); } // Case 2: Threshold - context is getting large diff --git a/packages/coding-agent/test/suite/agent-session-compaction.test.ts b/packages/coding-agent/test/suite/agent-session-compaction.test.ts index e04732d0..e790d896 100644 --- a/packages/coding-agent/test/suite/agent-session-compaction.test.ts +++ b/packages/coding-agent/test/suite/agent-session-compaction.test.ts @@ -248,6 +248,37 @@ describe("AgentSession compaction characterization", () => { ); }); + it("compacts successful overflow responses without retrying", async () => { + const harness = await createHarness({ + settings: { compaction: { enabled: true, keepRecentTokens: 1, reserveTokens: 0 } }, + models: [{ id: "faux-1", contextWindow: 1, maxTokens: 100 }], + extensionFactories: [ + (pi) => { + pi.on("session_before_compact", async (event) => ({ + compaction: { + summary: "successful overflow compacted", + firstKeptEntryId: event.preparation.firstKeptEntryId, + tokensBefore: event.preparation.tokensBefore, + details: {}, + }, + })); + }, + ], + }); + harnesses.push(harness); + harness.setResponses([fauxAssistantMessage("completed answer")]); + + await expect(harness.session.prompt("hello")).resolves.toBeUndefined(); + + const compactionEnd = harness.eventsOfType("compaction_end").at(-1); + expect(compactionEnd).toMatchObject({ + reason: "overflow", + aborted: false, + willRetry: false, + }); + expect(harness.faux.state.callCount).toBe(1); + }); + it("ignores stale pre-compaction assistant usage on pre-prompt checks", async () => { const harness = await createHarness(); harnesses.push(harness); From 7d08c81a0962b4731ca41e89cddc9ed9eb103455 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 22:17:57 +0200 Subject: [PATCH 028/122] fix(coding-agent): avoid empty compaction summaries closes #4811 --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/core/agent-session.ts | 53 +++++++------------ .../src/core/compaction/compaction.ts | 4 ++ .../src/modes/interactive/interactive-mode.ts | 9 ---- .../test/compaction-extensions.test.ts | 1 + packages/coding-agent/test/compaction.test.ts | 25 +-------- .../suite/agent-session-compaction.test.ts | 19 ++++--- 7 files changed, 37 insertions(+), 75 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index ab9812c5..0fcc303a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed compaction to refuse sessions with no eligible messages instead of producing empty summaries ([#4811](https://github.com/earendil-works/pi/issues/4811)). - Fixed successful overflow-triggered auto-compaction to avoid retrying completed assistant responses ([#5720](https://github.com/earendil-works/pi/issues/5720)). ## [0.79.7] - 2026-06-18 diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 23fb5983..cae330f3 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -1892,19 +1892,10 @@ export class AgentSession { */ private async _runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean): Promise { const settings = this.settingsManager.getCompactionSettings(); - - this._emit({ type: "compaction_start", reason }); - this._autoCompactionAbortController = new AbortController(); + let started = false; try { if (!this.model) { - this._emit({ - type: "compaction_end", - reason, - result: undefined, - aborted: false, - willRetry: false, - }); return false; } @@ -1914,13 +1905,6 @@ export class AgentSession { if (this.agent.streamFn === streamSimple) { const authResult = await this._modelRegistry.getApiKeyAndHeaders(this.model); if (!authResult.ok || !authResult.apiKey) { - this._emit({ - type: "compaction_end", - reason, - result: undefined, - aborted: false, - willRetry: false, - }); return false; } apiKey = authResult.apiKey; @@ -1934,16 +1918,13 @@ export class AgentSession { const preparation = prepareCompaction(pathEntries, settings); if (!preparation) { - this._emit({ - type: "compaction_end", - reason, - result: undefined, - aborted: false, - willRetry: false, - }); return false; } + this._emit({ type: "compaction_start", reason }); + this._autoCompactionAbortController = new AbortController(); + started = true; + let extensionCompaction: CompactionResult | undefined; let fromExtension = false; @@ -2054,17 +2035,19 @@ export class AgentSession { return this.agent.hasQueuedMessages(); } catch (error) { const errorMessage = error instanceof Error ? error.message : "compaction failed"; - this._emit({ - type: "compaction_end", - reason, - result: undefined, - aborted: false, - willRetry: false, - errorMessage: - reason === "overflow" - ? `Context overflow recovery failed: ${errorMessage}` - : `Auto-compaction failed: ${errorMessage}`, - }); + if (started) { + this._emit({ + type: "compaction_end", + reason, + result: undefined, + aborted: false, + willRetry: false, + errorMessage: + reason === "overflow" + ? `Context overflow recovery failed: ${errorMessage}` + : `Auto-compaction failed: ${errorMessage}`, + }); + } return false; } finally { this._autoCompactionAbortController = undefined; diff --git a/packages/coding-agent/src/core/compaction/compaction.ts b/packages/coding-agent/src/core/compaction/compaction.ts index 07f2f9fe..f3f3d59f 100644 --- a/packages/coding-agent/src/core/compaction/compaction.ts +++ b/packages/coding-agent/src/core/compaction/compaction.ts @@ -698,6 +698,10 @@ export function prepareCompaction( } } + if (messagesToSummarize.length === 0 && turnPrefixMessages.length === 0) { + return undefined; + } + // Extract file operations from messages and previous compaction const fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 00054eb2..962c414f 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -3700,7 +3700,6 @@ export class InteractiveMode { showError(errorMessage: string): void { this.chatContainer.addChild(new Spacer(1)); this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0)); - this.chatContainer.addChild(new Spacer(1)); this.ui.requestRender(); } @@ -5695,14 +5694,6 @@ export class InteractiveMode { } private async handleCompactCommand(customInstructions?: string): Promise { - const entries = this.sessionManager.getEntries(); - const messageCount = entries.filter((e) => e.type === "message").length; - - if (messageCount < 2) { - this.showWarning("Nothing to compact (no messages yet)"); - return; - } - if (this.loadingAnimation) { this.loadingAnimation.stop(); this.loadingAnimation = undefined; diff --git a/packages/coding-agent/test/compaction-extensions.test.ts b/packages/coding-agent/test/compaction-extensions.test.ts index 2c41210e..c95125f2 100644 --- a/packages/coding-agent/test/compaction-extensions.test.ts +++ b/packages/coding-agent/test/compaction-extensions.test.ts @@ -98,6 +98,7 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => { const sessionManager = SessionManager.create(tempDir); const settingsManager = SettingsManager.create(tempDir, tempDir); + settingsManager.applyOverrides({ compaction: { keepRecentTokens: 1 } }); const authStorage = AuthStorage.create(join(tempDir, "auth.json")); const modelRegistry = ModelRegistry.create(authStorage); diff --git a/packages/coding-agent/test/compaction.test.ts b/packages/coding-agent/test/compaction.test.ts index 929d06ec..be3d5c63 100644 --- a/packages/coding-agent/test/compaction.test.ts +++ b/packages/coding-agent/test/compaction.test.ts @@ -9,7 +9,6 @@ import { calculateContextTokens, compact, DEFAULT_COMPACTION_SETTINGS, - estimateContextTokens, findCutPoint, getLastAssistantUsage, prepareCompaction, @@ -396,7 +395,7 @@ describe("buildSessionContext", () => { }); describe("prepareCompaction with previous compaction", () => { - it("should preserve kept messages across repeated compactions when they still fit", () => { + it("should skip repeated compactions when kept messages still fit", () => { const u1 = createMessageEntry(createUserMessage("user msg 1 (summarized by compaction1)")); const a1 = createMessageEntry(createAssistantMessage("assistant msg 1")); const u2 = createMessageEntry(createUserMessage("user msg 2 - kept by compaction1")); @@ -408,29 +407,9 @@ describe("prepareCompaction with previous compaction", () => { const a4 = createMessageEntry(createAssistantMessage("assistant msg 4", createMockUsage(8000, 2000))); const pathEntries = [u1, a1, u2, a2, u3, a3, compaction1, u4, a4]; - const contextBefore = buildSessionContext(pathEntries); const preparation = prepareCompaction(pathEntries, DEFAULT_COMPACTION_SETTINGS); - expect(preparation).toBeDefined(); - expect(preparation!.firstKeptEntryId).toBe(u2.id); - expect(preparation!.previousSummary).toBe("First summary"); - expect(extractText(preparation!.messagesToSummarize)).not.toContain("First summary"); - expect(preparation!.tokensBefore).toBe(estimateContextTokens(contextBefore.messages).tokens); - - const compaction2: CompactionEntry = { - type: "compaction", - id: "compaction2-id", - parentId: a4.id, - timestamp: new Date().toISOString(), - summary: "Second summary", - firstKeptEntryId: preparation!.firstKeptEntryId, - tokensBefore: preparation!.tokensBefore, - }; - const contextAfter = buildSessionContext([...pathEntries, compaction2]); - const contextAfterText = extractText(contextAfter.messages); - - expect(contextAfterText).toContain("user msg 2 - kept by compaction1"); - expect(contextAfterText).toContain("user msg 3 - kept by compaction1"); + expect(preparation).toBeUndefined(); }); it("should re-summarize previously kept messages when the recent window moves past them", () => { diff --git a/packages/coding-agent/test/suite/agent-session-compaction.test.ts b/packages/coding-agent/test/suite/agent-session-compaction.test.ts index e790d896..a3302adf 100644 --- a/packages/coding-agent/test/suite/agent-session-compaction.test.ts +++ b/packages/coding-agent/test/suite/agent-session-compaction.test.ts @@ -67,19 +67,20 @@ function useSummaryStreamFn(harness: Harness, summary: string): () => number { } function seedCompactableSession(harness: Harness): void { + harness.settingsManager.applyOverrides({ compaction: { keepRecentTokens: 1 } }); const now = Date.now(); harness.sessionManager.appendMessage({ role: "user", content: [{ type: "text", text: "message to compact" }], timestamp: now - 1000, }); - harness.sessionManager.appendMessage( - createAssistant(harness, { - stopReason: "stop", - totalTokens: 100, - timestamp: now - 500, - }), - ); + const assistant = createAssistant(harness, { + stopReason: "stop", + totalTokens: 100, + timestamp: now - 500, + }); + assistant.content = [{ type: "text", text: "assistant response to compact" }]; + harness.sessionManager.appendMessage(assistant); harness.session.agent.state.messages = harness.sessionManager.buildSessionContext().messages; } @@ -96,6 +97,7 @@ describe("AgentSession compaction characterization", () => { it("manually compacts using an extension-provided summary", async () => { const harness = await createHarness({ + settings: { compaction: { keepRecentTokens: 1 } }, extensionFactories: [ (pi) => { pi.on("session_before_compact", async (event) => ({ @@ -145,7 +147,7 @@ describe("AgentSession compaction characterization", () => { const result = await harness.session.compact(); - expect(result.summary).toBe("summary from custom stream"); + expect(result.summary).toContain("summary from custom stream"); expect(getStreamCallCount()).toBe(1); }); @@ -165,6 +167,7 @@ describe("AgentSession compaction characterization", () => { it("cancels in-progress manual compaction when abortCompaction is called", async () => { const harness = await createHarness({ + settings: { compaction: { keepRecentTokens: 1 } }, extensionFactories: [ (pi) => { pi.on("session_before_compact", async (event) => { From b09fbde00eaeb33413b9879137612e002bbb4086 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Thu, 18 Jun 2026 23:39:02 +0300 Subject: [PATCH 029/122] feat(ai): add OpenRouter Fusion alias (#5866) --- packages/ai/scripts/generate-models.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 3d75a82e..0c1bcc42 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -2030,6 +2030,32 @@ async function generateModels() { }); } + // Add "fusion" alias for openrouter/fusion. OpenRouter exposes Fusion as a + // router alias/plugin entry point; its model metadata does not advertise + // tools, but the alias resolves to a concrete model that can invoke caller + // tools and has the openrouter:fusion server tool auto-injected. + if (!allModels.some(m => m.provider === "openrouter" && m.id === "openrouter/fusion")) { + allModels.push({ + id: "openrouter/fusion", + name: "OpenRouter: Fusion", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + // we dont know about the costs because Fusion routes to multiple models + // and then charges you for the underlying used models + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 30000, + }); + } + // Azure Foundry deploys these with larger context windows than OpenAI's own API, // which caps gpt-5.4/gpt-5.5 at 272k. See models-sold-directly-by-azure docs. const AZURE_CONTEXT_WINDOW_OVERRIDES: Record = { From c60f6a8ab3b36155d572fe9c10df61f7613281ee Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 23:01:10 +0200 Subject: [PATCH 030/122] feat(coding-agent): expose post-compaction token estimates closes #5877 --- packages/coding-agent/CHANGELOG.md | 4 ++++ packages/coding-agent/docs/rpc.md | 4 ++++ packages/coding-agent/src/core/agent-session.ts | 15 ++++++++++++++- .../src/core/compaction/compaction.ts | 1 + .../test/suite/agent-session-compaction.test.ts | 5 +++++ 5 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0fcc303a..e58cd877 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added estimated post-compaction token counts to compact results and compaction events ([#5877](https://github.com/earendil-works/pi/issues/5877)). + ### Fixed - Fixed compaction to refuse sessions with no eligible messages instead of producing empty summaries ([#4811](https://github.com/earendil-works/pi/issues/4811)). diff --git a/packages/coding-agent/docs/rpc.md b/packages/coding-agent/docs/rpc.md index 9aa16ffc..a9942409 100644 --- a/packages/coding-agent/docs/rpc.md +++ b/packages/coding-agent/docs/rpc.md @@ -374,11 +374,14 @@ Response: "summary": "Summary of conversation...", "firstKeptEntryId": "abc123", "tokensBefore": 150000, + "estimatedTokensAfter": 32000, "details": {} } } ``` +`estimatedTokensAfter` is a heuristic estimate over the rebuilt message context immediately after compaction, not a provider-exact token count. + #### set_auto_compaction Enable or disable automatic compaction when context is nearly full. @@ -924,6 +927,7 @@ The `reason` field is `"manual"`, `"threshold"`, or `"overflow"`. "summary": "Summary of conversation...", "firstKeptEntryId": "abc123", "tokensBefore": 150000, + "estimatedTokensAfter": 32000, "details": {} }, "aborted": false, diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index cae330f3..f8af4320 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -45,6 +45,7 @@ import { collectEntriesForBranchSummary, compact, estimateContextTokens, + estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, @@ -242,6 +243,14 @@ interface ToolDefinitionEntry { sourceInfo: SourceInfo; } +function estimateMessagesTokens(messages: AgentMessage[]): number { + let tokens = 0; + for (const message of messages) { + tokens += estimateTokens(message); + } + return tokens; +} + // ============================================================================ // Constants // ============================================================================ @@ -1726,6 +1735,7 @@ export class AgentSession { const newEntries = this.sessionManager.getEntries(); const sessionContext = this.sessionManager.buildSessionContext(); this.agent.state.messages = sessionContext.messages; + const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages); // Get the saved compaction entry for the extension event const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary) as @@ -1740,10 +1750,11 @@ export class AgentSession { }); } - const compactionResult = { + const compactionResult: CompactionResult = { summary, firstKeptEntryId, tokensBefore, + estimatedTokensAfter, details, }; this._emit({ @@ -1999,6 +2010,7 @@ export class AgentSession { const newEntries = this.sessionManager.getEntries(); const sessionContext = this.sessionManager.buildSessionContext(); this.agent.state.messages = sessionContext.messages; + const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages); // Get the saved compaction entry for the extension event const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary) as @@ -2017,6 +2029,7 @@ export class AgentSession { summary, firstKeptEntryId, tokensBefore, + estimatedTokensAfter, details, }; this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry }); diff --git a/packages/coding-agent/src/core/compaction/compaction.ts b/packages/coding-agent/src/core/compaction/compaction.ts index f3f3d59f..83315a84 100644 --- a/packages/coding-agent/src/core/compaction/compaction.ts +++ b/packages/coding-agent/src/core/compaction/compaction.ts @@ -104,6 +104,7 @@ export interface CompactionResult { summary: string; firstKeptEntryId: string; tokensBefore: number; + estimatedTokensAfter?: number; /** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */ details?: T; } diff --git a/packages/coding-agent/test/suite/agent-session-compaction.test.ts b/packages/coding-agent/test/suite/agent-session-compaction.test.ts index a3302adf..ba5ae5c7 100644 --- a/packages/coding-agent/test/suite/agent-session-compaction.test.ts +++ b/packages/coding-agent/test/suite/agent-session-compaction.test.ts @@ -5,6 +5,7 @@ import { type Model, } from "@earendil-works/pi-ai"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { estimateTokens } from "../../src/core/compaction/index.ts"; import { createHarness, type Harness } from "./harness.ts"; type SessionWithCompactionInternals = { @@ -118,8 +119,10 @@ describe("AgentSession compaction characterization", () => { const result = await harness.session.compact(); const compactionEntries = harness.sessionManager.getEntries().filter((entry) => entry.type === "compaction"); + const estimatedTokensAfter = harness.session.messages.reduce((sum, message) => sum + estimateTokens(message), 0); expect(result.summary).toBe("summary from extension"); + expect(result.estimatedTokensAfter).toBe(estimatedTokensAfter); expect(compactionEntries).toHaveLength(1); expect(harness.session.messages[0]?.role).toBe("compactionSummary"); }); @@ -161,7 +164,9 @@ describe("AgentSession compaction characterization", () => { await sessionInternals._runAutoCompaction("threshold", false); const compactionEntries = harness.sessionManager.getEntries().filter((entry) => entry.type === "compaction"); + const compactionEnd = harness.eventsOfType("compaction_end").at(-1); expect(compactionEntries).toHaveLength(1); + expect(compactionEnd?.result?.estimatedTokensAfter).toBeGreaterThan(0); expect(getStreamCallCount()).toBe(1); }); From cab89d143587e19b4f77b0d390028bd90a279b19 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 23:09:41 +0200 Subject: [PATCH 031/122] docs: audit unreleased changelogs --- packages/ai/CHANGELOG.md | 4 ++++ packages/coding-agent/CHANGELOG.md | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 72851f5f..2b5aeca1 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added the OpenRouter Fusion alias as `openrouter/fusion` ([#5866](https://github.com/earendil-works/pi/pull/5866) by [@dannote](https://github.com/dannote)). + ## [0.79.7] - 2026-06-18 ### Added diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e58cd877..5f0c6186 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,9 +2,15 @@ ## [Unreleased] +### New Features + +- **Post-compaction token estimates** - Compact results and compaction events now include estimated post-compaction token counts so clients can show the approximate context reduction. See [RPC compact](docs/rpc.md#compact) and [compaction events](docs/rpc.md#compaction_start--compaction_end). +- **OpenRouter Fusion alias** - `openrouter/fusion` is available as a built-in OpenRouter model alias. See [API Keys](docs/providers.md#api-keys). + ### Added - Added estimated post-compaction token counts to compact results and compaction events ([#5877](https://github.com/earendil-works/pi/issues/5877)). +- Added the inherited OpenRouter Fusion alias as `openrouter/fusion` ([#5866](https://github.com/earendil-works/pi/pull/5866) by [@dannote](https://github.com/dannote)). ### Fixed From fd1ba2c7feaedb78e5d87107f6e6a274044121b8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 23:15:18 +0200 Subject: [PATCH 032/122] test(coding-agent): seed auto-compaction queue fixture --- ...gent-session-auto-compaction-queue.test.ts | 103 ++++++++++-------- 1 file changed, 59 insertions(+), 44 deletions(-) diff --git a/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts b/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts index 1abe3e28..7fd56b46 100644 --- a/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts +++ b/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts @@ -2,7 +2,12 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Agent } from "@earendil-works/pi-agent-core"; -import { type AssistantMessage, getModel } from "@earendil-works/pi-ai"; +import { + type AssistantMessage, + createAssistantMessageEventStream, + fauxAssistantMessage, + getModel, +} from "@earendil-works/pi-ai"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { AgentSession } from "../src/core/agent-session.ts"; import { AuthStorage } from "../src/core/auth-storage.ts"; @@ -11,51 +16,10 @@ import { SessionManager } from "../src/core/session-manager.ts"; import { SettingsManager } from "../src/core/settings-manager.ts"; import { createTestResourceLoader } from "./utilities.ts"; -vi.mock("../src/core/compaction/index.js", () => ({ - calculateContextTokens: (usage: { - input: number; - output: number; - cacheRead: number; - cacheWrite: number; - totalTokens?: number; - }) => usage.totalTokens ?? usage.input + usage.output + usage.cacheRead + usage.cacheWrite, - collectEntriesForBranchSummary: () => ({ entries: [], commonAncestorId: null }), - compact: async () => ({ - summary: "compacted", - firstKeptEntryId: "entry-1", - tokensBefore: 100, - details: {}, - }), - estimateContextTokens: ( - messages: Array<{ - role: string; - usage?: { input: number; output: number; cacheRead: number; cacheWrite: number; totalTokens?: number }; - stopReason?: string; - }>, - ) => { - // Walk backwards to find last non-error, non-aborted assistant with usage - for (let i = messages.length - 1; i >= 0; i--) { - const msg = messages[i]; - if (msg.role === "assistant" && msg.stopReason !== "error" && msg.stopReason !== "aborted" && msg.usage) { - const tokens = - msg.usage.totalTokens ?? msg.usage.input + msg.usage.output + msg.usage.cacheRead + msg.usage.cacheWrite; - return { tokens, usageTokens: tokens, trailingTokens: 0, lastUsageIndex: i }; - } - } - return { tokens: 0, usageTokens: 0, trailingTokens: 0, lastUsageIndex: null }; - }, - generateBranchSummary: async () => ({ summary: "", aborted: false, readFiles: [], modifiedFiles: [] }), - prepareCompaction: () => ({ dummy: true }), - shouldCompact: ( - contextTokens: number, - contextWindow: number, - settings: { enabled: boolean; reserveTokens: number }, - ) => settings.enabled && contextTokens > contextWindow - settings.reserveTokens, -})); - describe("AgentSession auto-compaction queue resume", () => { let session: AgentSession; let sessionManager: SessionManager; + let settingsManager: SettingsManager; let tempDir: string; beforeEach(() => { @@ -73,7 +37,7 @@ describe("AgentSession auto-compaction queue resume", () => { }); sessionManager = SessionManager.inMemory(); - const settingsManager = SettingsManager.create(tempDir, tempDir); + settingsManager = SettingsManager.create(tempDir, tempDir); const authStorage = AuthStorage.create(join(tempDir, "auth.json")); authStorage.setRuntimeApiKey("anthropic", "test-key"); const modelRegistry = ModelRegistry.create(authStorage, tempDir); @@ -98,6 +62,57 @@ describe("AgentSession auto-compaction queue resume", () => { }); it("should resume after threshold compaction when only agent-level queued messages exist", async () => { + settingsManager.applyOverrides({ compaction: { keepRecentTokens: 1 } }); + const model = session.model!; + const now = Date.now(); + sessionManager.appendMessage({ + role: "user", + content: [{ type: "text", text: "message to compact" }], + timestamp: now - 1000, + }); + sessionManager.appendMessage({ + role: "assistant", + content: [{ type: "text", text: "assistant response to compact" }], + api: model.api, + provider: model.provider, + model: model.id, + usage: { + input: 100, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 100, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "stop", + timestamp: now - 500, + }); + session.agent.state.messages = sessionManager.buildSessionContext().messages; + session.agent.streamFn = (summaryModel) => { + const stream = createAssistantMessageEventStream(); + queueMicrotask(() => { + stream.push({ + type: "done", + reason: "stop", + message: { + ...fauxAssistantMessage("compacted"), + api: summaryModel.api, + provider: summaryModel.provider, + model: summaryModel.id, + usage: { + input: 10, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 10, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + }, + }); + }); + return stream; + }; + session.agent.followUp({ role: "custom", customType: "test", From 8025fdd01f718f3dd2624b18089ac3623ee3c628 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 23:36:08 +0200 Subject: [PATCH 033/122] meta: Update readmes slightly --- README.md | 52 ++++++++++++++++----------------- packages/coding-agent/README.md | 13 ++++----- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index dc31b930..130a412f 100644 --- a/README.md +++ b/README.md @@ -5,46 +5,24 @@

Discord -

-

- pi.dev domain graciously donated by -

- Exy mascot
exe.dev
+ npm

> New issues and PRs from new contributors are auto-closed by default. Maintainers review auto-closed issues daily. See [CONTRIBUTING.md](CONTRIBUTING.md). ---- +# Pi Agent Harness -# Pi Agent Harness Mono Repo - -This is the home of the pi agent harness project including our self extensible coding agent. +This is the home of the Pi agent harness project including our self extensible coding agent. * **[@earendil-works/pi-coding-agent](packages/coding-agent)**: Interactive coding agent CLI * **[@earendil-works/pi-agent-core](packages/agent)**: Agent runtime with tool calling and state management * **[@earendil-works/pi-ai](packages/ai)**: Unified multi-provider LLM API (OpenAI, Anthropic, Google, …) -To learn more about pi: +To learn more about Pi: * [Visit pi.dev](https://pi.dev), the project website with demos * [Read the documentation](https://pi.dev/docs/latest), but you can also ask the agent to explain itself -## Share your OSS coding agent sessions - -If you use pi or other coding agents for open source work, please share your sessions. - -Public OSS session data helps improve coding agents with real-world tasks, tool use, failures, and fixes instead of toy benchmarks. - -For the full explanation, see [this post on X](https://x.com/badlogicgames/status/2037811643774652911). - -To publish sessions, use [`badlogic/pi-share-hf`](https://github.com/badlogic/pi-share-hf). Read its README.md for setup instructions. All you need is a Hugging Face account, the Hugging Face CLI, and `pi-share-hf`. - -You can also watch [this video](https://x.com/badlogicgames/status/2041151967695634619), where I show how I publish my `pi-mono` sessions. - -I regularly publish my own `pi-mono` work sessions here: - -- [badlogicgames/pi-mono on Hugging Face](https://huggingface.co/datasets/badlogicgames/pi-mono) - ## All Packages | Package | Description | @@ -94,6 +72,28 @@ We treat npm dependency changes as reviewed code changes. - CI installs with `npm ci --ignore-scripts`, and a scheduled GitHub workflow runs `npm audit --omit=dev` plus `npm audit signatures --omit=dev`. - Shrinkwrap generation has an explicit allowlist for dependency lifecycle scripts; new lifecycle-script deps fail checks until reviewed. +## Share your OSS coding agent sessions + +If you use Pi or other coding agents for open source work, please share your sessions. + +Public OSS session data helps improve coding agents with real-world tasks, tool use, failures, and fixes instead of toy benchmarks. + +For the full explanation, see [this post on X](https://x.com/badlogicgames/status/2037811643774652911). + +To publish sessions, use [`badlogic/pi-share-hf`](https://github.com/badlogic/pi-share-hf). Read its README.md for setup instructions. All you need is a Hugging Face account, the Hugging Face CLI, and `pi-share-hf`. + +You can also watch [this video](https://x.com/badlogicgames/status/2041151967695634619), where I show how I publish my `pi-mono` sessions. + +I regularly publish my own `pi-mono` work sessions here: + +- [badlogicgames/pi-mono on Hugging Face](https://huggingface.co/datasets/badlogicgames/pi-mono) + ## License MIT + +

+ pi.dev domain graciously donated by +

+ Exy mascot
exe.dev
+

diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index ce7dba2a..68e17a62 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -7,11 +7,6 @@ Discord npm

-

- pi.dev domain graciously donated by -

- Exy mascot
exe.dev
-

> New issues and PRs from new contributors are auto-closed by default. Maintainers review auto-closed issues daily. See [CONTRIBUTING.md](../../CONTRIBUTING.md). @@ -675,8 +670,6 @@ pi --thinking high "Solve this complex problem" See [CONTRIBUTING.md](../../CONTRIBUTING.md) for guidelines and [docs/development.md](docs/development.md) for setup, forking, and debugging. ---- - ## License MIT @@ -686,3 +679,9 @@ MIT - [@earendil-works/pi-ai](https://www.npmjs.com/package/@earendil-works/pi-ai): Core LLM toolkit - [@earendil-works/pi-agent-core](https://www.npmjs.com/package/@earendil-works/pi-agent-core): Agent framework - [@earendil-works/pi-tui](https://www.npmjs.com/package/@earendil-works/pi-tui): Terminal UI components + +

+ pi.dev domain graciously donated by +

+ Exy mascot
exe.dev
+

From 651d10d90b4bd80c85c3a09e18a719e4d5dc4c13 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 23:47:25 +0200 Subject: [PATCH 034/122] feat(ai): enable Mistral prompt caching --- package-lock.json | 36 ++++++++++-- packages/ai/CHANGELOG.md | 1 + packages/ai/package.json | 5 +- packages/ai/scripts/generate-models.ts | 2 +- packages/ai/src/models.generated.ts | 58 +++++++++---------- packages/ai/src/providers/mistral.ts | 39 +++++++++++-- .../ai/test/mistral-reasoning-mode.test.ts | 18 ++++++ packages/coding-agent/npm-shrinkwrap.json | 38 ++++++++++-- 8 files changed, 152 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index 821c9d30..d1abbf97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1624,14 +1624,23 @@ } }, "node_modules/@mistralai/mistralai": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", - "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", + "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", "license": "Apache-2.0", "dependencies": { + "@opentelemetry/semantic-conventions": "^1.40.0", "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.25.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } } }, "node_modules/@nodable/entities": { @@ -1684,6 +1693,24 @@ "node": ">= 8" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -6219,7 +6246,8 @@ "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", "@google/genai": "1.52.0", - "@mistralai/mistralai": "2.2.1", + "@mistralai/mistralai": "2.2.6", + "@opentelemetry/api": "1.9.0", "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 2b5aeca1..c4c63d8b 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +- Added prompt caching for Mistral requests using the pi session ID as `prompt_cache_key`, including cached-token usage and cost accounting ([#5854](https://github.com/earendil-works/pi/issues/5854)). - Added the OpenRouter Fusion alias as `openrouter/fusion` ([#5866](https://github.com/earendil-works/pi/pull/5866) by [@dannote](https://github.com/dannote)). ## [0.79.7] - 2026-06-18 diff --git a/packages/ai/package.json b/packages/ai/package.json index 9c71d156..2c0bd6cf 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -69,9 +69,10 @@ "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", - "@smithy/node-http-handler": "4.7.3", "@google/genai": "1.52.0", - "@mistralai/mistralai": "2.2.1", + "@mistralai/mistralai": "2.2.6", + "@opentelemetry/api": "1.9.0", + "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", "openai": "6.26.0", diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 0c1bcc42..c1fddc92 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -948,7 +948,7 @@ async function loadModelsDevData(): Promise[]> { cost: { input: m.cost?.input || 0, output: m.cost?.output || 0, - cacheRead: m.cost?.cache_read || 0, + cacheRead: m.cost?.cache_read ?? (m.cost?.input ? roundCost(m.cost.input * 0.1) : 0), cacheWrite: m.cost?.cache_write || 0, }, contextWindow: m.limit?.context || 4096, diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 8e1b776f..82d0e137 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -5761,7 +5761,7 @@ export const MODELS = { cost: { input: 0.3, output: 0.9, - cacheRead: 0, + cacheRead: 0.03, cacheWrite: 0, }, contextWindow: 256000, @@ -5778,7 +5778,7 @@ export const MODELS = { cost: { input: 0.4, output: 2, - cacheRead: 0, + cacheRead: 0.04, cacheWrite: 0, }, contextWindow: 262144, @@ -5795,7 +5795,7 @@ export const MODELS = { cost: { input: 0.4, output: 2, - cacheRead: 0, + cacheRead: 0.04, cacheWrite: 0, }, contextWindow: 262144, @@ -5812,7 +5812,7 @@ export const MODELS = { cost: { input: 0.4, output: 2, - cacheRead: 0, + cacheRead: 0.04, cacheWrite: 0, }, contextWindow: 128000, @@ -5829,7 +5829,7 @@ export const MODELS = { cost: { input: 0.4, output: 2, - cacheRead: 0, + cacheRead: 0.04, cacheWrite: 0, }, contextWindow: 262144, @@ -5846,7 +5846,7 @@ export const MODELS = { cost: { input: 0.1, output: 0.3, - cacheRead: 0, + cacheRead: 0.01, cacheWrite: 0, }, contextWindow: 128000, @@ -5863,7 +5863,7 @@ export const MODELS = { cost: { input: 0.1, output: 0.3, - cacheRead: 0, + cacheRead: 0.01, cacheWrite: 0, }, contextWindow: 128000, @@ -5897,7 +5897,7 @@ export const MODELS = { cost: { input: 2, output: 5, - cacheRead: 0, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 128000, @@ -5914,7 +5914,7 @@ export const MODELS = { cost: { input: 0.5, output: 1.5, - cacheRead: 0, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 128000, @@ -5931,7 +5931,7 @@ export const MODELS = { cost: { input: 0.04, output: 0.04, - cacheRead: 0, + cacheRead: 0.004, cacheWrite: 0, }, contextWindow: 128000, @@ -5948,7 +5948,7 @@ export const MODELS = { cost: { input: 0.1, output: 0.1, - cacheRead: 0, + cacheRead: 0.01, cacheWrite: 0, }, contextWindow: 128000, @@ -5965,7 +5965,7 @@ export const MODELS = { cost: { input: 2, output: 6, - cacheRead: 0, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 131072, @@ -5982,7 +5982,7 @@ export const MODELS = { cost: { input: 0.5, output: 1.5, - cacheRead: 0, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 262144, @@ -5999,7 +5999,7 @@ export const MODELS = { cost: { input: 0.5, output: 1.5, - cacheRead: 0, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 262144, @@ -6016,7 +6016,7 @@ export const MODELS = { cost: { input: 0.4, output: 2, - cacheRead: 0, + cacheRead: 0.04, cacheWrite: 0, }, contextWindow: 131072, @@ -6033,7 +6033,7 @@ export const MODELS = { cost: { input: 0.4, output: 2, - cacheRead: 0, + cacheRead: 0.04, cacheWrite: 0, }, contextWindow: 262144, @@ -6050,7 +6050,7 @@ export const MODELS = { cost: { input: 1.5, output: 7.5, - cacheRead: 0, + cacheRead: 0.15, cacheWrite: 0, }, contextWindow: 262144, @@ -6067,7 +6067,7 @@ export const MODELS = { cost: { input: 1.5, output: 7.5, - cacheRead: 0, + cacheRead: 0.15, cacheWrite: 0, }, contextWindow: 262144, @@ -6084,7 +6084,7 @@ export const MODELS = { cost: { input: 0.4, output: 2, - cacheRead: 0, + cacheRead: 0.04, cacheWrite: 0, }, contextWindow: 262144, @@ -6101,7 +6101,7 @@ export const MODELS = { cost: { input: 0.15, output: 0.15, - cacheRead: 0, + cacheRead: 0.015, cacheWrite: 0, }, contextWindow: 128000, @@ -6118,7 +6118,7 @@ export const MODELS = { cost: { input: 0.1, output: 0.3, - cacheRead: 0, + cacheRead: 0.01, cacheWrite: 0, }, contextWindow: 128000, @@ -6135,7 +6135,7 @@ export const MODELS = { cost: { input: 0.15, output: 0.6, - cacheRead: 0, + cacheRead: 0.015, cacheWrite: 0, }, contextWindow: 256000, @@ -6152,7 +6152,7 @@ export const MODELS = { cost: { input: 0.15, output: 0.6, - cacheRead: 0, + cacheRead: 0.015, cacheWrite: 0, }, contextWindow: 256000, @@ -6169,7 +6169,7 @@ export const MODELS = { cost: { input: 0.25, output: 0.25, - cacheRead: 0, + cacheRead: 0.025, cacheWrite: 0, }, contextWindow: 8000, @@ -6186,7 +6186,7 @@ export const MODELS = { cost: { input: 0.15, output: 0.15, - cacheRead: 0, + cacheRead: 0.015, cacheWrite: 0, }, contextWindow: 128000, @@ -6203,7 +6203,7 @@ export const MODELS = { cost: { input: 2, output: 6, - cacheRead: 0, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 64000, @@ -6220,7 +6220,7 @@ export const MODELS = { cost: { input: 0.7, output: 0.7, - cacheRead: 0, + cacheRead: 0.07, cacheWrite: 0, }, contextWindow: 32000, @@ -6237,7 +6237,7 @@ export const MODELS = { cost: { input: 0.15, output: 0.15, - cacheRead: 0, + cacheRead: 0.015, cacheWrite: 0, }, contextWindow: 128000, @@ -6254,7 +6254,7 @@ export const MODELS = { cost: { input: 2, output: 6, - cacheRead: 0, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 128000, diff --git a/packages/ai/src/providers/mistral.ts b/packages/ai/src/providers/mistral.ts index 1bc7d4ce..6a132236 100644 --- a/packages/ai/src/providers/mistral.ts +++ b/packages/ai/src/providers/mistral.ts @@ -226,7 +226,7 @@ function buildRequestOptions(model: Model<"mistral-conversations">, options?: Mi // Mistral infrastructure uses `x-affinity` for KV-cache reuse (prefix caching). // Respect explicit caller-provided header values. - if (options?.sessionId && !headers["x-affinity"]) { + if (shouldUsePromptCaching(options) && !headers["x-affinity"]) { headers["x-affinity"] = options.sessionId; } @@ -255,6 +255,7 @@ function buildChatPayload( if (options?.toolChoice) payload.toolChoice = mapToolChoice(options.toolChoice); if (options?.promptMode) payload.promptMode = options.promptMode; if (options?.reasoningEffort) payload.reasoningEffort = options.reasoningEffort; + if (shouldUsePromptCaching(options)) payload.promptCacheKey = options.sessionId; if (context.systemPrompt) { payload.messages.unshift({ @@ -266,6 +267,31 @@ function buildChatPayload( return payload; } +function shouldUsePromptCaching(options?: MistralOptions): options is MistralOptions & { sessionId: string } { + return options?.cacheRetention !== "none" && !!options?.sessionId; +} + +function getMistralCachedPromptTokens(usage: unknown, promptTokens: number): number { + const rawUsage = usage as { + promptTokensDetails?: { cachedTokens?: unknown } | null; + prompt_tokens_details?: { cached_tokens?: unknown } | null; + promptTokenDetails?: { cachedTokens?: unknown } | null; + prompt_token_details?: { cached_tokens?: unknown } | null; + numCachedTokens?: unknown; + num_cached_tokens?: unknown; + }; + const rawCachedTokens = + rawUsage.promptTokensDetails?.cachedTokens ?? + rawUsage.prompt_tokens_details?.cached_tokens ?? + rawUsage.promptTokenDetails?.cachedTokens ?? + rawUsage.prompt_token_details?.cached_tokens ?? + rawUsage.numCachedTokens ?? + rawUsage.num_cached_tokens ?? + 0; + const cachedTokens = typeof rawCachedTokens === "number" && Number.isFinite(rawCachedTokens) ? rawCachedTokens : 0; + return Math.min(promptTokens, Math.max(0, cachedTokens)); +} + async function consumeChatStream( model: Model<"mistral-conversations">, output: AssistantMessage, @@ -305,11 +331,16 @@ async function consumeChatStream( output.responseId ||= chunk.id; if (chunk.usage) { - output.usage.input = chunk.usage.promptTokens || 0; + const promptTokens = chunk.usage.promptTokens || 0; + const cachedPromptTokens = getMistralCachedPromptTokens(chunk.usage, promptTokens); + + output.usage.input = Math.max(0, promptTokens - cachedPromptTokens); output.usage.output = chunk.usage.completionTokens || 0; - output.usage.cacheRead = 0; + output.usage.cacheRead = cachedPromptTokens; output.usage.cacheWrite = 0; - output.usage.totalTokens = chunk.usage.totalTokens || output.usage.input + output.usage.output; + output.usage.totalTokens = + chunk.usage.totalTokens || + output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite; calculateCost(model, output.usage); } diff --git a/packages/ai/test/mistral-reasoning-mode.test.ts b/packages/ai/test/mistral-reasoning-mode.test.ts index 35a5b96b..d197a56a 100644 --- a/packages/ai/test/mistral-reasoning-mode.test.ts +++ b/packages/ai/test/mistral-reasoning-mode.test.ts @@ -6,6 +6,7 @@ import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface MistralPayload { promptMode?: "reasoning"; reasoningEffort?: "none" | "high"; + promptCacheKey?: string; } function makeContext(): Context { @@ -77,4 +78,21 @@ describe("Mistral reasoning mode selection", () => { expect(payload.reasoningEffort).toBeUndefined(); expect(payload.promptMode).toBeUndefined(); }); + + it("uses the session id as prompt cache key", async () => { + const payload = await capturePayload(getModel("mistral", "mistral-large-latest"), { + sessionId: "session-123", + }); + + expect(payload.promptCacheKey).toBe("session-123"); + }); + + it("omits prompt cache key when cache retention is disabled", async () => { + const payload = await capturePayload(getModel("mistral", "mistral-large-latest"), { + sessionId: "session-123", + cacheRetention: "none", + }); + + expect(payload.promptCacheKey).toBeUndefined(); + }); }); diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index c366f1a5..ad672bf2 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -494,9 +494,10 @@ "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", - "@smithy/node-http-handler": "4.7.3", "@google/genai": "1.52.0", - "@mistralai/mistralai": "2.2.1", + "@mistralai/mistralai": "2.2.6", + "@opentelemetry/api": "1.9.0", + "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", "openai": "6.26.0", @@ -741,14 +742,23 @@ "optional": true }, "node_modules/@mistralai/mistralai": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", - "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", + "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", "license": "Apache-2.0", "dependencies": { + "@opentelemetry/semantic-conventions": "^1.40.0", "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.25.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } } }, "node_modules/@nodable/entities": { @@ -763,6 +773,24 @@ } ] }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", From 9179734cc205fb40a4deaa70e4cd586529cdf927 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 23:58:34 +0200 Subject: [PATCH 035/122] docs(coding-agent): audit unreleased changelog --- packages/coding-agent/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 5f0c6186..06ab4f9b 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,11 +4,13 @@ ### New Features +- **Mistral prompt caching** - Mistral sessions now use provider-side prompt caching with session affinity and cached-token usage/cost accounting. See [API Keys](docs/providers.md#api-keys) and [Environment Variables](docs/usage.md#environment-variables). - **Post-compaction token estimates** - Compact results and compaction events now include estimated post-compaction token counts so clients can show the approximate context reduction. See [RPC compact](docs/rpc.md#compact) and [compaction events](docs/rpc.md#compaction_start--compaction_end). - **OpenRouter Fusion alias** - `openrouter/fusion` is available as a built-in OpenRouter model alias. See [API Keys](docs/providers.md#api-keys). ### Added +- Added inherited Mistral prompt caching using the pi session ID as `prompt_cache_key`, including cached-token usage and cost accounting ([#5854](https://github.com/earendil-works/pi/issues/5854)). - Added estimated post-compaction token counts to compact results and compaction events ([#5877](https://github.com/earendil-works/pi/issues/5877)). - Added the inherited OpenRouter Fusion alias as `openrouter/fusion` ([#5866](https://github.com/earendil-works/pi/pull/5866) by [@dannote](https://github.com/dannote)). From 1a418ad223a13a58aab10549f0dcf15a84670c8f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 19 Jun 2026 00:26:30 +0200 Subject: [PATCH 036/122] chore: remove inprogress label on close --- .../workflows/remove-inprogress-on-close.yml | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/remove-inprogress-on-close.yml diff --git a/.github/workflows/remove-inprogress-on-close.yml b/.github/workflows/remove-inprogress-on-close.yml new file mode 100644 index 00000000..e94b10e2 --- /dev/null +++ b/.github/workflows/remove-inprogress-on-close.yml @@ -0,0 +1,31 @@ +name: Remove In Progress Label On Close + +on: + issues: + types: [closed] + +jobs: + remove-label: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Remove inprogress label + uses: actions/github-script@v7 + with: + script: | + const labelName = 'inprogress'; + const labels = context.payload.issue.labels ?? []; + const hasLabel = labels.some((label) => label.name === labelName); + + if (!hasLabel) { + console.log(`Issue does not have ${labelName} label`); + return; + } + + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + name: labelName, + }); From 0d89a333739d35254cc62e5579a8cf526c096550 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" <622227+FredKSchott@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:41:49 -0700 Subject: [PATCH 037/122] feat(packages): Add selective pi-ai base entrypoints (#5348) --- packages/agent/README.md | 18 + packages/agent/package.json | 4 + packages/agent/src/agent-loop.ts | 2 +- packages/agent/src/agent.ts | 2 +- packages/agent/src/base.ts | 39 ++ packages/agent/src/harness/agent-harness.ts | 2 +- .../compaction/branch-summarization.ts | 3 +- .../src/harness/compaction/compaction.ts | 10 +- .../agent/src/harness/compaction/utils.ts | 2 +- packages/agent/src/harness/messages.ts | 2 +- packages/agent/src/harness/session/session.ts | 2 +- packages/agent/src/harness/types.ts | 4 +- packages/agent/src/index.ts | 49 +- packages/agent/src/proxy.ts | 2 +- packages/agent/src/types.ts | 2 +- packages/agent/vitest.config.ts | 6 + packages/agent/vitest.harness.config.ts | 6 + packages/ai/README.md | 32 +- packages/ai/package.json | 12 + packages/ai/src/base.ts | 45 ++ packages/ai/src/bedrock-provider.ts | 4 +- packages/ai/src/images-api-registry.ts | 4 + packages/ai/src/images.ts | 2 - packages/ai/src/index.ts | 49 +- packages/ai/src/providers/amazon-bedrock.ts | 9 + packages/ai/src/providers/anthropic.ts | 9 + .../src/providers/azure-openai-responses.ts | 9 + packages/ai/src/providers/google-vertex.ts | 9 + packages/ai/src/providers/google.ts | 9 + .../ai/src/providers/images/openrouter.ts | 8 + .../src/providers/images/register-builtins.ts | 50 -- packages/ai/src/providers/mistral.ts | 9 + .../src/providers/openai-codex-responses.ts | 9 + .../ai/src/providers/openai-completions.ts | 9 + packages/ai/src/providers/openai-responses.ts | 9 + .../ai/src/providers/register-builtins.ts | 464 +++++++----------- packages/ai/src/stream.ts | 4 - packages/ai/test/abort.test.ts | 2 +- .../anthropic-eager-tool-input-e2e.test.ts | 2 +- ...ic-empty-thinking-signature-compat.test.ts | 2 +- .../anthropic-force-adaptive-thinking.test.ts | 2 +- ...anthropic-long-cache-retention-e2e.test.ts | 2 +- .../ai/test/anthropic-opus-4-8-smoke.test.ts | 2 +- .../test/anthropic-temperature-compat.test.ts | 2 +- .../test/anthropic-thinking-disable.test.ts | 2 +- .../anthropic-tool-name-normalization.test.ts | 2 +- packages/ai/test/base-entrypoint.test.ts | 68 +++ packages/ai/test/bedrock-models.test.ts | 2 +- packages/ai/test/cache-retention.test.ts | 2 +- packages/ai/test/context-overflow.test.ts | 2 +- .../ai/test/cross-provider-handoff.test.ts | 2 +- packages/ai/test/empty.test.ts | 2 +- .../ai/test/google-thinking-disable.test.ts | 2 +- packages/ai/test/interleaved-thinking.test.ts | 2 +- packages/ai/test/lazy-module-load.test.ts | 52 +- .../ai/test/mistral-reasoning-mode.test.ts | 2 +- packages/ai/test/mistral-tool-schema.test.ts | 2 +- .../openai-codex-cache-affinity-e2e.test.ts | 2 +- .../openai-completions-empty-tools.test.ts | 2 +- .../openai-completions-response-model.test.ts | 2 +- .../openai-completions-tool-choice.test.ts | 2 +- ...penai-responses-cache-affinity-e2e.test.ts | 2 +- ...nai-responses-reasoning-replay-e2e.test.ts | 2 +- .../test/openrouter-cache-write-repro.test.ts | 2 +- packages/ai/test/openrouter-images.test.ts | 12 + packages/ai/test/responseid.test.ts | 2 +- packages/ai/test/stream.test.ts | 2 +- packages/ai/test/tokens.test.ts | 2 +- .../test/tool-call-id-normalization.test.ts | 2 +- .../ai/test/tool-call-without-result.test.ts | 2 +- packages/ai/test/total-tokens.test.ts | 2 +- packages/ai/test/unicode-surrogate.test.ts | 2 +- packages/ai/test/xhigh.test.ts | 2 +- ...ms-anthropic-empty-signature-smoke.test.ts | 2 +- packages/ai/test/zen.test.ts | 2 +- packages/coding-agent/vitest.config.ts | 9 + scripts/check-browser-smoke.mjs | 44 ++ tsconfig.json | 1 + 78 files changed, 671 insertions(+), 495 deletions(-) create mode 100644 packages/agent/src/base.ts create mode 100644 packages/ai/src/base.ts delete mode 100644 packages/ai/src/providers/images/register-builtins.ts create mode 100644 packages/ai/test/base-entrypoint.test.ts diff --git a/packages/agent/README.md b/packages/agent/README.md index f0fedb81..ca4c1080 100644 --- a/packages/agent/README.md +++ b/packages/agent/README.md @@ -31,6 +31,24 @@ agent.subscribe((event) => { await agent.prompt("Hello!"); ``` +## Base Entry Point + +Use `@earendil-works/pi-agent-core/base` with `@earendil-works/pi-ai/base` when bundling applications that should include only selected provider transports: + +```typescript +import { Agent } from "@earendil-works/pi-agent-core/base"; +import { getModel } from "@earendil-works/pi-ai/base"; +import { register } from "@earendil-works/pi-ai/anthropic"; + +register(); + +const agent = new Agent({ + initialState: { model: getModel("anthropic", "claude-sonnet-4-6") }, +}); +``` + +The default `@earendil-works/pi-agent-core` entry point remains batteries-included and registers pi-ai's lazy built-in transports for backward compatibility. + ## Core Concepts ### AgentMessage vs LLM Message diff --git a/packages/agent/package.json b/packages/agent/package.json index 81d3e356..48934f8f 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -10,6 +10,10 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js" }, + "./base": { + "types": "./dist/base.d.ts", + "import": "./dist/base.js" + }, "./node": { "types": "./dist/node.d.ts", "import": "./dist/node.js" diff --git a/packages/agent/src/agent-loop.ts b/packages/agent/src/agent-loop.ts index 77bd9568..8a8f20f1 100644 --- a/packages/agent/src/agent-loop.ts +++ b/packages/agent/src/agent-loop.ts @@ -10,7 +10,7 @@ import { streamSimple, type ToolResultMessage, validateToolArguments, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/base"; import type { AgentContext, AgentEvent, diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index db6684a8..ef26e213 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -7,7 +7,7 @@ import { type TextContent, type ThinkingBudgets, type Transport, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/base"; import { runAgentLoop, runAgentLoopContinue } from "./agent-loop.ts"; import type { AfterToolCallContext, diff --git a/packages/agent/src/base.ts b/packages/agent/src/base.ts new file mode 100644 index 00000000..ad1abafa --- /dev/null +++ b/packages/agent/src/base.ts @@ -0,0 +1,39 @@ +export * from "./agent.ts"; +export * from "./agent-loop.ts"; +export * from "./harness/agent-harness.ts"; +export { + type BranchPreparation, + type BranchSummaryDetails, + type CollectEntriesResult, + collectEntriesForBranchSummary, + generateBranchSummary, + prepareBranchEntries, +} from "./harness/compaction/branch-summarization.ts"; +export { + calculateContextTokens, + compact, + DEFAULT_COMPACTION_SETTINGS, + estimateContextTokens, + estimateTokens, + findCutPoint, + findTurnStartIndex, + generateSummary, + getLastAssistantUsage, + prepareCompaction, + serializeConversation, + shouldCompact, +} from "./harness/compaction/compaction.ts"; +export * from "./harness/messages.ts"; +export * from "./harness/prompt-templates.ts"; +export * from "./harness/session/jsonl-repo.ts"; +export * from "./harness/session/memory-repo.ts"; +export * from "./harness/session/repo-utils.ts"; +export * from "./harness/session/session.ts"; +export { uuidv7 } from "./harness/session/uuid.ts"; +export * from "./harness/skills.ts"; +export * from "./harness/system-prompt.ts"; +export * from "./harness/types.ts"; +export * from "./harness/utils/shell-output.ts"; +export * from "./harness/utils/truncate.ts"; +export * from "./proxy.ts"; +export * from "./types.ts"; diff --git a/packages/agent/src/harness/agent-harness.ts b/packages/agent/src/harness/agent-harness.ts index 96563465..f097ee75 100644 --- a/packages/agent/src/harness/agent-harness.ts +++ b/packages/agent/src/harness/agent-harness.ts @@ -4,7 +4,7 @@ import { type Model, streamSimple, type UserMessage, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/base"; import { runAgentLoop } from "../agent-loop.ts"; import type { AgentContext, diff --git a/packages/agent/src/harness/compaction/branch-summarization.ts b/packages/agent/src/harness/compaction/branch-summarization.ts index c1824ebf..60ffe771 100644 --- a/packages/agent/src/harness/compaction/branch-summarization.ts +++ b/packages/agent/src/harness/compaction/branch-summarization.ts @@ -1,5 +1,4 @@ -import type { Model } from "@earendil-works/pi-ai"; -import { completeSimple } from "@earendil-works/pi-ai"; +import { completeSimple, type Model } from "@earendil-works/pi-ai/base"; import type { AgentMessage } from "../../types.ts"; import { convertToLlm, diff --git a/packages/agent/src/harness/compaction/compaction.ts b/packages/agent/src/harness/compaction/compaction.ts index dba753d7..d619b34a 100644 --- a/packages/agent/src/harness/compaction/compaction.ts +++ b/packages/agent/src/harness/compaction/compaction.ts @@ -1,5 +1,11 @@ -import type { AssistantMessage, ImageContent, Model, TextContent, Usage } from "@earendil-works/pi-ai"; -import { completeSimple } from "@earendil-works/pi-ai"; +import { + type AssistantMessage, + completeSimple, + type ImageContent, + type Model, + type TextContent, + type Usage, +} from "@earendil-works/pi-ai/base"; import type { AgentMessage, ThinkingLevel } from "../../types.ts"; import { convertToLlm, diff --git a/packages/agent/src/harness/compaction/utils.ts b/packages/agent/src/harness/compaction/utils.ts index 07535b79..388dd84a 100644 --- a/packages/agent/src/harness/compaction/utils.ts +++ b/packages/agent/src/harness/compaction/utils.ts @@ -1,4 +1,4 @@ -import type { Message } from "@earendil-works/pi-ai"; +import type { Message } from "@earendil-works/pi-ai/base"; import type { AgentMessage } from "../../types.ts"; /** File paths touched by a session branch or compaction range. */ diff --git a/packages/agent/src/harness/messages.ts b/packages/agent/src/harness/messages.ts index 36ce96a1..8368dd34 100644 --- a/packages/agent/src/harness/messages.ts +++ b/packages/agent/src/harness/messages.ts @@ -1,4 +1,4 @@ -import type { ImageContent, Message, TextContent } from "@earendil-works/pi-ai"; +import type { ImageContent, Message, TextContent } from "@earendil-works/pi-ai/base"; import type { AgentMessage } from "../types.ts"; export const COMPACTION_SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary: diff --git a/packages/agent/src/harness/session/session.ts b/packages/agent/src/harness/session/session.ts index 6f136208..3fd76c6b 100644 --- a/packages/agent/src/harness/session/session.ts +++ b/packages/agent/src/harness/session/session.ts @@ -1,4 +1,4 @@ -import type { ImageContent, TextContent } from "@earendil-works/pi-ai"; +import type { ImageContent, TextContent } from "@earendil-works/pi-ai/base"; import type { AgentMessage } from "../../types.ts"; import { createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage } from "../messages.ts"; import type { diff --git a/packages/agent/src/harness/types.ts b/packages/agent/src/harness/types.ts index 4756ca84..8f281570 100644 --- a/packages/agent/src/harness/types.ts +++ b/packages/agent/src/harness/types.ts @@ -1,5 +1,5 @@ -import type { ImageContent, Model, SimpleStreamOptions, TextContent, Transport } from "@earendil-works/pi-ai"; -import type { AgentEvent, AgentMessage, AgentTool, QueueMode, ThinkingLevel } from "../index.ts"; +import type { ImageContent, Model, SimpleStreamOptions, TextContent, Transport } from "@earendil-works/pi-ai/base"; +import type { AgentEvent, AgentMessage, AgentTool, QueueMode, ThinkingLevel } from "../types.ts"; import type { Session } from "./session/session.ts"; /** Result of a fallible operation. Expected failures are returned as `ok: false` instead of thrown. */ diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index fd3c2b9f..08b7b9cb 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -1,44 +1,5 @@ -// Core Agent -export * from "./agent.ts"; -// Loop functions -export * from "./agent-loop.ts"; -export * from "./harness/agent-harness.ts"; -export { - type BranchPreparation, - type BranchSummaryDetails, - type CollectEntriesResult, - collectEntriesForBranchSummary, - generateBranchSummary, - prepareBranchEntries, -} from "./harness/compaction/branch-summarization.ts"; -export { - calculateContextTokens, - compact, - DEFAULT_COMPACTION_SETTINGS, - estimateContextTokens, - estimateTokens, - findCutPoint, - findTurnStartIndex, - generateSummary, - getLastAssistantUsage, - prepareCompaction, - serializeConversation, - shouldCompact, -} from "./harness/compaction/compaction.ts"; -export * from "./harness/messages.ts"; -export * from "./harness/prompt-templates.ts"; -export * from "./harness/session/jsonl-repo.ts"; -export * from "./harness/session/memory-repo.ts"; -export * from "./harness/session/repo-utils.ts"; -export * from "./harness/session/session.ts"; -export { uuidv7 } from "./harness/session/uuid.ts"; -export * from "./harness/skills.ts"; -export * from "./harness/system-prompt.ts"; -// Harness -export * from "./harness/types.ts"; -export * from "./harness/utils/shell-output.ts"; -export * from "./harness/utils/truncate.ts"; -// Proxy utilities -export * from "./proxy.ts"; -// Types -export * from "./types.ts"; +// Import the default pi-ai entrypoint so that all built-in providers register +// automatically. Unlike "@earendil-works/pi-ai/base" which does not. +import "@earendil-works/pi-ai"; + +export * from "./base.ts"; diff --git a/packages/agent/src/proxy.ts b/packages/agent/src/proxy.ts index 5f0925c9..8e5cb2c2 100644 --- a/packages/agent/src/proxy.ts +++ b/packages/agent/src/proxy.ts @@ -14,7 +14,7 @@ import { type SimpleStreamOptions, type StopReason, type ToolCall, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/base"; // Create stream class matching ProxyMessageEventStream class ProxyMessageEventStream extends EventStream { diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts index cb99a79a..541d8d63 100644 --- a/packages/agent/src/types.ts +++ b/packages/agent/src/types.ts @@ -9,7 +9,7 @@ import type { TextContent, Tool, ToolResultMessage, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/base"; import type { Static, TSchema } from "typebox"; /** diff --git a/packages/agent/vitest.config.ts b/packages/agent/vitest.config.ts index bcc497fa..297fbd50 100644 --- a/packages/agent/vitest.config.ts +++ b/packages/agent/vitest.config.ts @@ -1,6 +1,12 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ + resolve: { + alias: { + "@earendil-works/pi-ai/base": new URL("../ai/src/base.ts", import.meta.url).pathname, + "@earendil-works/pi-ai": new URL("../ai/src/index.ts", import.meta.url).pathname, + }, + }, test: { globals: true, environment: "node", diff --git a/packages/agent/vitest.harness.config.ts b/packages/agent/vitest.harness.config.ts index 9421e5a9..37acc34d 100644 --- a/packages/agent/vitest.harness.config.ts +++ b/packages/agent/vitest.harness.config.ts @@ -1,6 +1,12 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ + resolve: { + alias: { + "@earendil-works/pi-ai/base": new URL("../ai/src/base.ts", import.meta.url).pathname, + "@earendil-works/pi-ai": new URL("../ai/src/index.ts", import.meta.url).pathname, + }, + }, test: { globals: true, environment: "node", diff --git a/packages/ai/README.md b/packages/ai/README.md index a7b8028e..8137ccd4 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -8,6 +8,7 @@ Unified LLM API with automatic model discovery, provider configuration, token an - [Supported Providers](#supported-providers) - [Installation](#installation) +- [Base Entry Point](#base-entry-point) - [Quick Start](#quick-start) - [Tools](#tools) - [Defining Tools](#defining-tools) @@ -88,6 +89,30 @@ npm install @earendil-works/pi-ai TypeBox exports are re-exported from `@earendil-works/pi-ai`: `Type`, `Static`, and `TSchema`. +## Base Entry Point + +Use `@earendil-works/pi-ai/base` when a bundler should include only explicitly selected transport implementations. The base entry point exposes model discovery, registries, generic dispatch, environment-key helpers, and provider-neutral utilities without registering built-in transports during module initialization. Generic dispatch still resolves configured environment keys when called. + +Import each selected transport directly and call its `register()` function: + +```typescript +import { getModel, streamSimple } from '@earendil-works/pi-ai/base'; +import { register as registerAnthropic } from '@earendil-works/pi-ai/anthropic'; + +registerAnthropic(); + +const model = getModel('anthropic', 'claude-sonnet-4-6'); +const response = await streamSimple(model, { + messages: [{ role: 'user', content: 'Hello!' }] +}, { + apiKey: process.env.ANTHROPIC_API_KEY +}).result(); +``` + +Direct transport imports bundle their implementation and SDK code. Register only the transports used by the application. For example, OpenRouter, Groq, xAI, and DeepSeek chat models share the `@earendil-works/pi-ai/openai-completions` transport. + +Use `@earendil-works/pi-ai` for the batteries-included behavior. The root entry point remains backward-compatible: it registers lazy wrappers for all built-in transports and resolves environment API keys automatically during dispatch. + ## Quick Start ```typescript @@ -438,7 +463,7 @@ Do not use `stream()` or `complete()` for image generation. Image generation is ### Basic Image Generation ```typescript -import { getImageModel, generateImages } from '@mariozechner/pi-ai'; +import { getImageModel, generateImages } from '@earendil-works/pi-ai'; const model = getImageModel('openrouter', 'google/gemini-2.5-flash-image'); @@ -1097,6 +1122,7 @@ const response = await complete(model, { - OAuth login flows are not supported in browser environments. Use the `@earendil-works/pi-ai/oauth` entry point in Node.js. - In browser builds, Bedrock can still appear in model lists. Calls to Bedrock models fail at runtime. - Use a server-side proxy or backend service if you need Bedrock or OAuth-based auth from a web app. +- Use `@earendil-works/pi-ai/base` plus explicit direct transport registration when a browser bundle should exclude unused provider SDK implementations. ### Environment Variables (Node.js only) @@ -1333,6 +1359,7 @@ Create a new provider file (for example `amazon-bedrock.ts`) that exports: - `stream()` function returning `AssistantMessageEventStream` - `streamSimple()` for `SimpleStreamOptions` mapping +- `register()` for explicit direct transport registration from `@earendil-works/pi-ai/base` - Provider-specific options interface - Message conversion functions to transform `Context` to provider format - Tool conversion if the provider supports tools @@ -1343,7 +1370,8 @@ Create a new provider file (for example `amazon-bedrock.ts`) that exports: - Register the API with `registerApiProvider()` - Add a package subpath export in `package.json` for the provider module (`./dist/providers/.js`) - Add lazy loader wrappers in `src/providers/register-builtins.ts`, do not statically import provider implementation modules there -- Add any root-level `export type` re-exports in `src/index.ts` that should remain available from `@earendil-works/pi-ai` +- Add any root-level `export type` re-exports in `src/index.ts` and `src/base.ts` that should remain available from `@earendil-works/pi-ai` and `@earendil-works/pi-ai/base` +- Keep `src/base.ts` free of built-in registration imports - Add credential detection in `env-api-keys.ts` for the new provider - Ensure `streamSimple` handles auth lookup via `getEnvApiKey()` or provider-specific auth diff --git a/packages/ai/package.json b/packages/ai/package.json index 2c0bd6cf..1b34843a 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -10,6 +10,14 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js" }, + "./base": { + "types": "./dist/base.d.ts", + "import": "./dist/base.js" + }, + "./amazon-bedrock": { + "types": "./dist/providers/amazon-bedrock.d.ts", + "import": "./dist/providers/amazon-bedrock.js" + }, "./anthropic": { "types": "./dist/providers/anthropic.d.ts", "import": "./dist/providers/anthropic.js" @@ -42,6 +50,10 @@ "types": "./dist/providers/openai-responses.d.ts", "import": "./dist/providers/openai-responses.js" }, + "./openrouter-images": { + "types": "./dist/providers/images/openrouter.d.ts", + "import": "./dist/providers/images/openrouter.js" + }, "./oauth": { "types": "./dist/oauth.d.ts", "import": "./dist/oauth.js" diff --git a/packages/ai/src/base.ts b/packages/ai/src/base.ts new file mode 100644 index 00000000..f1a8a110 --- /dev/null +++ b/packages/ai/src/base.ts @@ -0,0 +1,45 @@ +export type { Static, TSchema } from "typebox"; +export { Type } from "typebox"; + +export * from "./api-registry.ts"; +export * from "./env-api-keys.ts"; +export * from "./image-models.ts"; +export * from "./images.ts"; +export * from "./images-api-registry.ts"; +export * from "./models.ts"; +export type { BedrockOptions, BedrockThinkingDisplay } from "./providers/amazon-bedrock.ts"; +export type { AnthropicEffort, AnthropicOptions, AnthropicThinkingDisplay } from "./providers/anthropic.ts"; +export type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses.ts"; +export * from "./providers/faux.ts"; +export type { GoogleOptions } from "./providers/google.ts"; +export type { GoogleThinkingLevel } from "./providers/google-shared.ts"; +export type { GoogleVertexOptions } from "./providers/google-vertex.ts"; +export type { MistralOptions } from "./providers/mistral.ts"; +export type { + OpenAICodexResponsesOptions, + OpenAICodexWebSocketDebugStats, +} from "./providers/openai-codex-responses.ts"; +export type { OpenAICompletionsOptions } from "./providers/openai-completions.ts"; +export type { OpenAIResponsesOptions } from "./providers/openai-responses.ts"; +export * from "./session-resources.ts"; +export * from "./stream.ts"; +export * from "./types.ts"; +export * from "./utils/diagnostics.ts"; +export * from "./utils/event-stream.ts"; +export * from "./utils/json-parse.ts"; +export type { + OAuthAuthInfo, + OAuthCredentials, + OAuthDeviceCodeInfo, + OAuthLoginCallbacks, + OAuthPrompt, + OAuthProvider, + OAuthProviderId, + OAuthProviderInfo, + OAuthProviderInterface, + OAuthSelectOption, + OAuthSelectPrompt, +} from "./utils/oauth/types.ts"; +export * from "./utils/overflow.ts"; +export * from "./utils/typebox-helpers.ts"; +export * from "./utils/validation.ts"; diff --git a/packages/ai/src/bedrock-provider.ts b/packages/ai/src/bedrock-provider.ts index cf08b33e..f5aa3998 100644 --- a/packages/ai/src/bedrock-provider.ts +++ b/packages/ai/src/bedrock-provider.ts @@ -1,4 +1,6 @@ -import { streamBedrock, streamSimpleBedrock } from "./providers/amazon-bedrock.ts"; +import { register, streamBedrock, streamSimpleBedrock } from "./providers/amazon-bedrock.ts"; + +export { register }; export const bedrockProviderModule = { streamBedrock, diff --git a/packages/ai/src/images-api-registry.ts b/packages/ai/src/images-api-registry.ts index 4758439b..5335a926 100644 --- a/packages/ai/src/images-api-registry.ts +++ b/packages/ai/src/images-api-registry.ts @@ -51,3 +51,7 @@ export function registerImagesApiProvider, project: string, diff --git a/packages/ai/src/providers/google.ts b/packages/ai/src/providers/google.ts index a270792a..62ad9cfd 100644 --- a/packages/ai/src/providers/google.ts +++ b/packages/ai/src/providers/google.ts @@ -4,6 +4,7 @@ import { GoogleGenAI, type ThinkingConfig, } from "@google/genai"; +import { registerApiProvider } from "../api-registry.ts"; import { calculateCost, clampThinkingLevel } from "../models.ts"; import type { Api, @@ -315,6 +316,14 @@ export const streamSimpleGoogle: StreamFunction<"google-generative-ai", SimpleSt } satisfies GoogleOptions); }; +export function register(): void { + registerApiProvider({ + api: "google-generative-ai", + stream: streamGoogle, + streamSimple: streamSimpleGoogle, + }); +} + function createClient( model: Model<"google-generative-ai">, apiKey?: string, diff --git a/packages/ai/src/providers/images/openrouter.ts b/packages/ai/src/providers/images/openrouter.ts index 54caeaf0..d1b2c7e8 100644 --- a/packages/ai/src/providers/images/openrouter.ts +++ b/packages/ai/src/providers/images/openrouter.ts @@ -6,6 +6,7 @@ import type { ChatCompletionContentPartText, ChatCompletionCreateParamsNonStreaming, } from "openai/resources/chat/completions.js"; +import { registerImagesApiProvider } from "../../images-api-registry.ts"; import type { AssistantImages, ImageContent, @@ -103,6 +104,13 @@ export const generateImagesOpenRouter: ImagesFunction<"openrouter-images", Image } }; +export function register(): void { + registerImagesApiProvider({ + api: "openrouter-images", + generateImages: generateImagesOpenRouter, + }); +} + function createClient( model: ImagesModel<"openrouter-images">, apiKey: string, diff --git a/packages/ai/src/providers/images/register-builtins.ts b/packages/ai/src/providers/images/register-builtins.ts deleted file mode 100644 index e3decbb9..00000000 --- a/packages/ai/src/providers/images/register-builtins.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { registerImagesApiProvider } from "../../images-api-registry.ts"; -import type { AssistantImages, ImagesContext, ImagesFunction, ImagesModel, ImagesOptions } from "../../types.ts"; -import type { generateImagesOpenRouter as generateImagesOpenRouterFunction } from "./openrouter.ts"; - -interface OpenRouterImagesProviderModule { - generateImagesOpenRouter: typeof generateImagesOpenRouterFunction; -} - -let openRouterImagesProviderModulePromise: Promise | undefined; - -function createLazyLoadErrorImages(model: ImagesModel<"openrouter-images">, error: unknown): AssistantImages { - return { - api: model.api, - provider: model.provider, - model: model.id, - output: [], - stopReason: "error", - errorMessage: error instanceof Error ? error.message : String(error), - timestamp: Date.now(), - }; -} - -function loadOpenRouterImagesProviderModule(): Promise { - openRouterImagesProviderModulePromise ||= import("./openrouter.ts").then( - (module) => module as OpenRouterImagesProviderModule, - ); - return openRouterImagesProviderModulePromise; -} - -export const generateImagesOpenRouter: ImagesFunction<"openrouter-images", ImagesOptions> = async ( - model: ImagesModel<"openrouter-images">, - context: ImagesContext, - options?: ImagesOptions, -) => { - try { - const module = await loadOpenRouterImagesProviderModule(); - return await module.generateImagesOpenRouter(model, context, options); - } catch (error) { - return createLazyLoadErrorImages(model, error); - } -}; - -export function registerBuiltInImagesApiProviders(): void { - registerImagesApiProvider({ - api: "openrouter-images", - generateImages: generateImagesOpenRouter, - }); -} - -registerBuiltInImagesApiProviders(); diff --git a/packages/ai/src/providers/mistral.ts b/packages/ai/src/providers/mistral.ts index 6a132236..ba331af4 100644 --- a/packages/ai/src/providers/mistral.ts +++ b/packages/ai/src/providers/mistral.ts @@ -6,6 +6,7 @@ import type { ContentChunk, FunctionTool, } from "@mistralai/mistralai/models/components"; +import { registerApiProvider } from "../api-registry.ts"; import { calculateCost, clampThinkingLevel } from "../models.ts"; import type { AssistantMessage, @@ -130,6 +131,14 @@ export const streamSimpleMistral: StreamFunction<"mistral-conversations", Simple } satisfies MistralOptions); }; +export function register(): void { + registerApiProvider({ + api: "mistral-conversations", + stream: streamMistral, + streamSimple: streamSimpleMistral, + }); +} + function createOutput(model: Model<"mistral-conversations">): AssistantMessage { return { role: "assistant", diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index 19a2f5d7..1bc2879a 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -20,6 +20,7 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version }); } +import { registerApiProvider } from "../api-registry.ts"; import { clampThinkingLevel } from "../models.ts"; import { registerSessionResourceCleanup } from "../session-resources.ts"; import type { @@ -428,6 +429,14 @@ export const streamSimpleOpenAICodexResponses: StreamFunction<"openai-codex-resp } satisfies OpenAICodexResponsesOptions); }; +export function register(): void { + registerApiProvider({ + api: "openai-codex-responses", + stream: streamOpenAICodexResponses, + streamSimple: streamSimpleOpenAICodexResponses, + }); +} + // ============================================================================ // Request Building // ============================================================================ diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 61b03631..2f5edb62 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -10,6 +10,7 @@ import type { ChatCompletionSystemMessageParam, ChatCompletionToolMessageParam, } from "openai/resources/chat/completions.js"; +import { registerApiProvider } from "../api-registry.ts"; import { calculateCost, clampThinkingLevel } from "../models.ts"; import type { AssistantMessage, @@ -449,6 +450,14 @@ export const streamSimpleOpenAICompletions: StreamFunction<"openai-completions", } satisfies OpenAICompletionsOptions); }; +export function register(): void { + registerApiProvider({ + api: "openai-completions", + stream: streamOpenAICompletions, + streamSimple: streamSimpleOpenAICompletions, + }); +} + function createClient( model: Model<"openai-completions">, context: Context, diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index 014233d5..b1692978 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -1,5 +1,6 @@ import OpenAI from "openai"; import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js"; +import { registerApiProvider } from "../api-registry.ts"; import { clampThinkingLevel } from "../models.ts"; import type { Api, @@ -181,6 +182,14 @@ export const streamSimpleOpenAIResponses: StreamFunction<"openai-responses", Sim } satisfies OpenAIResponsesOptions); }; +export function register(): void { + registerApiProvider({ + api: "openai-responses", + stream: streamOpenAIResponses, + streamSimple: streamSimpleOpenAIResponses, + }); +} + function createClient( model: Model<"openai-responses">, context: Context, diff --git a/packages/ai/src/providers/register-builtins.ts b/packages/ai/src/providers/register-builtins.ts index 8fdcaaf0..6192f8a2 100644 --- a/packages/ai/src/providers/register-builtins.ts +++ b/packages/ai/src/providers/register-builtins.ts @@ -1,9 +1,14 @@ -import { clearApiProviders, registerApiProvider } from "../api-registry.ts"; +import { type ApiProvider, clearApiProviders, getApiProvider, registerApiProvider } from "../api-registry.ts"; +import { getImagesApiProvider, type ImagesApiProvider, registerImagesApiProvider } from "../images-api-registry.ts"; import type { Api, + AssistantImages, AssistantMessage, AssistantMessageEvent, - Context, + ImagesApi, + ImagesContext, + ImagesModel, + ImagesOptions, Model, SimpleStreamOptions, StreamFunction, @@ -20,70 +25,47 @@ import type { OpenAICodexResponsesOptions } from "./openai-codex-responses.ts"; import type { OpenAICompletionsOptions } from "./openai-completions.ts"; import type { OpenAIResponsesOptions } from "./openai-responses.ts"; -interface LazyProviderModule< - TApi extends Api, - TOptions extends StreamOptions, - TSimpleOptions extends SimpleStreamOptions, -> { - stream: (model: Model, context: Context, options?: TOptions) => AsyncIterable; - streamSimple: ( - model: Model, - context: Context, - options?: TSimpleOptions, - ) => AsyncIterable; +interface RegisteringProviderModule { + register(): void; } -interface AnthropicProviderModule { - streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOptions>; - streamSimpleAnthropic: StreamFunction<"anthropic-messages", SimpleStreamOptions>; +function createLazyLoadErrorImages(model: ImagesModel<"openrouter-images">, error: unknown): AssistantImages { + return { + api: model.api, + provider: model.provider, + model: model.id, + output: [], + stopReason: "error", + errorMessage: error instanceof Error ? error.message : String(error), + timestamp: Date.now(), + }; } -interface AzureOpenAIResponsesProviderModule { - streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses", AzureOpenAIResponsesOptions>; - streamSimpleAzureOpenAIResponses: StreamFunction<"azure-openai-responses", SimpleStreamOptions>; -} - -interface GoogleProviderModule { - streamGoogle: StreamFunction<"google-generative-ai", GoogleOptions>; - streamSimpleGoogle: StreamFunction<"google-generative-ai", SimpleStreamOptions>; -} - -interface GoogleVertexProviderModule { - streamGoogleVertex: StreamFunction<"google-vertex", GoogleVertexOptions>; - streamSimpleGoogleVertex: StreamFunction<"google-vertex", SimpleStreamOptions>; -} - -interface MistralProviderModule { - streamMistral: StreamFunction<"mistral-conversations", MistralOptions>; - streamSimpleMistral: StreamFunction<"mistral-conversations", SimpleStreamOptions>; -} - -interface OpenAICodexResponsesProviderModule { - streamOpenAICodexResponses: StreamFunction<"openai-codex-responses", OpenAICodexResponsesOptions>; - streamSimpleOpenAICodexResponses: StreamFunction<"openai-codex-responses", SimpleStreamOptions>; -} - -interface OpenAICompletionsProviderModule { - streamOpenAICompletions: StreamFunction<"openai-completions", OpenAICompletionsOptions>; - streamSimpleOpenAICompletions: StreamFunction<"openai-completions", SimpleStreamOptions>; -} - -interface OpenAIResponsesProviderModule { - streamOpenAIResponses: StreamFunction<"openai-responses", OpenAIResponsesOptions>; - streamSimpleOpenAIResponses: StreamFunction<"openai-responses", SimpleStreamOptions>; +function createLazyImagesApiProvider( + api: TApi, + loadModule: () => Promise, +): ImagesApiProvider { + return { + api, + generateImages: async (model: ImagesModel, context: ImagesContext, options?: TOptions) => { + try { + const module = await loadModule(); + module.register(); + const provider = getImagesApiProvider(api); + if (!provider) { + throw new Error(`No API provider registered for api: ${api}`); + } + return await provider.generateImages(model, context, options); + } catch (error) { + return createLazyLoadErrorImages(model as ImagesModel<"openrouter-images">, error); + } + }, + }; } interface BedrockProviderModule { - streamBedrock: ( - model: Model<"bedrock-converse-stream">, - context: Context, - options?: BedrockOptions, - ) => AsyncIterable; - streamSimpleBedrock: ( - model: Model<"bedrock-converse-stream">, - context: Context, - options?: SimpleStreamOptions, - ) => AsyncIterable; + streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOptions>; + streamSimpleBedrock: StreamFunction<"bedrock-converse-stream", SimpleStreamOptions>; } const importNodeOnlyProvider = (specifier: string): Promise => { @@ -91,42 +73,10 @@ const importNodeOnlyProvider = (specifier: string): Promise => { return import(runtimeSpecifier); }; -let anthropicProviderModulePromise: - | Promise> - | undefined; -let azureOpenAIResponsesProviderModulePromise: - | Promise> - | undefined; -let googleProviderModulePromise: - | Promise> - | undefined; -let googleVertexProviderModulePromise: - | Promise> - | undefined; -let mistralProviderModulePromise: - | Promise> - | undefined; -let openAICodexResponsesProviderModulePromise: - | Promise> - | undefined; -let openAICompletionsProviderModulePromise: - | Promise> - | undefined; -let openAIResponsesProviderModulePromise: - | Promise> - | undefined; -let bedrockProviderModuleOverride: - | LazyProviderModule<"bedrock-converse-stream", BedrockOptions, SimpleStreamOptions> - | undefined; -let bedrockProviderModulePromise: - | Promise> - | undefined; +let bedrockProviderModuleOverride: BedrockProviderModule | undefined; export function setBedrockProviderModule(module: BedrockProviderModule): void { - bedrockProviderModuleOverride = { - stream: module.streamBedrock, - streamSimple: module.streamSimpleBedrock, - }; + bedrockProviderModuleOverride = module; } function forwardStream(target: AssistantMessageEventStream, source: AsyncIterable): void { @@ -159,15 +109,29 @@ function createLazyLoadErrorMessage(model: Model, error: }; } -function createLazyStream( - loadModule: () => Promise>, +async function loadAndRegisterProvider( + api: TApi, + loadModule: () => Promise, +) { + const module = await loadModule(); + module.register(); + const provider = getApiProvider(api); + if (!provider) { + throw new Error(`No API provider registered for api: ${api}`); + } + return provider; +} + +function createLazyStream( + api: TApi, + loadModule: () => Promise, ): StreamFunction { return (model, context, options) => { const outer = new AssistantMessageEventStream(); - loadModule() - .then((module) => { - const inner = module.stream(model, context, options); + loadAndRegisterProvider(api, loadModule) + .then((provider) => { + const inner = provider.stream(model, context, options); forwardStream(outer, inner); }) .catch((error) => { @@ -180,17 +144,16 @@ function createLazyStream(loadModule: () => Promise>): StreamFunction { +function createLazySimpleStream( + api: TApi, + loadModule: () => Promise, +): StreamFunction { return (model, context, options) => { const outer = new AssistantMessageEventStream(); - loadModule() - .then((module) => { - const inner = module.streamSimple(model, context, options); + loadAndRegisterProvider(api, loadModule) + .then((provider) => { + const inner = provider.streamSimple(model, context, options); forwardStream(outer, inner); }) .catch((error) => { @@ -203,201 +166,114 @@ function createLazySimpleStream< }; } -function loadAnthropicProviderModule(): Promise< - LazyProviderModule<"anthropic-messages", AnthropicOptions, SimpleStreamOptions> -> { - anthropicProviderModulePromise ||= import("./anthropic.ts").then((module) => { - const provider = module as AnthropicProviderModule; - return { - stream: provider.streamAnthropic, - streamSimple: provider.streamSimpleAnthropic, - }; - }); - return anthropicProviderModulePromise; +function createLazyApiProvider( + api: TApi, + loadModule: () => Promise, +): ApiProvider { + return { + api, + stream: createLazyStream(api, loadModule), + streamSimple: createLazySimpleStream(api, loadModule), + }; } -function loadAzureOpenAIResponsesProviderModule(): Promise< - LazyProviderModule<"azure-openai-responses", AzureOpenAIResponsesOptions, SimpleStreamOptions> -> { - azureOpenAIResponsesProviderModulePromise ||= import("./azure-openai-responses.ts").then((module) => { - const provider = module as AzureOpenAIResponsesProviderModule; - return { - stream: provider.streamAzureOpenAIResponses, - streamSimple: provider.streamSimpleAzureOpenAIResponses, - }; - }); - return azureOpenAIResponsesProviderModulePromise; -} - -function loadGoogleProviderModule(): Promise< - LazyProviderModule<"google-generative-ai", GoogleOptions, SimpleStreamOptions> -> { - googleProviderModulePromise ||= import("./google.ts").then((module) => { - const provider = module as GoogleProviderModule; - return { - stream: provider.streamGoogle, - streamSimple: provider.streamSimpleGoogle, - }; - }); - return googleProviderModulePromise; -} - -function loadGoogleVertexProviderModule(): Promise< - LazyProviderModule<"google-vertex", GoogleVertexOptions, SimpleStreamOptions> -> { - googleVertexProviderModulePromise ||= import("./google-vertex.ts").then((module) => { - const provider = module as GoogleVertexProviderModule; - return { - stream: provider.streamGoogleVertex, - streamSimple: provider.streamSimpleGoogleVertex, - }; - }); - return googleVertexProviderModulePromise; -} - -function loadMistralProviderModule(): Promise< - LazyProviderModule<"mistral-conversations", MistralOptions, SimpleStreamOptions> -> { - mistralProviderModulePromise ||= import("./mistral.ts").then((module) => { - const provider = module as MistralProviderModule; - return { - stream: provider.streamMistral, - streamSimple: provider.streamSimpleMistral, - }; - }); - return mistralProviderModulePromise; -} - -function loadOpenAICodexResponsesProviderModule(): Promise< - LazyProviderModule<"openai-codex-responses", OpenAICodexResponsesOptions, SimpleStreamOptions> -> { - openAICodexResponsesProviderModulePromise ||= import("./openai-codex-responses.ts").then((module) => { - const provider = module as OpenAICodexResponsesProviderModule; - return { - stream: provider.streamOpenAICodexResponses, - streamSimple: provider.streamSimpleOpenAICodexResponses, - }; - }); - return openAICodexResponsesProviderModulePromise; -} - -function loadOpenAICompletionsProviderModule(): Promise< - LazyProviderModule<"openai-completions", OpenAICompletionsOptions, SimpleStreamOptions> -> { - openAICompletionsProviderModulePromise ||= import("./openai-completions.ts").then((module) => { - const provider = module as OpenAICompletionsProviderModule; - return { - stream: provider.streamOpenAICompletions, - streamSimple: provider.streamSimpleOpenAICompletions, - }; - }); - return openAICompletionsProviderModulePromise; -} - -function loadOpenAIResponsesProviderModule(): Promise< - LazyProviderModule<"openai-responses", OpenAIResponsesOptions, SimpleStreamOptions> -> { - openAIResponsesProviderModulePromise ||= import("./openai-responses.ts").then((module) => { - const provider = module as OpenAIResponsesProviderModule; - return { - stream: provider.streamOpenAIResponses, - streamSimple: provider.streamSimpleOpenAIResponses, - }; - }); - return openAIResponsesProviderModulePromise; -} - -function loadBedrockProviderModule(): Promise< - LazyProviderModule<"bedrock-converse-stream", BedrockOptions, SimpleStreamOptions> -> { - if (bedrockProviderModuleOverride) { - return Promise.resolve(bedrockProviderModuleOverride); - } - bedrockProviderModulePromise ||= importNodeOnlyProvider("./amazon-bedrock.ts").then((module) => { - const provider = module as BedrockProviderModule; - return { - stream: provider.streamBedrock, - streamSimple: provider.streamSimpleBedrock, - }; - }); - return bedrockProviderModulePromise; -} - -export const streamAnthropic = createLazyStream(loadAnthropicProviderModule); -export const streamSimpleAnthropic = createLazySimpleStream(loadAnthropicProviderModule); -export const streamAzureOpenAIResponses = createLazyStream(loadAzureOpenAIResponsesProviderModule); -export const streamSimpleAzureOpenAIResponses = createLazySimpleStream(loadAzureOpenAIResponsesProviderModule); -export const streamGoogle = createLazyStream(loadGoogleProviderModule); -export const streamSimpleGoogle = createLazySimpleStream(loadGoogleProviderModule); -export const streamGoogleVertex = createLazyStream(loadGoogleVertexProviderModule); -export const streamSimpleGoogleVertex = createLazySimpleStream(loadGoogleVertexProviderModule); -export const streamMistral = createLazyStream(loadMistralProviderModule); -export const streamSimpleMistral = createLazySimpleStream(loadMistralProviderModule); -export const streamOpenAICodexResponses = createLazyStream(loadOpenAICodexResponsesProviderModule); -export const streamSimpleOpenAICodexResponses = createLazySimpleStream(loadOpenAICodexResponsesProviderModule); -export const streamOpenAICompletions = createLazyStream(loadOpenAICompletionsProviderModule); -export const streamSimpleOpenAICompletions = createLazySimpleStream(loadOpenAICompletionsProviderModule); -export const streamOpenAIResponses = createLazyStream(loadOpenAIResponsesProviderModule); -export const streamSimpleOpenAIResponses = createLazySimpleStream(loadOpenAIResponsesProviderModule); -const streamBedrockLazy = createLazyStream(loadBedrockProviderModule); -const streamSimpleBedrockLazy = createLazySimpleStream(loadBedrockProviderModule); - -export function registerBuiltInApiProviders(): void { - registerApiProvider({ - api: "anthropic-messages", - stream: streamAnthropic, - streamSimple: streamSimpleAnthropic, - }); - - registerApiProvider({ - api: "openai-completions", - stream: streamOpenAICompletions, - streamSimple: streamSimpleOpenAICompletions, - }); - - registerApiProvider({ - api: "mistral-conversations", - stream: streamMistral, - streamSimple: streamSimpleMistral, - }); - - registerApiProvider({ - api: "openai-responses", - stream: streamOpenAIResponses, - streamSimple: streamSimpleOpenAIResponses, - }); - - registerApiProvider({ - api: "azure-openai-responses", - stream: streamAzureOpenAIResponses, - streamSimple: streamSimpleAzureOpenAIResponses, - }); - - registerApiProvider({ - api: "openai-codex-responses", - stream: streamOpenAICodexResponses, - streamSimple: streamSimpleOpenAICodexResponses, - }); - - registerApiProvider({ - api: "google-generative-ai", - stream: streamGoogle, - streamSimple: streamSimpleGoogle, - }); - - registerApiProvider({ - api: "google-vertex", - stream: streamGoogleVertex, - streamSimple: streamSimpleGoogleVertex, - }); - +function registerBedrockProviderModule(module: BedrockProviderModule): void { registerApiProvider({ api: "bedrock-converse-stream", - stream: streamBedrockLazy, - streamSimple: streamSimpleBedrockLazy, + stream: module.streamBedrock, + streamSimple: module.streamSimpleBedrock, }); } +function loadBedrockProviderModule(): Promise { + const module = bedrockProviderModuleOverride; + if (module) { + return Promise.resolve({ register: () => registerBedrockProviderModule(module) }); + } + return importNodeOnlyProvider("./amazon-bedrock.ts").then((provider) => provider as RegisteringProviderModule); +} + +const anthropicProvider = createLazyApiProvider<"anthropic-messages", AnthropicOptions>( + "anthropic-messages", + () => import("./anthropic.ts"), +); +const azureOpenAIResponsesProvider = createLazyApiProvider<"azure-openai-responses", AzureOpenAIResponsesOptions>( + "azure-openai-responses", + () => import("./azure-openai-responses.ts"), +); +const googleProvider = createLazyApiProvider<"google-generative-ai", GoogleOptions>( + "google-generative-ai", + () => import("./google.ts"), +); +const googleVertexProvider = createLazyApiProvider<"google-vertex", GoogleVertexOptions>( + "google-vertex", + () => import("./google-vertex.ts"), +); +const mistralProvider = createLazyApiProvider<"mistral-conversations", MistralOptions>( + "mistral-conversations", + () => import("./mistral.ts"), +); +const openAICodexResponsesProvider = createLazyApiProvider<"openai-codex-responses", OpenAICodexResponsesOptions>( + "openai-codex-responses", + () => import("./openai-codex-responses.ts"), +); +const openAICompletionsProvider = createLazyApiProvider<"openai-completions", OpenAICompletionsOptions>( + "openai-completions", + () => import("./openai-completions.ts"), +); +const openAIResponsesProvider = createLazyApiProvider<"openai-responses", OpenAIResponsesOptions>( + "openai-responses", + () => import("./openai-responses.ts"), +); +const bedrockProvider = createLazyApiProvider<"bedrock-converse-stream", BedrockOptions>( + "bedrock-converse-stream", + loadBedrockProviderModule, +); +const openRouterImagesProvider = createLazyImagesApiProvider( + "openrouter-images", + () => import("./images/openrouter.ts"), +); + +export const generateImagesOpenRouter = openRouterImagesProvider.generateImages; +export const streamAnthropic = anthropicProvider.stream; +export const streamSimpleAnthropic = anthropicProvider.streamSimple; +export const streamAzureOpenAIResponses = azureOpenAIResponsesProvider.stream; +export const streamSimpleAzureOpenAIResponses = azureOpenAIResponsesProvider.streamSimple; +export const streamGoogle = googleProvider.stream; +export const streamSimpleGoogle = googleProvider.streamSimple; +export const streamGoogleVertex = googleVertexProvider.stream; +export const streamSimpleGoogleVertex = googleVertexProvider.streamSimple; +export const streamMistral = mistralProvider.stream; +export const streamSimpleMistral = mistralProvider.streamSimple; +export const streamOpenAICodexResponses = openAICodexResponsesProvider.stream; +export const streamSimpleOpenAICodexResponses = openAICodexResponsesProvider.streamSimple; +export const streamOpenAICompletions = openAICompletionsProvider.stream; +export const streamSimpleOpenAICompletions = openAICompletionsProvider.streamSimple; +export const streamOpenAIResponses = openAIResponsesProvider.stream; +export const streamSimpleOpenAIResponses = openAIResponsesProvider.streamSimple; + +const registerBuiltInApiProviderFunctions = [ + () => registerApiProvider(anthropicProvider), + () => registerApiProvider(openAICompletionsProvider), + () => registerApiProvider(mistralProvider), + () => registerApiProvider(openAIResponsesProvider), + () => registerApiProvider(azureOpenAIResponsesProvider), + () => registerApiProvider(openAICodexResponsesProvider), + () => registerApiProvider(googleProvider), + () => registerApiProvider(googleVertexProvider), + () => registerApiProvider(bedrockProvider), +]; + +export function registerBuiltInImagesApiProviders(): void { + registerImagesApiProvider(openRouterImagesProvider); +} + +export function registerBuiltInApiProviders(): void { + for (const register of registerBuiltInApiProviderFunctions) { + register(); + } +} + export function resetApiProviders(): void { clearApiProviders(); registerBuiltInApiProviders(); diff --git a/packages/ai/src/stream.ts b/packages/ai/src/stream.ts index 3f333d9e..7b6b28c6 100644 --- a/packages/ai/src/stream.ts +++ b/packages/ai/src/stream.ts @@ -1,5 +1,3 @@ -import "./providers/register-builtins.ts"; - import { getApiProvider } from "./api-registry.ts"; import { getEnvApiKey } from "./env-api-keys.ts"; import type { @@ -13,8 +11,6 @@ import type { StreamOptions, } from "./types.ts"; -export { getEnvApiKey } from "./env-api-keys.ts"; - function hasExplicitApiKey(apiKey: string | undefined): apiKey is string { return typeof apiKey === "string" && apiKey.trim().length > 0; } diff --git a/packages/ai/test/abort.test.ts b/packages/ai/test/abort.test.ts index 27c274aa..303044ad 100644 --- a/packages/ai/test/abort.test.ts +++ b/packages/ai/test/abort.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { complete, stream } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { complete, stream } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts b/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts index 2483e1a4..a9862485 100644 --- a/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts +++ b/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts @@ -1,8 +1,8 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { getEnvApiKey } from "../src/env-api-keys.ts"; +import { complete } from "../src/index.ts"; import { getModels, getProviders } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Api, KnownProvider, Model, ProviderStreamOptions, Tool } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts b/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts index 69e58e27..5663e2a8 100644 --- a/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts +++ b/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { streamSimple } from "../src/stream.ts"; +import { streamSimple } from "../src/index.ts"; import type { AssistantMessage, Context, Model } from "../src/types.ts"; interface AnthropicPayload { diff --git a/packages/ai/test/anthropic-force-adaptive-thinking.test.ts b/packages/ai/test/anthropic-force-adaptive-thinking.test.ts index e797c3a9..cef2ca48 100644 --- a/packages/ai/test/anthropic-force-adaptive-thinking.test.ts +++ b/packages/ai/test/anthropic-force-adaptive-thinking.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface AnthropicThinkingPayload { diff --git a/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts b/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts index 2b7667d7..0c9d590f 100644 --- a/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts +++ b/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { getEnvApiKey } from "../src/env-api-keys.ts"; +import { complete } from "../src/index.ts"; import { getModels, getProviders } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Api, KnownProvider, Model, ProviderStreamOptions } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/anthropic-opus-4-8-smoke.test.ts b/packages/ai/test/anthropic-opus-4-8-smoke.test.ts index bb4b739b..0c1288b3 100644 --- a/packages/ai/test/anthropic-opus-4-8-smoke.test.ts +++ b/packages/ai/test/anthropic-opus-4-8-smoke.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; import type { Context } from "../src/types.ts"; interface AnthropicThinkingPayload { diff --git a/packages/ai/test/anthropic-temperature-compat.test.ts b/packages/ai/test/anthropic-temperature-compat.test.ts index 00161ab8..06a05698 100644 --- a/packages/ai/test/anthropic-temperature-compat.test.ts +++ b/packages/ai/test/anthropic-temperature-compat.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface AnthropicTemperaturePayload { diff --git a/packages/ai/test/anthropic-thinking-disable.test.ts b/packages/ai/test/anthropic-thinking-disable.test.ts index 13d333e5..581d3a34 100644 --- a/packages/ai/test/anthropic-thinking-disable.test.ts +++ b/packages/ai/test/anthropic-thinking-disable.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface AnthropicThinkingPayload { diff --git a/packages/ai/test/anthropic-tool-name-normalization.test.ts b/packages/ai/test/anthropic-tool-name-normalization.test.ts index bb454081..d52ff50e 100644 --- a/packages/ai/test/anthropic-tool-name-normalization.test.ts +++ b/packages/ai/test/anthropic-tool-name-normalization.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; +import { stream } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { stream } from "../src/stream.ts"; import type { Context, Tool } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/base-entrypoint.test.ts b/packages/ai/test/base-entrypoint.test.ts new file mode 100644 index 00000000..d54fa492 --- /dev/null +++ b/packages/ai/test/base-entrypoint.test.ts @@ -0,0 +1,68 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { clearApiProviders, complete, getApiProvider, getApiProviders } from "../src/base.ts"; +import { register as registerAmazonBedrock } from "../src/providers/amazon-bedrock.ts"; +import { register as registerAnthropic } from "../src/providers/anthropic.ts"; +import { register as registerAzureOpenAIResponses } from "../src/providers/azure-openai-responses.ts"; +import { fauxAssistantMessage, registerFauxProvider } from "../src/providers/faux.ts"; +import { register as registerGoogle } from "../src/providers/google.ts"; +import { register as registerGoogleVertex } from "../src/providers/google-vertex.ts"; +import { register as registerMistral } from "../src/providers/mistral.ts"; +import { register as registerOpenAICodexResponses } from "../src/providers/openai-codex-responses.ts"; +import { register as registerOpenAICompletions } from "../src/providers/openai-completions.ts"; +import { register as registerOpenAIResponses } from "../src/providers/openai-responses.ts"; + +const registrations = [ + ["bedrock-converse-stream", registerAmazonBedrock], + ["anthropic-messages", registerAnthropic], + ["azure-openai-responses", registerAzureOpenAIResponses], + ["google-generative-ai", registerGoogle], + ["google-vertex", registerGoogleVertex], + ["mistral-conversations", registerMistral], + ["openai-codex-responses", registerOpenAICodexResponses], + ["openai-completions", registerOpenAICompletions], + ["openai-responses", registerOpenAIResponses], +] as const; + +afterEach(() => { + clearApiProviders(); +}); + +describe("base entrypoint", () => { + it("starts without built-in provider registrations", () => { + expect(getApiProviders()).toEqual([]); + }); + + it.each(registrations)("registers the %s transport explicitly", (api, register) => { + register(); + expect(getApiProvider(api)?.api).toBe(api); + }); + + it("dispatches custom providers", async () => { + const registration = registerFauxProvider(); + registration.setResponses([fauxAssistantMessage("hello")]); + const response = await complete(registration.getModel(), { + messages: [{ role: "user", content: "hi", timestamp: Date.now() }], + }); + expect(response.content).toEqual([{ type: "text", text: "hello" }]); + }); + + it("fails clearly when no transport is registered", async () => { + await expect( + complete( + { + id: "missing-model", + name: "Missing Model", + api: "missing-api", + provider: "missing-provider", + baseUrl: "https://example.com", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1, + maxTokens: 1, + }, + { messages: [] }, + ), + ).rejects.toThrow("No API provider registered for api: missing-api"); + }); +}); diff --git a/packages/ai/test/bedrock-models.test.ts b/packages/ai/test/bedrock-models.test.ts index 2cfd8fb9..5bd5a1f3 100644 --- a/packages/ai/test/bedrock-models.test.ts +++ b/packages/ai/test/bedrock-models.test.ts @@ -17,8 +17,8 @@ */ import { describe, expect, it } from "vitest"; +import { complete } from "../src/index.ts"; import { getModels } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Context } from "../src/types.ts"; import { hasBedrockCredentials } from "./bedrock-utils.ts"; diff --git a/packages/ai/test/cache-retention.test.ts b/packages/ai/test/cache-retention.test.ts index 6e2c1a5b..b5c55198 100644 --- a/packages/ai/test/cache-retention.test.ts +++ b/packages/ai/test/cache-retention.test.ts @@ -1,10 +1,10 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { stream } from "../src/index.ts"; import { MODELS } from "../src/models.generated.ts"; import { getModel } from "../src/models.ts"; import { streamAnthropic } from "../src/providers/anthropic.ts"; import { streamOpenAICompletions } from "../src/providers/openai-completions.ts"; import { streamOpenAIResponses } from "../src/providers/openai-responses.ts"; -import { stream } from "../src/stream.ts"; import type { Context, Model } from "../src/types.ts"; class PayloadCaptured extends Error { diff --git a/packages/ai/test/context-overflow.test.ts b/packages/ai/test/context-overflow.test.ts index 9f021a41..54087a29 100644 --- a/packages/ai/test/context-overflow.test.ts +++ b/packages/ai/test/context-overflow.test.ts @@ -14,8 +14,8 @@ import type { ChildProcess } from "child_process"; import { execSync, spawn } from "child_process"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { complete } from "../src/index.ts"; import { getModel, getModels } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { AssistantMessage, Context, Model, Usage } from "../src/types.ts"; import { isContextOverflow } from "../src/utils/overflow.ts"; import { hasAzureOpenAICredentials } from "./azure-utils.ts"; diff --git a/packages/ai/test/cross-provider-handoff.test.ts b/packages/ai/test/cross-provider-handoff.test.ts index 23593394..73fac229 100644 --- a/packages/ai/test/cross-provider-handoff.test.ts +++ b/packages/ai/test/cross-provider-handoff.test.ts @@ -25,8 +25,8 @@ import { writeFileSync } from "fs"; import { Type } from "typebox"; import { beforeAll, describe, expect, it } from "vitest"; +import { completeSimple, getEnvApiKey } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { completeSimple, getEnvApiKey } from "../src/stream.ts"; import type { Api, AssistantMessage, Message, Model, Tool, ToolResultMessage } from "../src/types.ts"; import { hasAzureOpenAICredentials } from "./azure-utils.ts"; import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.ts"; diff --git a/packages/ai/test/empty.test.ts b/packages/ai/test/empty.test.ts index a8453dcc..fadd67a9 100644 --- a/packages/ai/test/empty.test.ts +++ b/packages/ai/test/empty.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Api, AssistantMessage, Context, Model, StreamOptions, UserMessage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/google-thinking-disable.test.ts b/packages/ai/test/google-thinking-disable.test.ts index 30df3638..3920f4c8 100644 --- a/packages/ai/test/google-thinking-disable.test.ts +++ b/packages/ai/test/google-thinking-disable.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; import type { Api, Context, Model, SimpleStreamOptions } from "../src/types.ts"; type SimpleOptionsWithExtras = SimpleStreamOptions & Record; diff --git a/packages/ai/test/interleaved-thinking.test.ts b/packages/ai/test/interleaved-thinking.test.ts index 4cf387ce..a2d3f5a6 100644 --- a/packages/ai/test/interleaved-thinking.test.ts +++ b/packages/ai/test/interleaved-thinking.test.ts @@ -1,8 +1,8 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { getEnvApiKey } from "../src/env-api-keys.ts"; +import { completeSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { completeSimple } from "../src/stream.ts"; import type { Api, Context, Model, StopReason, Tool, ToolCall, ToolResultMessage } from "../src/types.ts"; import { StringEnum } from "../src/utils/typebox-helpers.ts"; import { hasBedrockCredentials } from "./bedrock-utils.ts"; diff --git a/packages/ai/test/lazy-module-load.test.ts b/packages/ai/test/lazy-module-load.test.ts index e21f0d12..14bccff0 100644 --- a/packages/ai/test/lazy-module-load.test.ts +++ b/packages/ai/test/lazy-module-load.test.ts @@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest"; const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const aiEntryUrl = new URL("../src/index.ts", import.meta.url).href; +const baseEntryUrl = new URL("../src/base.ts", import.meta.url).href; const SDK_SPECIFIERS = [ "@anthropic-ai/sdk", @@ -16,9 +17,10 @@ const SDK_SPECIFIERS = [ type ProbeResult = { loadedSpecifiers: string[]; + value?: unknown; }; -function runProbe(action: string): ProbeResult { +function runProbe(action: string, entryUrl = aiEntryUrl): ProbeResult { const script = ` import { registerHooks } from "node:module"; @@ -34,9 +36,11 @@ function runProbe(action: string): ProbeResult { }, }); - const mod = await import(${JSON.stringify(aiEntryUrl)}); - ${action} - console.log(JSON.stringify({ loadedSpecifiers: [...new Set(loaded)] })); + const mod = await import(${JSON.stringify(entryUrl)}); + const value = await (async () => { + ${action} + })(); + console.log(JSON.stringify({ loadedSpecifiers: [...new Set(loaded)], value })); `; const result = spawnSync(process.execPath, ["--input-type=module", "--eval", script], { @@ -66,6 +70,33 @@ describe("lazy provider module loading", () => { expect(result.loadedSpecifiers).toEqual([]); }); + it("registers built-in transports when importing the root barrel", () => { + const result = runProbe(`return mod.getApiProviders().map((provider) => provider.api).sort();`); + expect(result.value).toEqual([ + "anthropic-messages", + "azure-openai-responses", + "bedrock-converse-stream", + "google-generative-ai", + "google-vertex", + "mistral-conversations", + "openai-codex-responses", + "openai-completions", + "openai-responses", + ]); + }); + + it("registers built-in image transports when importing the root barrel", () => { + const result = runProbe(`return mod.getImagesApiProvider("openrouter-images")?.api;`); + expect(result.loadedSpecifiers).toEqual([]); + expect(result.value).toBe("openrouter-images"); + }); + + it("does not load provider SDKs or register transports when importing the base barrel", () => { + const result = runProbe(`return mod.getApiProviders().map((provider) => provider.api);`, baseEntryUrl); + expect(result.loadedSpecifiers).toEqual([]); + expect(result.value).toEqual([]); + }); + it("loads only the Anthropic SDK when calling the root lazy wrapper", () => { const result = runProbe(` const model = { @@ -96,4 +127,17 @@ describe("lazy provider module loading", () => { expect(result.loadedSpecifiers).toEqual(["@anthropic-ai/sdk"]); }); + + it("dispatches through a lazy wrapper again after resetting providers", () => { + const result = runProbe(` + const model = mod.getModel("anthropic", "claude-sonnet-4-6"); + const context = { messages: [{ role: "user", content: "hi" }] }; + await mod.streamSimple(model, context).result(); + mod.resetApiProviders(); + return (await mod.streamSimple(model, context).result()).stopReason; + `); + + expect(result.loadedSpecifiers).toEqual(["@anthropic-ai/sdk"]); + expect(result.value).toBe("error"); + }); }); diff --git a/packages/ai/test/mistral-reasoning-mode.test.ts b/packages/ai/test/mistral-reasoning-mode.test.ts index d197a56a..0e2f45c5 100644 --- a/packages/ai/test/mistral-reasoning-mode.test.ts +++ b/packages/ai/test/mistral-reasoning-mode.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface MistralPayload { diff --git a/packages/ai/test/mistral-tool-schema.test.ts b/packages/ai/test/mistral-tool-schema.test.ts index c6898fc6..3c490c6d 100644 --- a/packages/ai/test/mistral-tool-schema.test.ts +++ b/packages/ai/test/mistral-tool-schema.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; +import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Context, Model } from "../src/types.ts"; interface MistralToolPayload { diff --git a/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts b/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts index 0abb46f6..62245dcf 100644 --- a/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts +++ b/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Context } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/openai-completions-empty-tools.test.ts b/packages/ai/test/openai-completions-empty-tools.test.ts index be233670..0ecf8e13 100644 --- a/packages/ai/test/openai-completions-empty-tools.test.ts +++ b/packages/ai/test/openai-completions-empty-tools.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; // Empty tools arrays must NOT be serialized as `tools: []` — some OpenAI-compatible // backends (e.g. DashScope / Aliyun Qwen via compatible-mode) reject the request with diff --git a/packages/ai/test/openai-completions-response-model.test.ts b/packages/ai/test/openai-completions-response-model.test.ts index d8e4d2f9..ec2abea1 100644 --- a/packages/ai/test/openai-completions-response-model.test.ts +++ b/packages/ai/test/openai-completions-response-model.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { complete } from "../src/stream.ts"; +import { complete } from "../src/index.ts"; import type { Model } from "../src/types.ts"; // Router/virtual ids (e.g. OpenRouter `auto`) keep `model` pinned to the diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index 8ca86025..373b1b78 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -1,8 +1,8 @@ import { Type } from "typebox"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { stream, streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; import { convertMessages } from "../src/providers/openai-completions.ts"; -import { stream, streamSimple } from "../src/stream.ts"; import type { AssistantMessage, Model, Tool, ToolResultMessage } from "../src/types.ts"; const mockState = vi.hoisted(() => ({ diff --git a/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts b/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts index d173694f..0d247b0b 100644 --- a/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts +++ b/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Context } from "../src/types.ts"; describe.skipIf(!process.env.OPENAI_API_KEY)("openai responses cache affinity e2e", () => { diff --git a/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts b/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts index aa6f2e23..87945bc2 100644 --- a/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts +++ b/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; +import { complete, getEnvApiKey } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { complete, getEnvApiKey } from "../src/stream.ts"; import type { AssistantMessage, Context, Message, Tool, ToolCall } from "../src/types.ts"; const testToolSchema = Type.Object({ diff --git a/packages/ai/test/openrouter-cache-write-repro.test.ts b/packages/ai/test/openrouter-cache-write-repro.test.ts index 4bdeb286..2cf2a791 100644 --- a/packages/ai/test/openrouter-cache-write-repro.test.ts +++ b/packages/ai/test/openrouter-cache-write-repro.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { completeSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { completeSimple } from "../src/stream.ts"; function createLongSystemPrompt(): string { const nonce = `${Date.now()}-${Math.random()}`; diff --git a/packages/ai/test/openrouter-images.test.ts b/packages/ai/test/openrouter-images.test.ts index 31882bec..5811e6f4 100644 --- a/packages/ai/test/openrouter-images.test.ts +++ b/packages/ai/test/openrouter-images.test.ts @@ -1,5 +1,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { generateImages } from "../src/images.ts"; +import { clearImagesApiProviders, getImagesApiProvider } from "../src/images-api-registry.ts"; +import { register as registerOpenRouterImages } from "../src/providers/images/openrouter.ts"; +import { registerBuiltInImagesApiProviders } from "../src/providers/register-builtins.ts"; import type { ImagesContext, ImagesModel } from "../src/types.ts"; const mockState = vi.hoisted(() => ({ @@ -62,6 +65,15 @@ describe("openrouter images", () => { beforeEach(() => { mockState.lastParams = undefined; mockState.lastRequestOptions = undefined; + clearImagesApiProviders(); + registerBuiltInImagesApiProviders(); + }); + + it("registers the direct OpenRouter images transport explicitly", () => { + clearImagesApiProviders(); + expect(getImagesApiProvider("openrouter-images")).toBeUndefined(); + registerOpenRouterImages(); + expect(getImagesApiProvider("openrouter-images")?.api).toBe("openrouter-images"); }); it("returns text plus images in final output", async () => { diff --git a/packages/ai/test/responseid.test.ts b/packages/ai/test/responseid.test.ts index d250f5f4..bc5d6c54 100644 --- a/packages/ai/test/responseid.test.ts +++ b/packages/ai/test/responseid.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions } from "../src/types.ts"; import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index 9a70b6c2..3a381966 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -4,8 +4,8 @@ import { dirname, join } from "path"; import { Type } from "typebox"; import { fileURLToPath } from "url"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { complete, stream } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { complete, stream } from "../src/stream.ts"; import type { Api, Context, ImageContent, Model, StreamOptions, Tool, ToolResultMessage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/tokens.test.ts b/packages/ai/test/tokens.test.ts index 676f5a29..fdf45b94 100644 --- a/packages/ai/test/tokens.test.ts +++ b/packages/ai/test/tokens.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { stream } from "../src/index.ts"; import { getModel, getModels } from "../src/models.ts"; -import { stream } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/tool-call-id-normalization.test.ts b/packages/ai/test/tool-call-id-normalization.test.ts index 0672181b..622e5127 100644 --- a/packages/ai/test/tool-call-id-normalization.test.ts +++ b/packages/ai/test/tool-call-id-normalization.test.ts @@ -12,8 +12,8 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; +import { completeSimple, getEnvApiKey } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { completeSimple, getEnvApiKey } from "../src/stream.ts"; import type { AssistantMessage, Message, Tool, ToolResultMessage } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/tool-call-without-result.test.ts b/packages/ai/test/tool-call-without-result.test.ts index 11b832f3..b21c1ce8 100644 --- a/packages/ai/test/tool-call-without-result.test.ts +++ b/packages/ai/test/tool-call-without-result.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; +import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions, Tool } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/total-tokens.test.ts b/packages/ai/test/total-tokens.test.ts index 972913a7..4df4b2e1 100644 --- a/packages/ai/test/total-tokens.test.ts +++ b/packages/ai/test/total-tokens.test.ts @@ -13,8 +13,8 @@ */ import { describe, expect, it } from "vitest"; +import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions, Usage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/unicode-surrogate.test.ts b/packages/ai/test/unicode-surrogate.test.ts index 9cdddefa..d8b02392 100644 --- a/packages/ai/test/unicode-surrogate.test.ts +++ b/packages/ai/test/unicode-surrogate.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; +import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions, ToolResultMessage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/xhigh.test.ts b/packages/ai/test/xhigh.test.ts index 3c279863..1bda5c14 100644 --- a/packages/ai/test/xhigh.test.ts +++ b/packages/ai/test/xhigh.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { stream } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { stream } from "../src/stream.ts"; import type { Context, Model } from "../src/types.ts"; function makeContext(): Context { diff --git a/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts b/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts index ab2fec30..9a371b60 100644 --- a/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts +++ b/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { completeSimple, getEnvApiKey, streamSimple } from "../src/stream.ts"; +import { completeSimple, getEnvApiKey, streamSimple } from "../src/index.ts"; import type { AssistantMessage, Context, Model } from "../src/types.ts"; const provider = "xiaomi-token-plan-ams"; diff --git a/packages/ai/test/zen.test.ts b/packages/ai/test/zen.test.ts index 8ca80014..82a2079f 100644 --- a/packages/ai/test/zen.test.ts +++ b/packages/ai/test/zen.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { complete } from "../src/index.ts"; import { MODELS } from "../src/models.generated.ts"; -import { complete } from "../src/stream.ts"; import type { Model } from "../src/types.ts"; describe.skipIf(!process.env.OPENCODE_API_KEY)("OpenCode Models Smoke Test", () => { diff --git a/packages/coding-agent/vitest.config.ts b/packages/coding-agent/vitest.config.ts index 67ce0fca..5759e3c4 100644 --- a/packages/coding-agent/vitest.config.ts +++ b/packages/coding-agent/vitest.config.ts @@ -2,8 +2,11 @@ import { fileURLToPath } from "node:url"; import { defineConfig } from "vitest/config"; const aiSrcIndex = fileURLToPath(new URL("../ai/src/index.ts", import.meta.url)); +const aiSrcBase = fileURLToPath(new URL("../ai/src/base.ts", import.meta.url)); const aiSrcOAuth = fileURLToPath(new URL("../ai/src/oauth.ts", import.meta.url)); +const aiOpenRouterImages = fileURLToPath(new URL("../ai/src/providers/images/openrouter.ts", import.meta.url)); const agentSrcIndex = fileURLToPath(new URL("../agent/src/index.ts", import.meta.url)); +const agentSrcBase = fileURLToPath(new URL("../agent/src/base.ts", import.meta.url)); const tuiSrcIndex = fileURLToPath(new URL("../tui/src/index.ts", import.meta.url)); export default defineConfig({ @@ -20,12 +23,18 @@ export default defineConfig({ resolve: { alias: [ { find: /^@earendil-works\/pi-ai$/, replacement: aiSrcIndex }, + { find: /^@earendil-works\/pi-ai\/base$/, replacement: aiSrcBase }, { find: /^@earendil-works\/pi-ai\/oauth$/, replacement: aiSrcOAuth }, + { find: /^@earendil-works\/pi-ai\/openrouter-images$/, replacement: aiOpenRouterImages }, { find: /^@earendil-works\/pi-agent-core$/, replacement: agentSrcIndex }, + { find: /^@earendil-works\/pi-agent-core\/base$/, replacement: agentSrcBase }, { find: /^@earendil-works\/pi-tui$/, replacement: tuiSrcIndex }, { find: /^@mariozechner\/pi-ai$/, replacement: aiSrcIndex }, + { find: /^@mariozechner\/pi-ai\/base$/, replacement: aiSrcBase }, { find: /^@mariozechner\/pi-ai\/oauth$/, replacement: aiSrcOAuth }, + { find: /^@mariozechner\/pi-ai\/openrouter-images$/, replacement: aiOpenRouterImages }, { find: /^@mariozechner\/pi-agent-core$/, replacement: agentSrcIndex }, + { find: /^@mariozechner\/pi-agent-core\/base$/, replacement: agentSrcBase }, { find: /^@mariozechner\/pi-tui$/, replacement: tuiSrcIndex }, ], }, diff --git a/scripts/check-browser-smoke.mjs b/scripts/check-browser-smoke.mjs index d590988d..906eccd3 100644 --- a/scripts/check-browser-smoke.mjs +++ b/scripts/check-browser-smoke.mjs @@ -4,7 +4,21 @@ import { join } from "node:path"; import { build } from "esbuild"; const outputPath = join(tmpdir(), "pi-browser-smoke.js"); +const baseOutputPath = join(tmpdir(), "pi-browser-base-smoke.js"); +const selectiveOutputPath = join(tmpdir(), "pi-browser-selective-smoke.js"); const errorLogPath = join(tmpdir(), "pi-browser-smoke-errors.log"); +const providerImplementationInputs = [ + "packages/ai/src/providers/amazon-bedrock.ts", + "packages/ai/src/providers/anthropic.ts", + "packages/ai/src/providers/azure-openai-responses.ts", + "packages/ai/src/providers/google.ts", + "packages/ai/src/providers/google-vertex.ts", + "packages/ai/src/providers/images/openrouter.ts", + "packages/ai/src/providers/mistral.ts", + "packages/ai/src/providers/openai-codex-responses.ts", + "packages/ai/src/providers/openai-completions.ts", + "packages/ai/src/providers/openai-responses.ts", +]; try { await build({ @@ -15,6 +29,36 @@ try { logLevel: "silent", outfile: outputPath, }); + const baseBuild = await build({ + stdin: { + contents: `import { complete } from "@earendil-works/pi-ai/base";\nimport { Agent } from "@earendil-works/pi-agent-core/base";\nconsole.log(typeof complete, typeof Agent);\n`, + resolveDir: process.cwd(), + sourcefile: "pi-browser-base-smoke-entry.ts", + }, + bundle: true, + platform: "browser", + format: "esm", + logLevel: "silent", + metafile: true, + outfile: baseOutputPath, + }); + const bundledInputs = new Set(Object.keys(baseBuild.metafile.inputs)); + const reachableProviderImplementations = providerImplementationInputs.filter((input) => bundledInputs.has(input)); + if (reachableProviderImplementations.length > 0) { + throw new Error(`Base browser bundle reached provider implementations:\n${reachableProviderImplementations.join("\n")}`); + } + await build({ + stdin: { + contents: `import { register as registerAnthropic } from "@earendil-works/pi-ai/anthropic";\nimport { register as registerOpenAICompletions } from "@earendil-works/pi-ai/openai-completions";\nimport { register as registerOpenRouterImages } from "@earendil-works/pi-ai/openrouter-images";\nconsole.log(typeof registerAnthropic, typeof registerOpenAICompletions, typeof registerOpenRouterImages);\n`, + resolveDir: process.cwd(), + sourcefile: "pi-browser-selective-smoke-entry.ts", + }, + bundle: true, + platform: "browser", + format: "esm", + logLevel: "silent", + outfile: selectiveOutputPath, + }); process.exit(0); } catch (error) { let detailedErrors = ""; diff --git a/tsconfig.json b/tsconfig.json index 903f80f3..bcb12eee 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "*": ["./*"], "@earendil-works/pi-ai": ["./packages/ai/src/index.ts"], "@earendil-works/pi-ai/oauth": ["./packages/ai/src/oauth.ts"], + "@earendil-works/pi-ai/openrouter-images": ["./packages/ai/src/providers/images/openrouter.ts"], "@earendil-works/pi-ai/*": ["./packages/ai/src/*.ts", "./packages/ai/src/providers/*.ts"], "@earendil-works/pi-ai/dist/*": ["./packages/ai/src/*"], "@earendil-works/pi-agent-core": ["./packages/agent/src/index.ts"], From ea65a51a15c6c5965d1ffa87fee357758fba5e7f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 19 Jun 2026 00:42:53 +0200 Subject: [PATCH 038/122] fix: update vulnerable dependencies --- package-lock.json | 3307 ++++++++---------- package.json | 2 +- packages/agent/package.json | 4 +- packages/ai/package.json | 2 +- packages/coding-agent/npm-shrinkwrap.json | 37 +- packages/coding-agent/package.json | 4 +- scripts/generate-coding-agent-shrinkwrap.mjs | 2 +- 7 files changed, 1547 insertions(+), 1811 deletions(-) diff --git a/package-lock.json b/package-lock.json index d1abbf97..ce9dd552 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@biomejs/biome": "2.3.5", "@types/node": "22.19.19", "@typescript/native-preview": "7.0.0-dev.20260120.1", - "esbuild": "0.28.0", + "esbuild": "0.28.1", "husky": "9.1.7", "jiti": "2.7.0", "shx": "0.4.0", @@ -31,20 +31,6 @@ "node": ">=22.19.0" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@anthropic-ai/sandbox-runtime": { "version": "0.0.26", "resolved": "https://registry.npmjs.org/@anthropic-ai/sandbox-runtime/-/sandbox-runtime-0.0.26.tgz", @@ -811,10 +797,44 @@ "resolved": "packages/tui", "link": true }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -829,9 +849,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -846,9 +866,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -863,9 +883,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -880,9 +900,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -897,9 +917,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -914,9 +934,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -931,9 +951,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -948,9 +968,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -965,9 +985,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -982,9 +1002,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -999,9 +1019,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -1016,9 +1036,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -1033,9 +1053,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -1050,9 +1070,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -1067,9 +1087,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -1084,9 +1104,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -1101,9 +1121,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -1118,9 +1138,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -1135,9 +1155,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -1152,9 +1172,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -1169,9 +1189,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -1186,9 +1206,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -1203,9 +1223,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -1220,9 +1240,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -1237,9 +1257,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -1277,130 +1297,6 @@ } } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", - "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1643,6 +1539,25 @@ } } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@nodable/entities": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", @@ -1711,15 +1626,14 @@ "node": ">=14" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", "dev": true, "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, "node_modules/@pondwader/socks5-server": { @@ -1747,9 +1661,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { @@ -1767,12 +1681,6 @@ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", "license": "BSD-3-Clause" }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", - "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", - "license": "BSD-3-Clause" - }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", @@ -1791,24 +1699,10 @@ "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", "license": "BSD-3-Clause" }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", - "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", - "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", "cpu": [ "arm64" ], @@ -1817,12 +1711,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", - "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", "cpu": [ "arm64" ], @@ -1831,12 +1728,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", - "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", "cpu": [ "x64" ], @@ -1845,26 +1745,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", - "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", - "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", "cpu": [ "x64" ], @@ -1873,12 +1762,15 @@ "optional": true, "os": [ "freebsd" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", - "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", "cpu": [ "arm" ], @@ -1887,194 +1779,135 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", - "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", - "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", - "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", - "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", - "cpu": [ - "loong64" + "libc": [ + "musl" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", - "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", - "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", - "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", - "cpu": [ - "ppc64" + "libc": [ + "glibc" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", - "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", - "cpu": [ - "riscv64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", - "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", - "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", - "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", - "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", - "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", - "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", "cpu": [ "arm64" ], @@ -2083,12 +1916,34 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", - "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", "cpu": [ "arm64" ], @@ -2097,26 +1952,15 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", - "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", - "cpu": [ - "ia32" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", - "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", "cpu": [ "x64" ], @@ -2125,21 +1969,17 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", - "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", - "cpu": [ - "x64" ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, "node_modules/@silvia-odwyer/photon-node": { "version": "0.3.4", @@ -2267,6 +2107,24 @@ "node": ">=14.0.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -2488,155 +2346,6 @@ "win32" ] }, - "node_modules/@vitest/coverage-v8": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", - "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@bcoe/v8-coverage": "^1.0.2", - "ast-v8-to-istanbul": "^0.3.3", - "debug": "^4.4.1", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.17", - "magicast": "^0.3.5", - "std-env": "^3.9.0", - "test-exclude": "^7.0.1", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "3.2.4", - "vitest": "3.2.4" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } - } - }, - "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "3.2.4", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@xterm/headless": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.5.0.tgz", @@ -2653,32 +2362,6 @@ "node": ">= 14" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -2698,18 +2381,6 @@ "node": ">=12" } }, - "node_modules/ast-v8-to-istanbul": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", - "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", - "js-tokens": "^10.0.0" - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -2840,16 +2511,6 @@ "node": ">=10.0.0" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/canvas": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.3.tgz", @@ -2877,23 +2538,6 @@ "node": ">=20" } }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -2906,16 +2550,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -2923,26 +2557,6 @@ "dev": true, "license": "ISC" }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", @@ -2952,6 +2566,13 @@ "node": ">=18" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cpu-features": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", @@ -3022,16 +2643,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -3061,13 +2672,6 @@ "node": ">=0.3.1" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -3077,13 +2681,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -3104,17 +2701,10 @@ "node": ">= 0.4" } }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, "node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3125,32 +2715,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/estree-walker": { @@ -3381,36 +2971,6 @@ "node": ">=8" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -3750,16 +3310,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3837,21 +3387,6 @@ "node": ">=8" } }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/istanbul-reports": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", @@ -3866,22 +3401,6 @@ "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -3941,6 +3460,279 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lodash-es": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", @@ -3953,13 +3745,6 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lru-cache": { "version": "11.4.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz", @@ -3979,18 +3764,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" - } - }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -4233,6 +4006,20 @@ "node": ">=4" } }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4293,13 +4080,6 @@ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "license": "MIT" }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/partial-json": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", @@ -4360,16 +4140,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/pi-extension-custom-provider-anthropic": { "resolved": "packages/coding-agent/examples/extensions/custom-provider-anthropic", "link": true @@ -4411,9 +4181,9 @@ } }, "node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -4431,7 +4201,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4488,24 +4258,23 @@ } }, "node_modules/protobufjs": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.9.tgz", - "integrity": "sha512-Od4muIm3HW1AouyHF5lONOf1FWo3hY1NbFDoy191X9GzhpgW1clCoaFjfVs2rKJNFYpTNJbje4cbAIDBZJ63ZA==", + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", + "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" }, "engines": { "node": ">=12.0.0" @@ -4628,58 +4397,40 @@ "node": ">=0.10.0" } }, - "node_modules/rollup": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", - "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.4", - "@rollup/rollup-android-arm64": "4.60.4", - "@rollup/rollup-darwin-arm64": "4.60.4", - "@rollup/rollup-darwin-x64": "4.60.4", - "@rollup/rollup-freebsd-arm64": "4.60.4", - "@rollup/rollup-freebsd-x64": "4.60.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", - "@rollup/rollup-linux-arm-musleabihf": "4.60.4", - "@rollup/rollup-linux-arm64-gnu": "4.60.4", - "@rollup/rollup-linux-arm64-musl": "4.60.4", - "@rollup/rollup-linux-loong64-gnu": "4.60.4", - "@rollup/rollup-linux-loong64-musl": "4.60.4", - "@rollup/rollup-linux-ppc64-gnu": "4.60.4", - "@rollup/rollup-linux-ppc64-musl": "4.60.4", - "@rollup/rollup-linux-riscv64-gnu": "4.60.4", - "@rollup/rollup-linux-riscv64-musl": "4.60.4", - "@rollup/rollup-linux-s390x-gnu": "4.60.4", - "@rollup/rollup-linux-x64-gnu": "4.60.4", - "@rollup/rollup-linux-x64-musl": "4.60.4", - "@rollup/rollup-openbsd-x64": "4.60.4", - "@rollup/rollup-openharmony-arm64": "4.60.4", - "@rollup/rollup-win32-arm64-msvc": "4.60.4", - "@rollup/rollup-win32-ia32-msvc": "4.60.4", - "@rollup/rollup-win32-x64-gnu": "4.60.4", - "@rollup/rollup-win32-x64-msvc": "4.60.4", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" } }, - "node_modules/rollup/node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4905,13 +4656,6 @@ "dev": true, "license": "MIT" }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, - "license": "MIT" - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -4922,64 +4666,6 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", @@ -5000,26 +4686,6 @@ "node": ">=0.10.0" } }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, "node_modules/strnum": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", @@ -5075,100 +4741,6 @@ "node": ">=6" } }, - "node_modules/test-exclude": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", - "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^10.2.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/test-exclude/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/test-exclude/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -5176,17 +4748,10 @@ "dev": true, "license": "MIT" }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -5231,36 +4796,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", - "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5345,9 +4880,9 @@ } }, "node_modules/undici": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz", - "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz", + "integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==", "license": "MIT", "engines": { "node": ">=22.19.0" @@ -5367,18 +4902,17 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", - "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -5394,9 +4928,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -5409,15 +4944,18 @@ "@types/node": { "optional": true }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, "jiti": { "optional": true }, "less": { "optional": true }, - "lightningcss": { - "optional": true - }, "sass": { "optional": true }, @@ -5441,531 +4979,6 @@ } } }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/vite/node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", @@ -5979,92 +4992,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -6106,25 +5033,6 @@ "node": ">=8" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -6133,9 +5041,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", - "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -6213,9 +5121,9 @@ }, "devDependencies": { "@types/node": "24.12.4", - "@vitest/coverage-v8": "3.2.4", + "@vitest/coverage-v8": "4.1.9", "typescript": "5.9.3", - "vitest": "3.2.4" + "vitest": "4.1.9" }, "engines": { "node": ">=22.19.0" @@ -6231,6 +5139,204 @@ "undici-types": "~7.16.0" } }, + "packages/agent/node_modules/@vitest/coverage-v8": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.9.tgz", + "integrity": "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.9", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.9", + "vitest": "4.1.9" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "packages/agent/node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/agent/node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/agent/node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/agent/node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/agent/node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/agent/node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/agent/node_modules/ast-v8-to-istanbul": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.4.tgz", + "integrity": "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "packages/agent/node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "packages/agent/node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "packages/agent/node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "packages/agent/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "packages/agent/node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "packages/agent/node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "packages/agent/node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "packages/agent/node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -6238,6 +5344,123 @@ "dev": true, "license": "MIT" }, + "packages/agent/node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "packages/agent/node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, "packages/ai": { "name": "@earendil-works/pi-ai", "version": "0.79.7", @@ -6261,7 +5484,7 @@ "devDependencies": { "@types/node": "24.12.4", "canvas": "3.2.3", - "vitest": "3.2.4" + "vitest": "4.1.9" }, "engines": { "node": ">=22.19.0" @@ -6277,6 +5500,176 @@ "undici-types": "~7.16.0" } }, + "packages/ai/node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/ai/node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "packages/ai/node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/ai/node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/ai/node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/ai/node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/ai/node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/ai/node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "packages/ai/node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "packages/ai/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "packages/ai/node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "packages/ai/node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "packages/ai/node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "packages/ai/node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -6284,6 +5677,96 @@ "dev": true, "license": "MIT" }, + "packages/ai/node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", "version": "0.79.7", @@ -6305,7 +5788,7 @@ "proper-lockfile": "4.1.2", "semver": "7.8.0", "typebox": "1.1.38", - "undici": "8.3.0", + "undici": "8.5.0", "yaml": "2.9.0" }, "bin": { @@ -6321,7 +5804,7 @@ "@types/semver": "7.7.1", "shx": "0.4.0", "typescript": "5.9.3", - "vitest": "3.2.4" + "vitest": "4.1.9" }, "engines": { "node": ">=22.19.0" @@ -6384,6 +5867,149 @@ "undici-types": "~7.16.0" } }, + "packages/coding-agent/node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/coding-agent/node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/coding-agent/node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/coding-agent/node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/coding-agent/node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/coding-agent/node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/coding-agent/node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "packages/coding-agent/node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "packages/coding-agent/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "packages/coding-agent/node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "packages/coding-agent/node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "packages/coding-agent/node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "packages/coding-agent/node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -6391,6 +6017,123 @@ "dev": true, "license": "MIT" }, + "packages/coding-agent/node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "packages/coding-agent/node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, "packages/tui": { "name": "@earendil-works/pi-tui", "version": "0.79.7", diff --git a/package.json b/package.json index c88d7123..0deabeca 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@biomejs/biome": "2.3.5", "@types/node": "22.19.19", "@typescript/native-preview": "7.0.0-dev.20260120.1", - "esbuild": "0.28.0", + "esbuild": "0.28.1", "husky": "9.1.7", "jiti": "2.7.0", "shx": "0.4.0", diff --git a/packages/agent/package.json b/packages/agent/package.json index 48934f8f..ee24151f 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -57,8 +57,8 @@ }, "devDependencies": { "@types/node": "24.12.4", - "@vitest/coverage-v8": "3.2.4", + "@vitest/coverage-v8": "4.1.9", "typescript": "5.9.3", - "vitest": "3.2.4" + "vitest": "4.1.9" } } diff --git a/packages/ai/package.json b/packages/ai/package.json index 1b34843a..68165eba 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -114,6 +114,6 @@ "devDependencies": { "@types/node": "24.12.4", "canvas": "3.2.3", - "vitest": "3.2.4" + "vitest": "4.1.9" } } diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index ad672bf2..934827aa 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -25,7 +25,7 @@ "proper-lockfile": "4.1.2", "semver": "7.8.0", "typebox": "1.1.38", - "undici": "8.3.0", + "undici": "8.5.0", "yaml": "2.9.0" }, "optionalDependencies": { @@ -810,9 +810,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { @@ -830,12 +830,6 @@ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", "license": "BSD-3-Clause" }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", - "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", - "license": "BSD-3-Clause" - }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", @@ -1613,23 +1607,22 @@ } }, "node_modules/protobufjs": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.9.tgz", - "integrity": "sha512-Od4muIm3HW1AouyHF5lONOf1FWo3hY1NbFDoy191X9GzhpgW1clCoaFjfVs2rKJNFYpTNJbje4cbAIDBZJ63ZA==", + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", + "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" }, "engines": { "node": ">=12.0.0" @@ -1735,9 +1728,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz", - "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz", + "integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==", "license": "MIT", "engines": { "node": ">=22.19.0" @@ -1774,9 +1767,9 @@ } }, "node_modules/ws": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", - "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "peerDependencies": { "bufferutil": "^4.0.1", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index c873bb8d..3d890c7a 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -52,7 +52,7 @@ "proper-lockfile": "4.1.2", "semver": "7.8.0", "typebox": "1.1.38", - "undici": "8.3.0", + "undici": "8.5.0", "yaml": "2.9.0" }, "overrides": { @@ -74,7 +74,7 @@ "@types/semver": "7.7.1", "shx": "0.4.0", "typescript": "5.9.3", - "vitest": "3.2.4" + "vitest": "4.1.9" }, "keywords": [ "coding-agent", diff --git a/scripts/generate-coding-agent-shrinkwrap.mjs b/scripts/generate-coding-agent-shrinkwrap.mjs index 7bcf5681..1971869c 100644 --- a/scripts/generate-coding-agent-shrinkwrap.mjs +++ b/scripts/generate-coding-agent-shrinkwrap.mjs @@ -12,7 +12,7 @@ const shrinkwrapPath = join(codingAgentDir, "npm-shrinkwrap.json"); const internalPackagePrefix = "@earendil-works/pi-"; const allowedInstallScriptPackages = new Map([ ["@google/genai@1.52.0", "preinstall is a no-op in the published package"], - ["protobufjs@7.5.9", "postinstall only warns about protobufjs version scheme mismatches"], + ["protobufjs@7.6.4", "postinstall only warns about protobufjs version scheme mismatches"], ]); const args = new Set(process.argv.slice(2)); From a2f70e5f2817fd37433109c4aa7128019fd9bf97 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 19 Jun 2026 09:22:13 +0200 Subject: [PATCH 039/122] fix(coding-agent): reset tool test mocks --- packages/coding-agent/test/tools.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index c5e8fa6f..f187da62 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -44,6 +44,7 @@ describe("Coding Agent Tools", () => { }); afterEach(() => { + vi.restoreAllMocks(); // Clean up test directory rmSync(testDir, { recursive: true, force: true }); }); From 74677bbfbf7fd089b47b4197b8ef21468d5f2878 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 19 Jun 2026 09:45:03 +0200 Subject: [PATCH 040/122] docs: audit unreleased changelogs --- packages/agent/CHANGELOG.md | 4 ++++ packages/ai/CHANGELOG.md | 1 + packages/coding-agent/CHANGELOG.md | 3 +++ 3 files changed, 8 insertions(+) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 3050117f..d9a204c6 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added `@earendil-works/pi-agent-core/base` for bundlers that want to pair the agent core with selective `@earendil-works/pi-ai/base` provider registration ([#5348](https://github.com/earendil-works/pi/pull/5348) by [@FredKSchott](https://github.com/FredKSchott)). + ## [0.79.7] - 2026-06-18 ## [0.79.6] - 2026-06-16 diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index c4c63d8b..2aab174a 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +- Added `@earendil-works/pi-ai/base` and direct provider registration exports for bundlers that want selective provider transports without root built-in registration ([#5348](https://github.com/earendil-works/pi/pull/5348) by [@FredKSchott](https://github.com/FredKSchott)). - Added prompt caching for Mistral requests using the pi session ID as `prompt_cache_key`, including cached-token usage and cost accounting ([#5854](https://github.com/earendil-works/pi/issues/5854)). - Added the OpenRouter Fusion alias as `openrouter/fusion` ([#5866](https://github.com/earendil-works/pi/pull/5866) by [@dannote](https://github.com/dannote)). diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 06ab4f9b..2f0ecb2a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,18 +4,21 @@ ### New Features +- **Selective provider base entry points** - SDK users can pair `@earendil-works/pi-ai/base` and `@earendil-works/pi-agent-core/base` with explicit provider registration to keep bundled applications from including unused provider transports. See [`pi-ai` Base Entry Point](../ai/README.md#base-entry-point) and [`pi-agent-core` Base Entry Point](../agent/README.md#base-entry-point). - **Mistral prompt caching** - Mistral sessions now use provider-side prompt caching with session affinity and cached-token usage/cost accounting. See [API Keys](docs/providers.md#api-keys) and [Environment Variables](docs/usage.md#environment-variables). - **Post-compaction token estimates** - Compact results and compaction events now include estimated post-compaction token counts so clients can show the approximate context reduction. See [RPC compact](docs/rpc.md#compact) and [compaction events](docs/rpc.md#compaction_start--compaction_end). - **OpenRouter Fusion alias** - `openrouter/fusion` is available as a built-in OpenRouter model alias. See [API Keys](docs/providers.md#api-keys). ### Added +- Added inherited `@earendil-works/pi-ai/base` and `@earendil-works/pi-agent-core/base` entry points for selective provider registration in bundled applications ([#5348](https://github.com/earendil-works/pi/pull/5348) by [@FredKSchott](https://github.com/FredKSchott)). - Added inherited Mistral prompt caching using the pi session ID as `prompt_cache_key`, including cached-token usage and cost accounting ([#5854](https://github.com/earendil-works/pi/issues/5854)). - Added estimated post-compaction token counts to compact results and compaction events ([#5877](https://github.com/earendil-works/pi/issues/5877)). - Added the inherited OpenRouter Fusion alias as `openrouter/fusion` ([#5866](https://github.com/earendil-works/pi/pull/5866) by [@dannote](https://github.com/dannote)). ### Fixed +- Updated vulnerable runtime dependencies, including `undici` and the packaged `protobufjs` transitive dependency. - Fixed compaction to refuse sessions with no eligible messages instead of producing empty summaries ([#4811](https://github.com/earendil-works/pi/issues/4811)). - Fixed successful overflow-triggered auto-compaction to avoid retrying completed assistant responses ([#5720](https://github.com/earendil-works/pi/issues/5720)). From 8eb9704b3e75391dcc4f584b7fc1a03c9e04e383 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 19 Jun 2026 09:53:43 +0200 Subject: [PATCH 041/122] Release v0.79.8 --- package-lock.json | 26 +++++------ packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 +- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- packages/ai/src/models.generated.ts | 45 +++++++++++++------ packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/gondolin/package-lock.json | 4 +- .../examples/extensions/gondolin/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/npm-shrinkwrap.json | 24 +++++----- packages/coding-agent/package.json | 8 ++-- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- 20 files changed, 81 insertions(+), 64 deletions(-) diff --git a/package-lock.json b/package-lock.json index ce9dd552..1f9d4e66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5111,10 +5111,10 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.79.7", + "version": "0.79.8", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.7", + "@earendil-works/pi-ai": "^0.79.8", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -5463,7 +5463,7 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.79.7", + "version": "0.79.8", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -5769,12 +5769,12 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.7", + "version": "0.79.8", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.7", - "@earendil-works/pi-ai": "^0.79.7", - "@earendil-works/pi-tui": "^0.79.7", + "@earendil-works/pi-agent-core": "^0.79.8", + "@earendil-works/pi-ai": "^0.79.8", + "@earendil-works/pi-tui": "^0.79.8", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -5815,32 +5815,32 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.79.7", + "version": "0.79.8", "dependencies": { "@anthropic-ai/sdk": "0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.79.7" + "version": "0.79.8" }, "packages/coding-agent/examples/extensions/gondolin": { "name": "pi-extension-gondolin", - "version": "0.79.7", + "version": "0.79.8", "dependencies": { "@earendil-works/gondolin": "0.12.0" } }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.9.7", + "version": "1.9.8", "dependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.79.7", + "version": "0.79.8", "dependencies": { "ms": "2.1.3" }, @@ -6136,7 +6136,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.79.7", + "version": "0.79.8", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index d9a204c6..8adfae20 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.8] - 2026-06-19 ### Added diff --git a/packages/agent/package.json b/packages/agent/package.json index ee24151f..b3aa9bae 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.79.7", + "version": "0.79.8", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -33,7 +33,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.79.7", + "@earendil-works/pi-ai": "^0.79.8", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 2aab174a..f3d696fd 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.8] - 2026-06-19 ### Added diff --git a/packages/ai/package.json b/packages/ai/package.json index 68165eba..830df19e 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.79.7", + "version": "0.79.8", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 82d0e137..8639bf13 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -6067,7 +6067,7 @@ export const MODELS = { cost: { input: 1.5, output: 7.5, - cacheRead: 0.15, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, @@ -10573,13 +10573,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.68, - output: 3.41, - cacheRead: 0.34, + input: 0.67, + output: 3.5, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262142, + maxTokens: 262144, } satisfies Model<"openai-completions">, "moonshotai/kimi-k2.7-code": { id: "moonshotai/kimi-k2.7-code", @@ -11556,7 +11556,7 @@ export const MODELS = { cost: { input: 0.075, output: 0.3, - cacheRead: 0.037, + cacheRead: 0.0375, cacheWrite: 0, }, contextWindow: 131072, @@ -11749,6 +11749,23 @@ export const MODELS = { contextWindow: 200000, maxTokens: 4096, } satisfies Model<"openai-completions">, + "openrouter/fusion": { + id: "openrouter/fusion", + name: "OpenRouter: Fusion", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 30000, + } satisfies Model<"openai-completions">, "openrouter/owl-alpha": { id: "openrouter/owl-alpha", name: "Owl Alpha", @@ -13067,13 +13084,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 1.4, - output: 4.4, - cacheRead: 0.7, + input: 1.2, + output: 4.1, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 524288, + maxTokens: 131072, } satisfies Model<"openai-completions">, "~anthropic/claude-fable-latest": { id: "~anthropic/claude-fable-latest", @@ -13186,13 +13203,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.68, - output: 3.41, - cacheRead: 0.34, + input: 0.67, + output: 3.5, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262142, + maxTokens: 262144, } satisfies Model<"openai-completions">, "~openai/gpt-latest": { id: "~openai/gpt-latest", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 2f0ecb2a..e52073bc 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.8] - 2026-06-19 ### New Features diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index c3f35b25..436f151f 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.79.7", + "version": "0.79.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.79.7", + "version": "0.79.8", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index c817a94e..aa509051 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.79.7", + "version": "0.79.8", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index 12363537..51de298b 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.79.7", + "version": "0.79.8", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/gondolin/package-lock.json b/packages/coding-agent/examples/extensions/gondolin/package-lock.json index ea80b459..3f126430 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package-lock.json +++ b/packages/coding-agent/examples/extensions/gondolin/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-gondolin", - "version": "0.79.7", + "version": "0.79.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-gondolin", - "version": "0.79.7", + "version": "0.79.8", "dependencies": { "@earendil-works/gondolin": "0.12.0" } diff --git a/packages/coding-agent/examples/extensions/gondolin/package.json b/packages/coding-agent/examples/extensions/gondolin/package.json index 89705d97..1d42d841 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package.json +++ b/packages/coding-agent/examples/extensions/gondolin/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-gondolin", "private": true, - "version": "0.79.7", + "version": "0.79.8", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 41b01437..2a587f88 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.9.7", + "version": "1.9.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.9.7", + "version": "1.9.8", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index a80abe4a..c9d4aa3f 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.9.7", + "version": "1.9.8", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index b45b2214..2dc3506b 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.79.7", + "version": "0.79.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.79.7", + "version": "0.79.8", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index 02abd397..1c409ca4 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.79.7", + "version": "0.79.8", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index 934827aa..13ff9161 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -1,17 +1,17 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.7", + "version": "0.79.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.7", + "version": "0.79.8", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.7", - "@earendil-works/pi-ai": "^0.79.7", - "@earendil-works/pi-tui": "^0.79.7", + "@earendil-works/pi-agent-core": "^0.79.8", + "@earendil-works/pi-ai": "^0.79.8", + "@earendil-works/pi-tui": "^0.79.8", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -474,11 +474,11 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.79.7", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.7.tgz", + "version": "0.79.8", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.8.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.7", + "@earendil-works/pi-ai": "^0.79.8", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -488,8 +488,8 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.79.7", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.7.tgz", + "version": "0.79.8", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.8.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -512,8 +512,8 @@ } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.79.7", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.7.tgz", + "version": "0.79.8", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.8.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 3d890c7a..4aa30dd2 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.7", + "version": "0.79.8", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -36,9 +36,9 @@ "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.7", - "@earendil-works/pi-ai": "^0.79.7", - "@earendil-works/pi-tui": "^0.79.7", + "@earendil-works/pi-agent-core": "^0.79.8", + "@earendil-works/pi-ai": "^0.79.8", + "@earendil-works/pi-tui": "^0.79.8", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 4db80b91..9f54a3e3 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.8] - 2026-06-19 ## [0.79.7] - 2026-06-18 diff --git a/packages/tui/package.json b/packages/tui/package.json index 24590a18..36f002a7 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.79.7", + "version": "0.79.8", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", From 56b22768db0bf06ba09719ea9a62f0b29065aaad Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 19 Jun 2026 09:53:45 +0200 Subject: [PATCH 042/122] Add [Unreleased] section for next cycle --- packages/agent/CHANGELOG.md | 2 ++ packages/ai/CHANGELOG.md | 2 ++ packages/coding-agent/CHANGELOG.md | 2 ++ packages/tui/CHANGELOG.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 8adfae20..0ac61b2f 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.8] - 2026-06-19 ### Added diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index f3d696fd..63e7bba7 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.8] - 2026-06-19 ### Added diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e52073bc..53f60f55 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.8] - 2026-06-19 ### New Features diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 9f54a3e3..2e9f6d31 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.8] - 2026-06-19 ## [0.79.7] - 2026-06-18 From 783571a615b08b286247b86f383197d43990c2a9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 19 Jun 2026 11:03:11 +0200 Subject: [PATCH 043/122] feat: track auto-closed issue triage --- .github/workflows/issue-gate.yml | 7 ++ .github/workflows/issue-triage-labels.yml | 120 ++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 .github/workflows/issue-triage-labels.yml diff --git a/.github/workflows/issue-gate.yml b/.github/workflows/issue-gate.yml index 62663940..a8103fd1 100644 --- a/.github/workflows/issue-gate.yml +++ b/.github/workflows/issue-gate.yml @@ -111,6 +111,13 @@ jobs: body: message, }); + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: ['untriaged'], + }); + await github.rest.issues.update({ owner: context.repo.owner, repo: context.repo.repo, diff --git a/.github/workflows/issue-triage-labels.yml b/.github/workflows/issue-triage-labels.yml new file mode 100644 index 00000000..0be1e669 --- /dev/null +++ b/.github/workflows/issue-triage-labels.yml @@ -0,0 +1,120 @@ +name: Issue Triage Labels + +on: + issues: + types: [reopened, labeled] + +jobs: + update-labels: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Update triage labels + uses: actions/github-script@v7 + with: + script: | + const UNTRIAGED_LABEL = 'untriaged'; + const NO_ACTION_LABEL = 'no-action'; + const LAST_READ_LABEL = 'last-read'; + + function issueHasLabel(issue, labelName) { + return (issue.labels ?? []).some((label) => label.name === labelName); + } + + async function removeLabelIfPresent(issueNumber, issue, labelName) { + if (!issueHasLabel(issue, labelName)) { + console.log(`Issue #${issueNumber} does not have ${labelName}`); + return; + } + + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + name: labelName, + }); + console.log(`Removed ${labelName} from #${issueNumber}`); + } catch (error) { + if (error.status === 404) { + console.log(`Label ${labelName} was already absent from #${issueNumber}`); + return; + } + throw error; + } + } + + if (context.payload.action === 'reopened') { + await removeLabelIfPresent(context.issue.number, context.payload.issue, UNTRIAGED_LABEL); + await removeLabelIfPresent(context.issue.number, context.payload.issue, NO_ACTION_LABEL); + return; + } + + if (context.payload.action !== 'labeled' || context.payload.label?.name !== LAST_READ_LABEL) { + console.log('Not a last-read label event'); + return; + } + + const currentIssueNumber = context.issue.number; + const lastReadIssues = await github.paginate(github.rest.issues.listForRepo, { + owner: context.repo.owner, + repo: context.repo.repo, + state: 'all', + labels: LAST_READ_LABEL, + per_page: 100, + }); + + const previousIssueNumbers = lastReadIssues + .filter((issue) => !issue.pull_request) + .map((issue) => issue.number) + .filter((issueNumber) => issueNumber !== currentIssueNumber); + + if (previousIssueNumbers.length === 0) { + console.log('No previous last-read issue found'); + return; + } + + const previousIssueNumber = Math.max(...previousIssueNumbers); + if (currentIssueNumber <= previousIssueNumber) { + console.log( + `Last-read was added to old issue #${currentIssueNumber}; latest last-read is #${previousIssueNumber}`, + ); + return; + } + + const untriagedIssues = await github.paginate(github.rest.issues.listForRepo, { + owner: context.repo.owner, + repo: context.repo.repo, + state: 'all', + labels: UNTRIAGED_LABEL, + per_page: 100, + }); + + const issuesToMark = untriagedIssues + .filter((issue) => !issue.pull_request) + .filter((issue) => issue.number >= previousIssueNumber && issue.number <= currentIssueNumber) + .sort((a, b) => a.number - b.number); + + if (issuesToMark.length === 0) { + console.log(`No untriaged issues found from #${previousIssueNumber} to #${currentIssueNumber}`); + return; + } + + for (const issue of issuesToMark) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: [NO_ACTION_LABEL], + }); + console.log(`Added ${NO_ACTION_LABEL} to #${issue.number}`); + + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: UNTRIAGED_LABEL, + }); + console.log(`Removed ${UNTRIAGED_LABEL} from #${issue.number}`); + } From 47d1d90a8ed156d7d0ca90265f4ac60c75f56621 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 19 Jun 2026 11:08:16 +0200 Subject: [PATCH 044/122] fix: close no-action issues as not planned --- .github/workflows/issue-triage-labels.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/issue-triage-labels.yml b/.github/workflows/issue-triage-labels.yml index 0be1e669..4a9ea9d7 100644 --- a/.github/workflows/issue-triage-labels.yml +++ b/.github/workflows/issue-triage-labels.yml @@ -110,6 +110,15 @@ jobs: }); console.log(`Added ${NO_ACTION_LABEL} to #${issue.number}`); + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + state: 'closed', + state_reason: 'not_planned', + }); + console.log(`Closed #${issue.number} as not planned`); + await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, From 373cd6ae52b6a8954ee530b887bd39a2dc820931 Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Fri, 19 Jun 2026 12:25:02 +0200 Subject: [PATCH 045/122] fix(coding-agent): prioritize provider matches in model selector closes #5892 --- packages/coding-agent/CHANGELOG.md | 4 ++++ .../src/modes/interactive/components/model-selector.ts | 4 ++-- .../coding-agent/src/modes/interactive/model-search.ts | 10 ++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 53f60f55..07529966 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed `/model` selector search to rank exact provider-prefixed matches before proxy-provider model ID matches ([#5892](https://github.com/earendil-works/pi/issues/5892)). + ## [0.79.8] - 2026-06-19 ### New Features diff --git a/packages/coding-agent/src/modes/interactive/components/model-selector.ts b/packages/coding-agent/src/modes/interactive/components/model-selector.ts index a3cdf7d5..32711929 100644 --- a/packages/coding-agent/src/modes/interactive/components/model-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/model-selector.ts @@ -11,7 +11,7 @@ import { } from "@earendil-works/pi-tui"; import type { ModelRegistry } from "../../../core/model-registry.ts"; import type { SettingsManager } from "../../../core/settings-manager.ts"; -import { getModelSearchText } from "../model-search.ts"; +import { getModelSelectorSearchText } from "../model-search.ts"; import { theme } from "../theme/theme.ts"; import { DynamicBorder } from "./dynamic-border.ts"; import { keyHint } from "./keybinding-hints.ts"; @@ -219,7 +219,7 @@ export class ModelSelectorComponent extends Container implements Focusable { private filterModels(query: string): void { this.filteredModels = query ? fuzzyFilter(this.activeModels, query, ({ id, provider, model }) => - getModelSearchText({ id, provider, name: model.name }), + getModelSelectorSearchText({ id, provider, name: model.name }), ) : this.activeModels; this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1)); diff --git a/packages/coding-agent/src/modes/interactive/model-search.ts b/packages/coding-agent/src/modes/interactive/model-search.ts index f1dbc73c..bab9c5a5 100644 --- a/packages/coding-agent/src/modes/interactive/model-search.ts +++ b/packages/coding-agent/src/modes/interactive/model-search.ts @@ -9,3 +9,13 @@ export function getModelSearchText(item: ModelSearchItem): string { const name = item.name ? ` ${item.name}` : ""; return `${id} ${provider} ${provider}/${id} ${provider} ${id}${name}`; } + +/** + * The /model selector search should rank exact provider-prefixed queries before proxy-provider IDs + * like openrouter/openai/gpt-5, so keep the bare model ID out of the leading position. + */ +export function getModelSelectorSearchText(item: ModelSearchItem): string { + const { id, provider } = item; + const name = item.name ? ` ${item.name}` : ""; + return `${provider} ${provider}/${id} ${provider} ${id}${name}`; +} From 226a316824dc1835ffc34acad274488f5fe298f6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 19 Jun 2026 12:46:31 +0200 Subject: [PATCH 046/122] fix: mark auto-closed issues not planned --- .github/workflows/issue-gate.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/issue-gate.yml b/.github/workflows/issue-gate.yml index a8103fd1..d2bf830e 100644 --- a/.github/workflows/issue-gate.yml +++ b/.github/workflows/issue-gate.yml @@ -123,4 +123,5 @@ jobs: repo: context.repo.repo, issue_number: context.issue.number, state: 'closed', + state_reason: 'not_planned', }); From 6e6ce70caf3328683517b0e308fdbbc6d1c1abc9 Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Fri, 19 Jun 2026 13:16:51 +0200 Subject: [PATCH 047/122] fix(ai): filter Copilot models by account availability closes #5897 --- packages/ai/CHANGELOG.md | 4 + packages/ai/src/utils/oauth/github-copilot.ts | 83 +++++++++++++++++-- packages/ai/test/github-copilot-oauth.test.ts | 70 +++++++++++++++- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/test/model-registry.test.ts | 19 +++++ 5 files changed, 169 insertions(+), 8 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 63e7bba7..4f316c3b 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed GitHub Copilot OAuth model availability to use the authenticated account's model picker catalog ([#5897](https://github.com/earendil-works/pi/issues/5897)). + ## [0.79.8] - 2026-06-19 ### Added diff --git a/packages/ai/src/utils/oauth/github-copilot.ts b/packages/ai/src/utils/oauth/github-copilot.ts index 6a27b9d1..bb78a93a 100644 --- a/packages/ai/src/utils/oauth/github-copilot.ts +++ b/packages/ai/src/utils/oauth/github-copilot.ts @@ -9,6 +9,7 @@ import type { OAuthCredentials, OAuthDeviceCodeInfo, OAuthLoginCallbacks, OAuthP type CopilotCredentials = OAuthCredentials & { enterpriseUrl?: string; + availableModelIds: string[]; }; const decode = (s: string) => atob(s); @@ -20,6 +21,7 @@ const COPILOT_HEADERS = { "Editor-Plugin-Version": "copilot-chat/0.35.0", "Copilot-Integration-Id": "vscode-chat", } as const; +const COPILOT_API_VERSION = "2026-06-01"; type DeviceCodeResponse = { device_code: string; @@ -88,6 +90,48 @@ export function getGitHubCopilotBaseUrl(token?: string, enterpriseDomain?: strin return "https://api.individual.githubcopilot.com"; } +function asRecord(value: unknown): Record | undefined { + return value && typeof value === "object" ? (value as Record) : undefined; +} + +function isSelectableCopilotModel(item: Record): boolean { + const policy = asRecord(item.policy); + const capabilities = asRecord(item.capabilities); + const supports = asRecord(capabilities?.supports); + return item.model_picker_enabled === true && policy?.state !== "disabled" && supports?.tool_calls !== false; +} + +function parseAvailableCopilotModelIds(raw: unknown): string[] { + const data = asRecord(raw)?.data; + if (!Array.isArray(data)) { + throw new Error("Invalid Copilot models response"); + } + + const ids: string[] = []; + for (const rawItem of data) { + const item = asRecord(rawItem); + const id = item?.id; + if (typeof id === "string" && item && isSelectableCopilotModel(item)) { + ids.push(id); + } + } + return ids; +} + +async function fetchAvailableGitHubCopilotModelIds(copilotToken: string, enterpriseDomain?: string): Promise { + const baseUrl = getGitHubCopilotBaseUrl(copilotToken, enterpriseDomain); + const raw = await fetchJson(`${baseUrl}/models`, { + headers: { + Accept: "application/json", + Authorization: `Bearer ${copilotToken}`, + ...COPILOT_HEADERS, + "X-GitHub-Api-Version": COPILOT_API_VERSION, + }, + signal: AbortSignal.timeout(5000), + }); + return parseAvailableCopilotModelIds(raw); +} + async function fetchJson(url: string, init: RequestInit): Promise { const response = await fetch(url, init); if (!response.ok) { @@ -201,10 +245,7 @@ async function pollForGitHubAccessToken( }); } -/** - * Refresh GitHub Copilot token - */ -export async function refreshGitHubCopilotToken( +async function refreshGitHubCopilotAccessToken( refreshToken: string, enterpriseDomain?: string, ): Promise { @@ -238,6 +279,20 @@ export async function refreshGitHubCopilotToken( }; } +/** + * Refresh GitHub Copilot token + */ +export async function refreshGitHubCopilotToken( + refreshToken: string, + enterpriseDomain?: string, +): Promise { + const credentials = await refreshGitHubCopilotAccessToken(refreshToken, enterpriseDomain); + return { + ...credentials, + availableModelIds: await fetchAvailableGitHubCopilotModelIds(credentials.access, enterpriseDomain), + }; +} + /** * Enable a model for the user's GitHub Copilot account. * This is required for some models (like Claude, Grok) before they can be used. @@ -322,12 +377,18 @@ export async function loginGitHubCopilot(options: { }); const githubAccessToken = await pollForGitHubAccessToken(domain, device, options.signal); - const credentials = await refreshGitHubCopilotToken(githubAccessToken, enterpriseDomain ?? undefined); + const credentials = await refreshGitHubCopilotAccessToken(githubAccessToken, enterpriseDomain ?? undefined); // Enable all models after successful login options.onProgress?.("Enabling models..."); await enableAllGitHubCopilotModels(credentials.access, enterpriseDomain ?? undefined); - return credentials; + + // Fetch availability after policy enable so newly enabled models are included, + // while unavailable models are still filtered out. + return { + ...credentials, + availableModelIds: await fetchAvailableGitHubCopilotModelIds(credentials.access, enterpriseDomain ?? undefined), + }; } export const githubCopilotOAuthProvider: OAuthProviderInterface = { @@ -356,6 +417,14 @@ export const githubCopilotOAuthProvider: OAuthProviderInterface = { const creds = credentials as CopilotCredentials; const domain = creds.enterpriseUrl ? (normalizeDomain(creds.enterpriseUrl) ?? undefined) : undefined; const baseUrl = getGitHubCopilotBaseUrl(creds.access, domain); - return models.map((m) => (m.provider === "github-copilot" ? { ...m, baseUrl } : m)); + // Older stored Pi auth entries do not have account-specific model IDs yet; + // keep their existing generated-catalog behavior until the next refresh/login. + const availableModelIds = "availableModelIds" in creds ? new Set(creds.availableModelIds) : undefined; + + return models.flatMap((m) => { + if (m.provider !== "github-copilot") return [m]; + if (availableModelIds && !availableModelIds.has(m.id)) return []; + return [{ ...m, baseUrl }]; + }); }, }; diff --git a/packages/ai/test/github-copilot-oauth.test.ts b/packages/ai/test/github-copilot-oauth.test.ts index c94370da..2aba3084 100644 --- a/packages/ai/test/github-copilot-oauth.test.ts +++ b/packages/ai/test/github-copilot-oauth.test.ts @@ -1,5 +1,10 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { loginGitHubCopilot } from "../src/utils/oauth/github-copilot.ts"; +import { getModels } from "../src/models.ts"; +import { + githubCopilotOAuthProvider, + loginGitHubCopilot, + refreshGitHubCopilotToken, +} from "../src/utils/oauth/github-copilot.ts"; function jsonResponse(body: unknown, status: number = 200): Response { return new Response(JSON.stringify(body), { @@ -29,6 +34,57 @@ describe("GitHub Copilot OAuth device flow", () => { vi.useRealTimers(); }); + it("filters models to the authenticated account picker catalog", async () => { + const fetchMock = vi.fn(async (input: unknown, init?: RequestInit): Promise => { + const url = getUrl(input); + + if (url.includes("/copilot_internal/v2/token")) { + return jsonResponse({ + token: "tid=test;exp=9999999999;proxy-ep=proxy.individual.githubcopilot.com;", + expires_at: 9999999999, + }); + } + + if (url === "https://api.individual.githubcopilot.com/models") { + expect(init?.headers).toMatchObject({ + Authorization: "Bearer tid=test;exp=9999999999;proxy-ep=proxy.individual.githubcopilot.com;", + }); + return jsonResponse({ + data: [ + { + id: "gpt-4.1", + model_picker_enabled: true, + capabilities: { supports: { tool_calls: true } }, + }, + { + id: "claude-opus-4.7", + model_picker_enabled: true, + policy: { state: "disabled" }, + capabilities: { supports: { tool_calls: true } }, + }, + { + id: "gpt-5.4-nano", + model_picker_enabled: false, + capabilities: { supports: { tool_calls: true } }, + }, + ], + }); + } + + throw new Error(`Unexpected fetch URL: ${url}`); + }); + + vi.stubGlobal("fetch", fetchMock); + + const credentials = await refreshGitHubCopilotToken("ghu_refresh_token"); + expect(credentials.availableModelIds).toEqual(["gpt-4.1"]); + + const modifiedModels = githubCopilotOAuthProvider.modifyModels?.(getModels("github-copilot"), credentials) ?? []; + expect(modifiedModels.filter((model) => model.provider === "github-copilot").map((model) => model.id)).toEqual([ + "gpt-4.1", + ]); + }); + it("reports device-code details through onDeviceCode", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-03-09T00:00:00Z")); @@ -57,6 +113,10 @@ describe("GitHub Copilot OAuth device flow", () => { }); } + if (url.endsWith("/models")) { + return jsonResponse({ data: [] }); + } + if (url.includes("/models/") && url.endsWith("/policy")) { return new Response("", { status: 200 }); } @@ -146,6 +206,10 @@ describe("GitHub Copilot OAuth device flow", () => { }); } + if (url.endsWith("/models")) { + return jsonResponse({ data: [] }); + } + if (url.includes("/models/") && url.endsWith("/policy")) { return new Response("", { status: 200 }); } @@ -231,6 +295,10 @@ describe("GitHub Copilot OAuth device flow", () => { }); } + if (url.endsWith("/models")) { + return jsonResponse({ data: [] }); + } + if (url.includes("/models/") && url.endsWith("/policy")) { return new Response("", { status: 200 }); } diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 07529966..d1245f20 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed `/model` to hide GitHub Copilot models that are unavailable to the authenticated account ([#5897](https://github.com/earendil-works/pi/issues/5897)). - Fixed `/model` selector search to rank exact provider-prefixed matches before proxy-provider model ID matches ([#5892](https://github.com/earendil-works/pi/issues/5892)). ## [0.79.8] - 2026-06-19 diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index fe8609f2..1d720628 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -1669,6 +1669,25 @@ describe("ModelRegistry", () => { expect(count).toBe(0); }); + test("getAvailable filters GitHub Copilot OAuth models to account picker availability", () => { + authStorage.set("github-copilot", { + type: "oauth", + refresh: "github-access-token", + access: "tid=test;exp=9999999999;proxy-ep=proxy.individual.githubcopilot.com;", + expires: Date.now() + 60_000, + availableModelIds: ["gpt-4.1"], + }); + + const registry = ModelRegistry.create(authStorage, modelsJsonPath); + + expect( + registry + .getAvailable() + .filter((m) => m.provider === "github-copilot") + .map((m) => m.id), + ).toEqual(["gpt-4.1"]); + }); + test("getApiKeyAndHeaders resolves authHeader on every request", async () => { const tokenFile = join(tempDir, "token"); writeFileSync(tokenFile, "token-1"); From 1287b69fe026a9c3f9cec8a220ad9405851f7dc3 Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Fri, 19 Jun 2026 15:05:16 +0200 Subject: [PATCH 048/122] fix(coding-agent): run legacy WSL bash commands via stdin closes #5893 --- packages/agent/CHANGELOG.md | 4 ++ packages/agent/src/harness/env/nodejs.ts | 52 +++++++++++++----- .../agent/test/harness/nodejs-env.test.ts | 35 +++++++++++- packages/coding-agent/CHANGELOG.md | 1 + .../src/core/resolve-config-value.ts | 8 ++- packages/coding-agent/src/core/tools/bash.ts | 11 +++- packages/coding-agent/src/utils/shell.ts | 20 +++++-- .../coding-agent/test/auth-storage.test.ts | 27 ++++++++- packages/coding-agent/test/tools.test.ts | 50 +++++++++++++++++ scripts/repro-5893-wsl-bash.mjs | 55 +++++++++++++++++++ 10 files changed, 235 insertions(+), 28 deletions(-) create mode 100644 scripts/repro-5893-wsl-bash.mjs diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 0ac61b2f..18d0e561 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed Node execution environment commands through legacy WSL `bash.exe` to pass scripts over stdin so shell variables expand in the target bash ([#5893](https://github.com/earendil-works/pi/issues/5893)). + ## [0.79.8] - 2026-06-19 ### Added diff --git a/packages/agent/src/harness/env/nodejs.ts b/packages/agent/src/harness/env/nodejs.ts index e56e7aeb..3d929c82 100644 --- a/packages/agent/src/harness/env/nodejs.ts +++ b/packages/agent/src/harness/env/nodejs.ts @@ -144,12 +144,25 @@ async function findBashOnPath(): Promise { return firstMatch && (await pathExists(firstMatch)) ? firstMatch : null; } -async function getShellConfig( - customShellPath?: string, -): Promise> { +interface ShellConfig { + shell: string; + args: string[]; + commandTransport?: "argv" | "stdin"; +} + +function isLegacyWslBashPath(path: string): boolean { + const normalized = path.replace(/\//g, "\\").toLowerCase(); + return /^[a-z]:\\windows\\(?:system32|sysnative)\\bash\.exe$/.test(normalized); +} + +function getBashShellConfig(shell: string): ShellConfig { + return isLegacyWslBashPath(shell) ? { shell, args: ["-s"], commandTransport: "stdin" } : { shell, args: ["-c"] }; +} + +async function getShellConfig(customShellPath?: string): Promise> { if (customShellPath) { if (await pathExists(customShellPath)) { - return ok({ shell: customShellPath, args: ["-c"] }); + return ok(getBashShellConfig(customShellPath)); } return err(new ExecutionError("shell_unavailable", `Custom shell path not found: ${customShellPath}`)); } @@ -161,22 +174,22 @@ async function getShellConfig( if (programFilesX86) candidates.push(`${programFilesX86}\\Git\\bin\\bash.exe`); for (const candidate of candidates) { if (await pathExists(candidate)) { - return ok({ shell: candidate, args: ["-c"] }); + return ok(getBashShellConfig(candidate)); } } const bashOnPath = await findBashOnPath(); if (bashOnPath) { - return ok({ shell: bashOnPath, args: ["-c"] }); + return ok(getBashShellConfig(bashOnPath)); } return err(new ExecutionError("shell_unavailable", "No bash shell found")); } if (await pathExists("/bin/bash")) { - return ok({ shell: "/bin/bash", args: ["-c"] }); + return ok(getBashShellConfig("/bin/bash")); } const bashOnPath = await findBashOnPath(); if (bashOnPath) { - return ok({ shell: bashOnPath, args: ["-c"] }); + return ok(getBashShellConfig(bashOnPath)); } return ok({ shell: "sh", args: ["-c"] }); } @@ -274,13 +287,22 @@ export class NodeExecutionEnv implements ExecutionEnv { }; try { - child = spawn(shellConfig.value.shell, [...shellConfig.value.args, command], { - cwd, - detached: process.platform !== "win32", - env: getShellEnv(this.shellEnv, options?.env), - stdio: ["ignore", "pipe", "pipe"], - windowsHide: true, - }); + const commandFromStdin = shellConfig.value.commandTransport === "stdin"; + child = spawn( + shellConfig.value.shell, + commandFromStdin ? shellConfig.value.args : [...shellConfig.value.args, command], + { + cwd, + detached: process.platform !== "win32", + env: getShellEnv(this.shellEnv, options?.env), + stdio: [commandFromStdin ? "pipe" : "ignore", "pipe", "pipe"], + windowsHide: true, + }, + ); + if (commandFromStdin) { + child.stdin?.on("error", () => {}); + child.stdin?.end(command); + } } catch (error) { const cause = toError(error); settle(err(new ExecutionError("spawn_error", cause.message, cause))); diff --git a/packages/agent/test/harness/nodejs-env.test.ts b/packages/agent/test/harness/nodejs-env.test.ts index 758d5f59..d2d33a6f 100644 --- a/packages/agent/test/harness/nodejs-env.test.ts +++ b/packages/agent/test/harness/nodejs-env.test.ts @@ -1,5 +1,5 @@ import { access, chmod, realpath, symlink } from "node:fs/promises"; -import { join } from "node:path"; +import { delimiter, join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts"; import { FileError, getOrThrow } from "../../src/harness/types.ts"; @@ -201,6 +201,39 @@ describe("NodeExecutionEnv", () => { expect(result).toEqual({ stdout: `${await realpath(root)}:ok`, stderr: "", exitCode: 0 }); }); + it("uses stdin command transport for legacy WSL bash paths", async () => { + if (process.platform === "win32") return; + const root = createTempDir(); + const shellPath = "C:\\Windows\\System32\\bash.exe"; + const env = new NodeExecutionEnv({ cwd: root }); + getOrThrow(await env.writeFile(shellPath, '#!/bin/sh\nprintf \'args:%s\\n\' "$*" >&2\nexec /bin/bash "$@"\n')); + await chmod(join(root, shellPath), 0o755); + + const originalCwd = process.cwd(); + const originalPath = process.env.PATH; + const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform"); + try { + process.chdir(root); + process.env.PATH = `${root}${delimiter}${originalPath ?? ""}`; + Object.defineProperty(process, "platform", { + configurable: true, + value: "win32", + }); + + const wslEnv = new NodeExecutionEnv({ cwd: root, shellPath }); + const nameExpansion = "$" + "{name}"; + const result = getOrThrow(await wslEnv.exec(`name='World'; echo "Hello, ${nameExpansion}!"`)); + + expect(result).toEqual({ stdout: "Hello, World!\n", stderr: "args:-s\n", exitCode: 0 }); + } finally { + process.chdir(originalCwd); + process.env.PATH = originalPath; + if (platformDescriptor) { + Object.defineProperty(process, "platform", platformDescriptor); + } + } + }); + it("streams stdout and stderr chunks", async () => { const root = createTempDir(); const env = new NodeExecutionEnv({ cwd: root }); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d1245f20..c5aae0f9 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed bash commands through legacy WSL `bash.exe` to pass scripts over stdin so shell variables expand in the target bash ([#5893](https://github.com/earendil-works/pi/issues/5893)). - Fixed `/model` to hide GitHub Copilot models that are unavailable to the authenticated account ([#5897](https://github.com/earendil-works/pi/issues/5897)). - Fixed `/model` selector search to rank exact provider-prefixed matches before proxy-provider model ID matches ([#5892](https://github.com/earendil-works/pi/issues/5892)). diff --git a/packages/coding-agent/src/core/resolve-config-value.ts b/packages/coding-agent/src/core/resolve-config-value.ts index 9d47cff3..6d75b001 100644 --- a/packages/coding-agent/src/core/resolve-config-value.ts +++ b/packages/coding-agent/src/core/resolve-config-value.ts @@ -152,11 +152,13 @@ export function resolveConfigValue(config: string, env?: Record) function executeWithConfiguredShell(command: string): { executed: boolean; value: string | undefined } { try { - const { shell, args } = getShellConfig(); - const result = spawnSync(shell, [...args, command], { + const { shell, args, commandTransport } = getShellConfig(); + const commandFromStdin = commandTransport === "stdin"; + const result = spawnSync(shell, commandFromStdin ? args : [...args, command], { encoding: "utf-8", + input: commandFromStdin ? command : undefined, timeout: 10000, - stdio: ["ignore", "pipe", "ignore"], + stdio: [commandFromStdin ? "pipe" : "ignore", "pipe", "ignore"], shell: false, windowsHide: true, }); diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index e56291bb..da6934e7 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -66,7 +66,7 @@ export interface BashOperations { export function createLocalBashOperations(options?: { shellPath?: string }): BashOperations { return { exec: async (command, cwd, { onData, signal, timeout, env }) => { - const { shell, args } = getShellConfig(options?.shellPath); + const shellConfig = getShellConfig(options?.shellPath); try { await fsAccess(cwd, constants.F_OK); } catch { @@ -76,13 +76,18 @@ export function createLocalBashOperations(options?: { shellPath?: string }): Bas throw new Error("aborted"); } - const child = spawn(shell, [...args, command], { + const commandFromStdin = shellConfig.commandTransport === "stdin"; + const child = spawn(shellConfig.shell, commandFromStdin ? shellConfig.args : [...shellConfig.args, command], { cwd, detached: process.platform !== "win32", env: env ?? getShellEnv(), - stdio: ["ignore", "pipe", "pipe"], + stdio: [commandFromStdin ? "pipe" : "ignore", "pipe", "pipe"], windowsHide: true, }); + if (commandFromStdin) { + child.stdin?.on("error", () => {}); + child.stdin?.end(command); + } if (child.pid) trackDetachedChildPid(child.pid); let timedOut = false; let timeoutHandle: NodeJS.Timeout | undefined; diff --git a/packages/coding-agent/src/utils/shell.ts b/packages/coding-agent/src/utils/shell.ts index 817ba4f9..2cafa595 100644 --- a/packages/coding-agent/src/utils/shell.ts +++ b/packages/coding-agent/src/utils/shell.ts @@ -6,11 +6,21 @@ import { getBinDir } from "../config.ts"; export interface ShellConfig { shell: string; args: string[]; + commandTransport?: "argv" | "stdin"; } /** * Find bash executable on PATH (cross-platform) */ +function isLegacyWslBashPath(path: string): boolean { + const normalized = path.replace(/\//g, "\\").toLowerCase(); + return /^[a-z]:\\windows\\(?:system32|sysnative)\\bash\.exe$/.test(normalized); +} + +function getBashShellConfig(shell: string): ShellConfig { + return isLegacyWslBashPath(shell) ? { shell, args: ["-s"], commandTransport: "stdin" } : { shell, args: ["-c"] }; +} + function findBashOnPath(): string | null { if (process.platform === "win32") { // Windows: Use 'where' and verify file exists (where can return non-existent paths) @@ -58,7 +68,7 @@ export function getShellConfig(customShellPath?: string): ShellConfig { // 1. Check user-specified shell path if (customShellPath) { if (existsSync(customShellPath)) { - return { shell: customShellPath, args: ["-c"] }; + return getBashShellConfig(customShellPath); } throw new Error(`Custom shell path not found: ${customShellPath}`); } @@ -77,14 +87,14 @@ export function getShellConfig(customShellPath?: string): ShellConfig { for (const path of paths) { if (existsSync(path)) { - return { shell: path, args: ["-c"] }; + return getBashShellConfig(path); } } // 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.) const bashOnPath = findBashOnPath(); if (bashOnPath) { - return { shell: bashOnPath, args: ["-c"] }; + return getBashShellConfig(bashOnPath); } throw new Error( @@ -98,12 +108,12 @@ export function getShellConfig(customShellPath?: string): ShellConfig { // Unix: try /bin/bash, then bash on PATH, then fallback to sh if (existsSync("/bin/bash")) { - return { shell: "/bin/bash", args: ["-c"] }; + return getBashShellConfig("/bin/bash"); } const bashOnPath = findBashOnPath(); if (bashOnPath) { - return { shell: bashOnPath, args: ["-c"] }; + return getBashShellConfig(bashOnPath); } return { shell: "sh", args: ["-c"] }; diff --git a/packages/coding-agent/test/auth-storage.test.ts b/packages/coding-agent/test/auth-storage.test.ts index bcc353c1..651f9a17 100644 --- a/packages/coding-agent/test/auth-storage.test.ts +++ b/packages/coding-agent/test/auth-storage.test.ts @@ -5,7 +5,8 @@ import { registerOAuthProvider } from "@earendil-works/pi-ai/oauth"; import lockfile from "proper-lockfile"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { AuthStorage } from "../src/core/auth-storage.ts"; -import { clearConfigValueCache } from "../src/core/resolve-config-value.ts"; +import { clearConfigValueCache, resolveConfigValueUncached } from "../src/core/resolve-config-value.ts"; +import * as shellModule from "../src/utils/shell.ts"; describe("AuthStorage", () => { let tempDir: string; @@ -321,6 +322,30 @@ describe("AuthStorage", () => { expect(apiKey).toBe("hello-world"); }); + test("command config uses stdin when configured shell requires it", () => { + if (process.platform === "win32") return; + const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform"); + vi.spyOn(shellModule, "getShellConfig").mockReturnValue({ + shell: "/bin/bash", + args: ["-s"], + commandTransport: "stdin", + }); + + try { + Object.defineProperty(process, "platform", { + configurable: true, + value: "win32", + }); + const nameExpansion = "$" + "{name}"; + + expect(resolveConfigValueUncached(`!name='World'; echo "Hello, ${nameExpansion}!"`)).toBe("Hello, World!"); + } finally { + if (platformDescriptor) { + Object.defineProperty(process, "platform", platformDescriptor); + } + } + }); + describe("caching", () => { test("command is only executed once per process", async () => { // Use a command that writes to a file to count invocations diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index f187da62..b06590f7 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -536,6 +536,56 @@ describe("Coding Agent Tools", () => { expect(getShellConfigSpy).toHaveBeenCalledWith("/custom/bash"); }); + it("should send commands over stdin when shell resolution requires it", async () => { + vi.spyOn(shellModule, "getShellConfig").mockReturnValue({ + shell: process.execPath, + args: [ + "-e", + 'let input = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (chunk) => { input += chunk; }); process.stdin.on("end", () => { process.stdout.write(input); });', + ], + commandTransport: "stdin", + }); + const chunks: Buffer[] = []; + const ops = createLocalBashOperations({ shellPath: "C:\\Windows\\System32\\bash.exe" }); + const nameExpansion = "$" + "{name}"; + const countExpansion = "$" + "{count}"; + const iExpansion = "$" + "{i}"; + const command = `name='World'; echo "Hello, ${nameExpansion}!"; count=3; for i in $(seq 1 ${countExpansion}); do echo "Iteration ${iExpansion} of ${countExpansion}"; done`; + + const result = await ops.exec(command, testDir, { + onData: (data) => chunks.push(data), + }); + + expect(result.exitCode).toBe(0); + expect(Buffer.concat(chunks).toString("utf-8")).toBe(command); + }); + + it("should resolve legacy WSL bash.exe to stdin command transport", () => { + if (process.platform === "win32") return; + const originalCwd = process.cwd(); + const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform"); + const shellPath = "C:\\Windows\\System32\\bash.exe"; + writeFileSync(join(testDir, shellPath), ""); + try { + process.chdir(testDir); + Object.defineProperty(process, "platform", { + configurable: true, + value: "win32", + }); + + expect(shellModule.getShellConfig(shellPath)).toEqual({ + shell: shellPath, + args: ["-s"], + commandTransport: "stdin", + }); + } finally { + process.chdir(originalCwd); + if (platformDescriptor) { + Object.defineProperty(process, "platform", platformDescriptor); + } + } + }); + it("should prepend command prefix when configured", async () => { const bashWithPrefix = createBashTool(testDir, { commandPrefix: "export TEST_VAR=hello", diff --git a/scripts/repro-5893-wsl-bash.mjs b/scripts/repro-5893-wsl-bash.mjs new file mode 100644 index 00000000..b5048de7 --- /dev/null +++ b/scripts/repro-5893-wsl-bash.mjs @@ -0,0 +1,55 @@ +#!/usr/bin/env node +import { existsSync } from "node:fs"; +import { createBashTool } from "../packages/coding-agent/src/core/tools/bash.ts"; + +const shellPath = "C:\\Windows\\System32\\bash.exe"; +const nameExpansion = "$" + "{name}"; +const countExpansion = "$" + "{count}"; +const iExpansion = "$" + "{i}"; + +function getTextOutput(result) { + return result.content + .filter((content) => content.type === "text") + .map((content) => content.text ?? "") + .join("\n"); +} + +async function runCase(label, command, expectedOutput) { + const tool = createBashTool(process.cwd(), { shellPath }); + const result = await tool.execute(label, { command }); + const output = getTextOutput(result).trimEnd(); + if (output !== expectedOutput) { + throw new Error( + [ + `${label} failed`, + "Expected:", + expectedOutput, + "Actual:", + output, + ].join("\n"), + ); + } + console.log(output); +} + +if (process.platform !== "win32") { + throw new Error("This repro must run from Windows PowerShell/CMD, not macOS/Linux or inside WSL."); +} + +if (!existsSync(shellPath)) { + throw new Error(`WSL bash launcher not found at ${shellPath}. Install/enable WSL first.`); +} + +await runCase( + "issue-5893-simple-variable", + `name='World'; echo "Hello, ${nameExpansion}!"`, + "Hello, World!", +); + +await runCase( + "issue-5893-loop-variable", + `count=3; for i in $(seq 1 ${countExpansion}); do echo "Iteration ${iExpansion} of ${countExpansion}"; done`, + "Iteration 1 of 3\nIteration 2 of 3\nIteration 3 of 3", +); + +console.log("issue #5893 WSL bash repro passed"); From 128330e36f277452f6d07ae749643ee77f4ca4e8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 19 Jun 2026 20:16:05 +0200 Subject: [PATCH 049/122] fix(coding-agent): preserve untouched lines in fuzzy edit closes #5899 --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/core/tools/edit-diff.ts | 157 +++++++++++++++--- packages/coding-agent/test/tools.test.ts | 51 ++++++ 3 files changed, 184 insertions(+), 25 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index c5aae0f9..fdc99e61 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed fuzzy `edit` matches to preserve untouched line blocks instead of rewriting the whole file through normalized content ([#5899](https://github.com/earendil-works/pi/issues/5899)). - Fixed bash commands through legacy WSL `bash.exe` to pass scripts over stdin so shell variables expand in the target bash ([#5893](https://github.com/earendil-works/pi/issues/5893)). - Fixed `/model` to hide GitHub Copilot models that are unavailable to the authenticated account ([#5897](https://github.com/earendil-works/pi/issues/5897)). - Fixed `/model` selector search to rank exact provider-prefixed matches before proxy-provider model ID matches ([#5892](https://github.com/earendil-works/pi/issues/5892)). diff --git a/packages/coding-agent/src/core/tools/edit-diff.ts b/packages/coding-agent/src/core/tools/edit-diff.ts index cc94c202..5a4d966b 100644 --- a/packages/coding-agent/src/core/tools/edit-diff.ts +++ b/packages/coding-agent/src/core/tools/edit-diff.ts @@ -53,6 +53,124 @@ export function normalizeForFuzzyMatch(text: string): string { ); } +function splitLinesWithEndings(content: string): string[] { + return content.match(/[^\n]*\n|[^\n]+/g) ?? []; +} + +interface LineSpan { + start: number; + end: number; +} + +interface MatchedEdit { + editIndex: number; + matchIndex: number; + matchLength: number; + newText: string; +} + +type TextReplacement = Pick; + +function getLineSpans(content: string): LineSpan[] { + let offset = 0; + return splitLinesWithEndings(content).map((line) => { + const span = { start: offset, end: offset + line.length }; + offset = span.end; + return span; + }); +} + +function getReplacementLineRange(lines: LineSpan[], replacement: TextReplacement) { + const replacementStart = replacement.matchIndex; + const replacementEnd = replacement.matchIndex + replacement.matchLength; + + let startLine = -1; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (replacementStart >= line.start && replacementStart < line.end) { + startLine = i; + break; + } + } + if (startLine === -1) { + throw new Error("Replacement range is outside the base content."); + } + + let endLine = startLine; + while (endLine < lines.length && lines[endLine].end < replacementEnd) { + endLine++; + } + if (endLine >= lines.length) { + throw new Error("Replacement range is outside the base content."); + } + + return { startLine, endLine: endLine + 1 }; +} + +function applyReplacements(content: string, replacements: TextReplacement[], offset = 0): string { + let result = content; + for (let i = replacements.length - 1; i >= 0; i--) { + const replacement = replacements[i]; + const matchIndex = replacement.matchIndex - offset; + result = + result.substring(0, matchIndex) + replacement.newText + result.substring(matchIndex + replacement.matchLength); + } + return result; +} + +/** + * Apply replacements matched against `baseContent` to `originalContent` while + * preserving unchanged line blocks from the original. + * + * This is useful when `baseContent` is a normalized view of the original. Each + * replacement is widened to the lines it actually touches, those touched lines + * are rewritten from the normalized base, and all other lines are copied back + * from `originalContent`. The actual replacement ranges drive preservation so + * duplicate normalized lines cannot be aligned to the wrong occurrence. + */ +export function applyReplacementsPreservingUnchangedLines( + originalContent: string, + baseContent: string, + replacements: TextReplacement[], +): string { + const originalLines = splitLinesWithEndings(originalContent); + const baseLines = getLineSpans(baseContent); + if (originalLines.length !== baseLines.length) { + throw new Error("Cannot preserve unchanged lines because the base content has a different line count."); + } + + const groups: Array<{ startLine: number; endLine: number; replacements: TextReplacement[] }> = []; + const sortedReplacements = [...replacements].sort((a, b) => a.matchIndex - b.matchIndex); + for (const replacement of sortedReplacements) { + const range = getReplacementLineRange(baseLines, replacement); + const current = groups[groups.length - 1]; + if (current && range.startLine < current.endLine) { + current.endLine = Math.max(current.endLine, range.endLine); + current.replacements.push(replacement); + continue; + } + groups.push({ ...range, replacements: [replacement] }); + } + + let originalLineIndex = 0; + let result = ""; + for (const group of groups) { + result += originalLines.slice(originalLineIndex, group.startLine).join(""); + + const groupStartOffset = baseLines[group.startLine].start; + const groupEndOffset = baseLines[group.endLine - 1].end; + result += applyReplacements( + baseContent.slice(groupStartOffset, groupEndOffset), + group.replacements, + groupStartOffset, + ); + originalLineIndex = group.endLine; + } + result += originalLines.slice(originalLineIndex).join(""); + + return result; +} + export interface FuzzyMatchResult { /** Whether a match was found */ found: boolean; @@ -74,13 +192,6 @@ export interface Edit { newText: string; } -interface MatchedEdit { - editIndex: number; - matchIndex: number; - matchLength: number; - newText: string; -} - export interface AppliedEditsResult { baseContent: string; newContent: string; @@ -120,9 +231,9 @@ export function fuzzyFindText(content: string, oldText: string): FuzzyMatchResul }; } - // When fuzzy matching, we work in the normalized space for replacement. - // This means the output will have normalized whitespace/quotes/dashes, - // which is acceptable since we're fixing minor formatting differences anyway. + // When fuzzy matching, return offsets in normalized space. Callers can use + // the normalized content to compute replacements, then decide how much of + // that normalized output should be written back. return { found: true, index: fuzzyIndex, @@ -186,8 +297,9 @@ function getNoChangeError(path: string, totalEdits: number): Error { * * All edits are matched against the same original content. Replacements are * then applied in reverse order so offsets remain stable. If any edit needs - * fuzzy matching, the operation runs in fuzzy-normalized content space to - * preserve current single-edit behavior. + * fuzzy matching, the operation runs in fuzzy-normalized content space and then + * overlays those line-level changes onto the original content so unchanged line + * blocks keep their original bytes. */ export function applyEditsToNormalizedContent( normalizedContent: string, @@ -206,19 +318,18 @@ export function applyEditsToNormalizedContent( } const initialMatches = normalizedEdits.map((edit) => fuzzyFindText(normalizedContent, edit.oldText)); - const baseContent = initialMatches.some((match) => match.usedFuzzyMatch) - ? normalizeForFuzzyMatch(normalizedContent) - : normalizedContent; + const usedFuzzyMatch = initialMatches.some((match) => match.usedFuzzyMatch); + const replacementBaseContent = usedFuzzyMatch ? normalizeForFuzzyMatch(normalizedContent) : normalizedContent; const matchedEdits: MatchedEdit[] = []; for (let i = 0; i < normalizedEdits.length; i++) { const edit = normalizedEdits[i]; - const matchResult = fuzzyFindText(baseContent, edit.oldText); + const matchResult = fuzzyFindText(replacementBaseContent, edit.oldText); if (!matchResult.found) { throw getNotFoundError(path, i, normalizedEdits.length); } - const occurrences = countOccurrences(baseContent, edit.oldText); + const occurrences = countOccurrences(replacementBaseContent, edit.oldText); if (occurrences > 1) { throw getDuplicateError(path, i, normalizedEdits.length, occurrences); } @@ -242,14 +353,10 @@ export function applyEditsToNormalizedContent( } } - let newContent = baseContent; - for (let i = matchedEdits.length - 1; i >= 0; i--) { - const edit = matchedEdits[i]; - newContent = - newContent.substring(0, edit.matchIndex) + - edit.newText + - newContent.substring(edit.matchIndex + edit.matchLength); - } + const baseContent = normalizedContent; + const newContent = usedFuzzyMatch + ? applyReplacementsPreservingUnchangedLines(normalizedContent, replacementBaseContent, matchedEdits) + : applyReplacements(replacementBaseContent, matchedEdits); if (baseContent === newContent) { throw getNoChangeError(path, normalizedEdits.length); diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index b06590f7..f8b09c90 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -1031,6 +1031,57 @@ describe("edit tool fuzzy matching", () => { expect(readFileSync(testFile, "utf-8")).toBe("console.log('world');\nhello universe\n"); }); + + it("should preserve the correct occurrence when fuzzy replacement equals a nearby line", async () => { + const testFile = join(testDir, "fuzzy-preserve-duplicate-line.txt"); + const originalContent = ["replace me\u0020\u0020\u0020", "after\u0020\u0020\u0020", ""].join("\n"); + writeFileSync(testFile, originalContent); + + const result = await editTool.execute("test-fuzzy-preserve-duplicate-line", { + path: testFile, + edits: [{ oldText: "replace me\n", newText: "after\n" }], + }); + + const expectedContent = ["after", "after\u0020\u0020\u0020", ""].join("\n"); + expect(readFileSync(testFile, "utf-8")).toBe(expectedContent); + expect(applyPatch(originalContent, result.details?.patch ?? "")).toBe(expectedContent); + }); + + it("should preserve untouched lines and produce an applicable patch for fuzzy multi-edits", async () => { + const testFile = join(testDir, "fuzzy-preserve-multi.txt"); + const originalContent = [ + "keep before\u0020\u0020", + "first target\u0020\u0020", + "first after", + "keep middle\u0020\u0020\u0020", + "second target\u0020\u0020", + "second after", + "keep after\u0020\u0020", + "", + ].join("\n"); + writeFileSync(testFile, originalContent); + + const result = await editTool.execute("test-fuzzy-preserve-multi", { + path: testFile, + edits: [ + { oldText: "first target\nfirst after", newText: "FIRST\nFIRST2" }, + { oldText: "second target\nsecond after", newText: "SECOND\nSECOND2" }, + ], + }); + + const expectedContent = [ + "keep before\u0020\u0020", + "FIRST", + "FIRST2", + "keep middle\u0020\u0020\u0020", + "SECOND", + "SECOND2", + "keep after\u0020\u0020", + "", + ].join("\n"); + expect(readFileSync(testFile, "utf-8")).toBe(expectedContent); + expect(applyPatch(originalContent, result.details?.patch ?? "")).toBe(expectedContent); + }); }); describe("edit tool CRLF handling", () => { From 8b97e75c6b149fdd4dec95fe3321d1e94fd5c1d4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 19 Jun 2026 23:34:17 +0200 Subject: [PATCH 050/122] feat(ai): add chat-template thinking compat closes #5673 --- packages/ai/CHANGELOG.md | 4 + packages/ai/README.md | 3 +- .../ai/src/providers/openai-completions.ts | 48 ++++++++ packages/ai/src/types.ts | 14 ++- ...penai-completions-thinking-as-text.test.ts | 1 + .../openai-completions-tool-choice.test.ts | 114 +++++++++++++++++- ...nai-completions-tool-result-images.test.ts | 1 + packages/coding-agent/CHANGELOG.md | 4 + packages/coding-agent/docs/custom-provider.md | 7 +- packages/coding-agent/docs/models.md | 5 +- .../coding-agent/src/core/model-registry.ts | 18 +++ .../coding-agent/test/model-registry.test.ts | 37 ++++++ 12 files changed, 248 insertions(+), 8 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 4f316c3b..dc1a931b 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added configurable `chat-template` thinking support for OpenAI-compatible providers that use `chat_template_kwargs`, such as DeepSeek models behind vLLM ([#5673](https://github.com/earendil-works/pi/issues/5673)). + ### Fixed - Fixed GitHub Copilot OAuth model availability to use the authenticated account's model picker catalog ([#5897](https://github.com/earendil-works/pi/issues/5897)). diff --git a/packages/ai/README.md b/packages/ai/README.md index 8137ccd4..bec437aa 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -967,7 +967,8 @@ interface OpenAICompletionsCompat { requiresAssistantAfterToolResult?: boolean; // Whether tool results must be followed by an assistant message (default: false) requiresThinkingAsText?: boolean; // Whether thinking blocks must be converted to text (default: false) requiresReasoningContentOnAssistantMessages?: boolean; // Whether all replayed assistant messages must include empty reasoning_content when reasoning is enabled (default: auto-detected for DeepSeek) - thinkingFormat?: 'openai' | 'openrouter' | 'deepseek' | 'together' | 'zai' | 'qwen' | 'qwen-chat-template' | 'string-thinking' | 'ant-ling'; // Format for reasoning param: 'openai' uses reasoning_effort, 'openrouter' uses reasoning: { effort }, 'deepseek' uses thinking: { type } plus reasoning_effort when supported, 'together' uses reasoning: { enabled } plus reasoning_effort when supported, 'zai' uses enable_thinking, 'qwen' uses enable_thinking, 'qwen-chat-template' uses chat_template_kwargs.enable_thinking, 'string-thinking' uses top-level thinking, 'ant-ling' uses reasoning: { effort } only for mapped efforts (default: openai) + thinkingFormat?: 'openai' | 'openrouter' | 'deepseek' | 'together' | 'zai' | 'qwen' | 'chat-template' | 'qwen-chat-template' | 'string-thinking' | 'ant-ling'; // Format for reasoning param: 'openai' uses reasoning_effort, 'openrouter' uses reasoning: { effort }, 'deepseek' uses thinking: { type } plus reasoning_effort when supported, 'together' uses reasoning: { enabled } plus reasoning_effort when supported, 'zai' uses thinking: { type }, 'qwen' uses enable_thinking, 'chat-template' uses configurable chat_template_kwargs, 'qwen-chat-template' uses chat_template_kwargs.enable_thinking and preserve_thinking, 'string-thinking' uses top-level thinking, 'ant-ling' uses reasoning: { effort } only for mapped efforts (default: openai) + chatTemplateKwargs?: Record; // chat_template_kwargs values; use $var for pi-controlled thinking values cacheControlFormat?: 'anthropic'; // Anthropic-style cache_control on system prompt, last tool, and last user/assistant text content openRouterRouting?: OpenRouterRouting; // OpenRouter routing preferences (default: {}) vercelGatewayRouting?: VercelGatewayRouting; // Vercel AI Gateway routing preferences (default: {}) diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 2f5edb62..10c8e7e6 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -15,6 +15,7 @@ import { calculateCost, clampThinkingLevel } from "../models.ts"; import type { AssistantMessage, CacheRetention, + ChatTemplateKwargValue, Context, ImageContent, Message, @@ -91,6 +92,8 @@ type ResolvedOpenAICompletionsCompat = Omit, " cacheControlFormat?: OpenAICompletionsCompat["cacheControlFormat"]; }; +type ResolvedChatTemplateKwargValue = string | number | boolean | null; + type ChatCompletionInstructionMessageParam = ChatCompletionDeveloperMessageParam | ChatCompletionSystemMessageParam; type ChatCompletionTextPartWithCacheControl = ChatCompletionContentPartText & { @@ -585,6 +588,11 @@ function buildParams( enable_thinking: !!options?.reasoningEffort, preserve_thinking: true, }; + } else if (compat.thinkingFormat === "chat-template" && model.reasoning) { + const chatTemplateKwargs = buildChatTemplateKwargs(model, options, compat); + if (chatTemplateKwargs) { + (params as any).chat_template_kwargs = chatTemplateKwargs; + } } else if (compat.thinkingFormat === "deepseek" && model.reasoning) { if (options?.reasoningEffort) { (params as any).thinking = { type: "enabled" }; @@ -655,6 +663,44 @@ function buildParams( return params; } +function buildChatTemplateKwargs( + model: Model<"openai-completions">, + options: OpenAICompletionsOptions | undefined, + compat: ResolvedOpenAICompletionsCompat, +): Record | undefined { + const kwargs: Record = {}; + + for (const [key, value] of Object.entries(compat.chatTemplateKwargs)) { + const resolved = resolveChatTemplateKwargValue(model, options, value); + if (resolved !== undefined) { + kwargs[key] = resolved; + } + } + + return Object.keys(kwargs).length > 0 ? kwargs : undefined; +} + +function resolveChatTemplateKwargValue( + model: Model<"openai-completions">, + options: OpenAICompletionsOptions | undefined, + value: ChatTemplateKwargValue, +): ResolvedChatTemplateKwargValue | undefined { + if (typeof value !== "object" || value === null) { + return value; + } + + const reasoningEffort = options?.reasoningEffort; + if (!reasoningEffort && value.omitWhenOff) { + return undefined; + } + if (value.$var === "thinking.enabled") { + return !!reasoningEffort; + } + + const mappedValue = reasoningEffort ? model.thinkingLevelMap?.[reasoningEffort] : model.thinkingLevelMap?.off; + return mappedValue === undefined ? reasoningEffort : typeof mappedValue === "string" ? mappedValue : undefined; +} + function getCompatCacheControl( compat: ResolvedOpenAICompletionsCompat, cacheRetention: CacheRetention, @@ -1167,6 +1213,7 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet : "openai", openRouterRouting: {}, vercelGatewayRouting: {}, + chatTemplateKwargs: {}, zaiToolStream: false, supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia, cacheControlFormat, @@ -1205,6 +1252,7 @@ function getCompat(model: Model<"openai-completions">): ResolvedOpenAICompletion thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat, openRouterRouting: model.compat.openRouterRouting ?? {}, vercelGatewayRouting: model.compat.vercelGatewayRouting ?? detected.vercelGatewayRouting, + chatTemplateKwargs: model.compat.chatTemplateKwargs ?? detected.chatTemplateKwargs, zaiToolStream: model.compat.zaiToolStream ?? detected.zaiToolStream, supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode, cacheControlFormat: model.compat.cacheControlFormat ?? detected.cacheControlFormat, diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index cedac6e8..54f44685 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -65,6 +65,15 @@ export type ImagesProvider = KnownImagesProvider | string; export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh"; export type ModelThinkingLevel = "off" | ThinkingLevel; export type ThinkingLevelMap = Partial>; +export type ChatTemplateKwargValue = + | string + | number + | boolean + | null + | { + $var: "thinking.enabled" | "thinking.effort"; + omitWhenOff?: boolean; + }; /** Token budgets for each thinking level (token-based providers only) */ export interface ThinkingBudgets { @@ -403,7 +412,7 @@ export interface OpenAICompletionsCompat { requiresThinkingAsText?: boolean; /** Whether all replayed assistant messages must include an empty reasoning_content field when reasoning is enabled. Default: auto-detected from URL. */ requiresReasoningContentOnAssistantMessages?: boolean; - /** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "deepseek" uses thinking: { type } plus reasoning_effort when supported, "together" uses reasoning: { enabled } plus reasoning_effort when supported, "zai" uses thinking: { type }, "qwen" uses top-level enable_thinking: boolean, "qwen-chat-template" uses chat_template_kwargs.enable_thinking, "string-thinking" uses top-level thinking: string, and "ant-ling" uses reasoning: { effort } only when the mapped effort is non-null. Default: "openai". */ + /** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "deepseek" uses thinking: { type } plus reasoning_effort when supported, "together" uses reasoning: { enabled } plus reasoning_effort when supported, "zai" uses thinking: { type }, "qwen" uses top-level enable_thinking: boolean, "qwen-chat-template" uses chat_template_kwargs.enable_thinking and preserve_thinking, "chat-template" uses configurable chat_template_kwargs, "string-thinking" uses top-level thinking: string, and "ant-ling" uses reasoning: { effort } only when the mapped effort is non-null. Default: "openai". */ thinkingFormat?: | "openai" | "openrouter" @@ -411,9 +420,12 @@ export interface OpenAICompletionsCompat { | "together" | "zai" | "qwen" + | "chat-template" | "qwen-chat-template" | "string-thinking" | "ant-ling"; + /** Kwargs to send as `chat_template_kwargs` when `thinkingFormat` is `chat-template`. Use `{ "$var": "thinking.enabled" }` or `{ "$var": "thinking.effort" }` for pi-controlled thinking values. */ + chatTemplateKwargs?: Record; /** OpenRouter-compatible routing preferences sent as the `provider` request field. */ openRouterRouting?: OpenRouterRouting; /** Vercel AI Gateway routing preferences. Only used when baseUrl points to Vercel AI Gateway. */ diff --git a/packages/ai/test/openai-completions-thinking-as-text.test.ts b/packages/ai/test/openai-completions-thinking-as-text.test.ts index 1c49aad3..138eb3e3 100644 --- a/packages/ai/test/openai-completions-thinking-as-text.test.ts +++ b/packages/ai/test/openai-completions-thinking-as-text.test.ts @@ -34,6 +34,7 @@ const compat = { thinkingFormat: "openai", openRouterRouting: {}, vercelGatewayRouting: {}, + chatTemplateKwargs: {}, zaiToolStream: false, supportsStrictMode: true, cacheControlFormat: undefined, diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index 373b1b78..41601281 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { stream, streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; import { convertMessages } from "../src/providers/openai-completions.ts"; -import type { AssistantMessage, Model, Tool, ToolResultMessage } from "../src/types.ts"; +import type { AssistantMessage, Model, SimpleStreamOptions, Tool, ToolResultMessage } from "../src/types.ts"; const mockState = vi.hoisted(() => ({ lastParams: undefined as unknown, @@ -64,6 +64,46 @@ vi.mock("openai", () => { return { default: FakeOpenAI }; }); +const localOpenAICompletionsModel = { + api: "openai-completions", + provider: "local-vllm", + baseUrl: "http://localhost:8000/v1", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128000, + maxTokens: 8192, +} satisfies Omit, "id" | "name" | "compat">; + +type CapturedParams = { + chat_template_kwargs?: Record; + thinking?: unknown; + reasoning_effort?: string; +}; + +async function captureSimpleParams( + model: Model<"openai-completions">, + reasoning?: SimpleStreamOptions["reasoning"], +): Promise { + let payload: unknown; + + await streamSimple( + model, + { + messages: [{ role: "user", content: "Hi", timestamp: Date.now() }], + }, + { + apiKey: "test", + reasoning, + onPayload: (params: unknown) => { + payload = params; + }, + }, + ).result(); + + return (payload ?? mockState.lastParams) as CapturedParams; +} + describe("openai-completions tool_choice", () => { beforeEach(() => { mockState.lastParams = undefined; @@ -1142,6 +1182,7 @@ describe("openai-completions tool_choice", () => { thinkingFormat: "openai", openRouterRouting: {}, vercelGatewayRouting: {}, + chatTemplateKwargs: {}, zaiToolStream: false, supportsStrictMode: true, sendSessionAffinityHeaders: false, @@ -1449,6 +1490,77 @@ describe("openai-completions tool_choice", () => { expect(params.reasoning_effort).toBeUndefined(); }); + it("uses configurable chat template boolean thinking kwargs", async () => { + const model = { + ...localOpenAICompletionsModel, + id: "deepseek-ai/DeepSeek-V3.1", + name: "DeepSeek V3.1 via vLLM", + compat: { + thinkingFormat: "chat-template", + supportsReasoningEffort: false, + chatTemplateKwargs: { thinking: { $var: "thinking.enabled" } }, + }, + } satisfies Model<"openai-completions">; + + for (const testCase of [ + { reasoning: "high" as const, expected: true }, + { reasoning: undefined, expected: false }, + ]) { + const params = await captureSimpleParams(model, testCase.reasoning); + + expect(params.chat_template_kwargs).toEqual({ thinking: testCase.expected }); + expect(params.thinking).toBeUndefined(); + expect(params.reasoning_effort).toBeUndefined(); + } + }); + + it("uses qwen chat template thinking kwargs", async () => { + const model = { + ...localOpenAICompletionsModel, + id: "Qwen/Qwen3-Coder", + name: "Qwen3 Coder via vLLM", + compat: { + thinkingFormat: "qwen-chat-template", + supportsReasoningEffort: false, + }, + } satisfies Model<"openai-completions">; + + for (const testCase of [ + { reasoning: "high" as const, expected: true }, + { reasoning: undefined, expected: false }, + ]) { + const params = await captureSimpleParams(model, testCase.reasoning); + + expect(params.chat_template_kwargs).toEqual({ + enable_thinking: testCase.expected, + preserve_thinking: true, + }); + expect(params.reasoning_effort).toBeUndefined(); + } + }); + + it("uses configurable chat template effort kwargs with static kwargs", async () => { + const model = { + ...localOpenAICompletionsModel, + id: "unsloth/gpt-oss-120b-GGUF", + name: "GPT OSS via vLLM", + thinkingLevelMap: { xhigh: "max" }, + compat: { + thinkingFormat: "chat-template", + supportsReasoningEffort: false, + chatTemplateKwargs: { + preserve_thinking: true, + reasoning_effort: { $var: "thinking.effort", omitWhenOff: true }, + }, + }, + } satisfies Model<"openai-completions">; + + const params = await captureSimpleParams(model, "xhigh"); + + expect(params.chat_template_kwargs).toEqual({ preserve_thinking: true, reasoning_effort: "max" }); + expect(params.reasoning_effort).toBeUndefined(); + }); + it("uses Ant Ling compatibility metadata", async () => { const model = getModel("ant-ling", "Ring-2.6-1T")!; let payload: unknown; diff --git a/packages/ai/test/openai-completions-tool-result-images.test.ts b/packages/ai/test/openai-completions-tool-result-images.test.ts index 3510f396..4458e51f 100644 --- a/packages/ai/test/openai-completions-tool-result-images.test.ts +++ b/packages/ai/test/openai-completions-tool-result-images.test.ts @@ -32,6 +32,7 @@ const compat: Required = { thinkingFormat: "openai", openRouterRouting: {}, vercelGatewayRouting: {}, + chatTemplateKwargs: {}, zaiToolStream: false, supportsStrictMode: true, cacheControlFormat: "anthropic", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index fdc99e61..8813170b 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added inherited configurable `chat-template` thinking support for OpenAI-compatible providers that use `chat_template_kwargs`, such as DeepSeek models behind vLLM ([#5673](https://github.com/earendil-works/pi/issues/5673)). + ### Fixed - Fixed fuzzy `edit` matches to preserve untouched line blocks instead of rewriting the whole file through normalized content ([#5899](https://github.com/earendil-works/pi/issues/5899)). diff --git a/packages/coding-agent/docs/custom-provider.md b/packages/coding-agent/docs/custom-provider.md index c2bf4455..612d1e60 100644 --- a/packages/coding-agent/docs/custom-provider.md +++ b/packages/coding-agent/docs/custom-provider.md @@ -229,7 +229,7 @@ models: [{ }] ``` -Use `openrouter` for OpenRouter-style `reasoning: { effort }` controls. Use `together` for Together-style `reasoning: { enabled }` controls; with `supportsReasoningEffort`, it also sends `reasoning_effort`. Use `qwen-chat-template` instead for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking`. +Use `openrouter` for OpenRouter-style `reasoning: { effort }` controls. Use `together` for Together-style `reasoning: { enabled }` controls; with `supportsReasoningEffort`, it also sends `reasoning_effort`. Use `qwen-chat-template` for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking` and need `preserve_thinking`. Use `cacheControlFormat: "anthropic"` for OpenAI-compatible providers that expose Anthropic-style prompt caching via `cache_control` on the system prompt, last tool definition, and last user/assistant text content. For Anthropic-compatible providers using `api: "anthropic-messages"`, set `compat.forceAdaptiveThinking: true` on models or providers whose upstream model requires adaptive thinking (`thinking.type: "adaptive"` plus `output_config.effort`). Built-in adaptive Claude models set this automatically. Set `compat.allowEmptySignature: true` only for providers that emit empty thinking signatures and expect `signature: ""` on replay. @@ -718,7 +718,8 @@ interface ProviderModelConfig { requiresAssistantAfterToolResult?: boolean; requiresThinkingAsText?: boolean; requiresReasoningContentOnAssistantMessages?: boolean; - thinkingFormat?: "openai" | "openrouter" | "deepseek" | "together" | "zai" | "qwen" | "qwen-chat-template"; + thinkingFormat?: "openai" | "openrouter" | "deepseek" | "together" | "zai" | "qwen" | "chat-template" | "qwen-chat-template" | "string-thinking" | "ant-ling"; + chatTemplateKwargs?: Record; cacheControlFormat?: "anthropic"; // anthropic-messages @@ -732,5 +733,5 @@ interface ProviderModelConfig { } ``` -`openrouter` sends `reasoning: { effort }`. `deepseek` sends `thinking: { type: "enabled" | "disabled" }` and `reasoning_effort` when enabled. `together` sends `reasoning: { enabled }` and also `reasoning_effort` when `supportsReasoningEffort` is enabled. `qwen` is for DashScope-style top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking`. +`openrouter` sends `reasoning: { effort }`. `deepseek` sends `thinking: { type: "enabled" | "disabled" }` and `reasoning_effort` when enabled. `together` sends `reasoning: { enabled }` and also `reasoning_effort` when `supportsReasoningEffort` is enabled. `qwen` is for DashScope-style top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking` and need `preserve_thinking`. Use `chat-template` for configurable `chat_template_kwargs`, for example DeepSeek V3.x behind vLLM with `chatTemplateKwargs: { "thinking": { "$var": "thinking.enabled" } }`. `cacheControlFormat: "anthropic"` applies Anthropic-style `cache_control` markers to the system prompt, last tool definition, and last user/assistant text content. diff --git a/packages/coding-agent/docs/models.md b/packages/coding-agent/docs/models.md index 830eaacd..5679981a 100644 --- a/packages/coding-agent/docs/models.md +++ b/packages/coding-agent/docs/models.md @@ -399,14 +399,15 @@ For providers with partial OpenAI compatibility, use the `compat` field. | `requiresAssistantAfterToolResult` | Insert an assistant message before a user message after tool results | | `requiresThinkingAsText` | Convert thinking blocks to plain text | | `requiresReasoningContentOnAssistantMessages` | Include empty `reasoning_content` on all replayed assistant messages when reasoning is enabled | -| `thinkingFormat` | Use `reasoning_effort`, `openrouter`, `deepseek`, `together`, `zai`, `qwen`, or `qwen-chat-template` thinking parameters | +| `thinkingFormat` | Use `reasoning_effort`, `openrouter`, `deepseek`, `together`, `zai`, `qwen`, `chat-template`, or `qwen-chat-template` thinking parameters | +| `chatTemplateKwargs` | `chat_template_kwargs` values for `thinkingFormat: "chat-template"`; use `{ "$var": "thinking.enabled" }` or `{ "$var": "thinking.effort" }` for pi-controlled thinking values | | `cacheControlFormat` | Use Anthropic-style `cache_control` markers on the system prompt, last tool definition, and last user/assistant text content. Currently only `anthropic` is supported. | | `supportsStrictMode` | Include the `strict` field in tool definitions | | `supportsLongCacheRetention` | Whether the provider accepts long cache retention when cache retention is `long`: `prompt_cache_retention: "24h"` for OpenAI prompt caching, or `cache_control.ttl: "1h"` when `cacheControlFormat` is `anthropic`. Default: `true`. | | `openRouterRouting` | OpenRouter provider routing preferences. This object is sent as-is in the `provider` field of the [OpenRouter API request](https://openrouter.ai/docs/guides/routing/provider-selection). | | `vercelGatewayRouting` | Vercel AI Gateway routing config for provider selection (`only`, `order`) | -`openrouter` uses `reasoning: { effort }`. `together` uses `reasoning: { enabled }` and also `reasoning_effort` when `supportsReasoningEffort` is enabled. `qwen` uses top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that require `chat_template_kwargs.enable_thinking`. +`openrouter` uses `reasoning: { effort }`. `together` uses `reasoning: { enabled }` and also `reasoning_effort` when `supportsReasoningEffort` is enabled. `qwen` uses top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that require `chat_template_kwargs.enable_thinking` and `preserve_thinking`. Use `chat-template` for vLLM/Hugging Face chat templates that need configurable `chat_template_kwargs`, such as `chatTemplateKwargs: { "thinking": { "$var": "thinking.enabled" } }` for DeepSeek V3.x templates. `cacheControlFormat: "anthropic"` is for OpenAI-compatible providers that expose Anthropic-style prompt caching through `cache_control` markers on text content and tool definitions. diff --git a/packages/coding-agent/src/core/model-registry.ts b/packages/coding-agent/src/core/model-registry.ts index 60f9001f..305dca92 100644 --- a/packages/coding-agent/src/core/model-registry.ts +++ b/packages/coding-agent/src/core/model-registry.ts @@ -96,6 +96,13 @@ const ThinkingLevelMapSchema = Type.Object({ xhigh: Type.Optional(ThinkingLevelMapValueSchema), }); +const ChatTemplateKwargScalarSchema = Type.Union([Type.String(), Type.Number(), Type.Boolean(), Type.Null()]); +const ChatTemplateKwargVariableSchema = Type.Object({ + $var: Type.Union([Type.Literal("thinking.enabled"), Type.Literal("thinking.effort")]), + omitWhenOff: Type.Optional(Type.Boolean()), +}); +const ChatTemplateKwargSchema = Type.Union([ChatTemplateKwargScalarSchema, ChatTemplateKwargVariableSchema]); + const OpenAICompletionsCompatSchema = Type.Object({ supportsStore: Type.Optional(Type.Boolean()), supportsDeveloperRole: Type.Optional(Type.Boolean()), @@ -114,9 +121,13 @@ const OpenAICompletionsCompatSchema = Type.Object({ Type.Literal("deepseek"), Type.Literal("zai"), Type.Literal("qwen"), + Type.Literal("chat-template"), Type.Literal("qwen-chat-template"), + Type.Literal("string-thinking"), + Type.Literal("ant-ling"), ]), ), + chatTemplateKwargs: Type.Optional(Type.Record(Type.String(), ChatTemplateKwargSchema)), cacheControlFormat: Type.Optional(Type.Literal("anthropic")), openRouterRouting: Type.Optional(OpenRouterRoutingSchema), vercelGatewayRouting: Type.Optional(VercelGatewayRoutingSchema), @@ -289,6 +300,13 @@ function mergeCompat( }; } + if (baseCompletions?.chatTemplateKwargs || overrideCompletions.chatTemplateKwargs) { + mergedCompletions.chatTemplateKwargs = { + ...baseCompletions?.chatTemplateKwargs, + ...overrideCompletions.chatTemplateKwargs, + }; + } + return merged as Model["compat"]; } diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index 1d720628..1479808a 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -434,6 +434,43 @@ describe("ModelRegistry", () => { expect(compat?.cacheControlFormat).toBe("anthropic"); }); + test("compat schema accepts chat template thinking configuration", () => { + writeRawModelsJson({ + demo: { + baseUrl: "https://example.com/v1", + apiKey: "DEMO_KEY", + api: "openai-completions", + models: [ + { + id: "demo-model", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1000, + maxTokens: 100, + compat: { + thinkingFormat: "chat-template", + chatTemplateKwargs: { + preserve_thinking: true, + thinking: { $var: "thinking.enabled" }, + }, + }, + }, + ], + }, + }); + + const registry = ModelRegistry.create(authStorage, modelsJsonPath); + const compat = registry.find("demo", "demo-model")?.compat as OpenAICompletionsCompat | undefined; + + expect(registry.getError()).toBeUndefined(); + expect(compat?.thinkingFormat).toBe("chat-template"); + expect(compat?.chatTemplateKwargs).toEqual({ + preserve_thinking: true, + thinking: { $var: "thinking.enabled" }, + }); + }); + test("compat schema accepts Anthropic eager tool input streaming flag", () => { writeRawModelsJson({ demo: { From 3095977d13961e3fb64770c53ca4f9f9f8baf7df Mon Sep 17 00:00:00 2001 From: Alexey Zaytsev Date: Sat, 20 Jun 2026 07:58:30 -0500 Subject: [PATCH 051/122] fix(tui): stabilize streaming code fence rendering (#5846) --- packages/tui/src/components/markdown.ts | 26 +++++++++++++++ packages/tui/test/markdown.test.ts | 43 +++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/packages/tui/src/components/markdown.ts b/packages/tui/src/components/markdown.ts index 83f15aa5..1034cb47 100644 --- a/packages/tui/src/components/markdown.ts +++ b/packages/tui/src/components/markdown.ts @@ -22,6 +22,31 @@ class StrictStrikethroughTokenizer extends Tokenizer { } } +function trimPartialClosingFences(tokens: readonly Token[]): void { + const token = tokens[tokens.length - 1]; + if (token?.type === "list") { + trimPartialClosingFences(token.items[token.items.length - 1]?.tokens ?? []); + return; + } + if (token?.type === "blockquote") { + trimPartialClosingFences(token.tokens ?? []); + return; + } + if (token?.type !== "code") { + return; + } + + // Trim streamed partial closing fences so code blocks do not shrink/flicker + // when the final fence character arrives. See https://github.com/earendil-works/pi/issues/5825. + const marker = /^(`{3,}|~{3,})/.exec(token.raw)?.[1]; + const lastLine = token.raw.split("\n").pop(); + if (!marker || !lastLine || lastLine.length >= marker.length || lastLine !== marker[0]?.repeat(lastLine.length)) { + return; + } + + token.text = token.text.slice(0, -lastLine.length).replace(/\n$/, ""); +} + const markdownParser = new Marked(); markdownParser.setOptions({ tokenizer: new StrictStrikethroughTokenizer(), @@ -145,6 +170,7 @@ export class Markdown implements Component { // Parse markdown to HTML-like tokens const tokens = markdownParser.lexer(normalizedText); + trimPartialClosingFences(tokens); // Convert tokens to styled terminal output const renderedLines: string[] = []; diff --git a/packages/tui/test/markdown.test.ts b/packages/tui/test/markdown.test.ts index dc595b11..47bc0a81 100644 --- a/packages/tui/test/markdown.test.ts +++ b/packages/tui/test/markdown.test.ts @@ -1376,4 +1376,47 @@ bar`, ); }); }); + + describe("Streaming code fences", () => { + it("stabilizes partial closing fence rendering", () => { + const cases = [ + { + input: "```ts\nconst x = 1;\n``", + expected: ["```ts", " const x = 1;", "```"], + }, + { + input: "```md\nnot a closing fence:\n``\n```", + expected: ["```md", " not a closing fence:", " ``", "```"], + }, + { + input: "```ts\n``", + expected: ["```ts", "", "```"], + }, + { + input: "````\n```", + expected: ["```", "", "```"], + }, + { + input: "~~~~~\n~~~~", + expected: ["```", "", "```"], + }, + { + input: "```md\nnot a closing fence:\n``\n```\n\nafter", + expected: ["```md", " not a closing fence:", " ``", "```", "", "after"], + }, + ]; + + for (const { input, expected } of cases) { + const markdown = new Markdown(input, 0, 0, defaultMarkdownTheme); + const lines = markdown.render(80).map((line) => stripAnsi(line).trimEnd()); + + assert.deepStrictEqual(lines, expected); + } + + const partial = new Markdown("```ts\nconst x = 1;\n``", 0, 0, defaultMarkdownTheme); + const complete = new Markdown("```ts\nconst x = 1;\n```", 0, 0, defaultMarkdownTheme); + + assert.strictEqual(partial.render(80).length, complete.render(80).length); + }); + }); }); From 416c673dfe7f2c1fea051a10a74affa1a84f7ad1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 20 Jun 2026 16:13:13 +0200 Subject: [PATCH 052/122] fix: skip no-action for to-discuss issues --- .github/workflows/issue-triage-labels.yml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/workflows/issue-triage-labels.yml b/.github/workflows/issue-triage-labels.yml index 4a9ea9d7..cf17c39e 100644 --- a/.github/workflows/issue-triage-labels.yml +++ b/.github/workflows/issue-triage-labels.yml @@ -17,6 +17,7 @@ jobs: const UNTRIAGED_LABEL = 'untriaged'; const NO_ACTION_LABEL = 'no-action'; const LAST_READ_LABEL = 'last-read'; + const TO_DISCUSS_LABEL = 'to-discuss'; function issueHasLabel(issue, labelName) { return (issue.labels ?? []).some((label) => label.name === labelName); @@ -102,13 +103,17 @@ jobs: } for (const issue of issuesToMark) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - labels: [NO_ACTION_LABEL], - }); - console.log(`Added ${NO_ACTION_LABEL} to #${issue.number}`); + if (issueHasLabel(issue, TO_DISCUSS_LABEL)) { + console.log(`Skipped ${NO_ACTION_LABEL} for #${issue.number} because it has ${TO_DISCUSS_LABEL}`); + } else { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: [NO_ACTION_LABEL], + }); + console.log(`Added ${NO_ACTION_LABEL} to #${issue.number}`); + } await github.rest.issues.update({ owner: context.repo.owner, From 8597ebafd9f8930fc8e7251946999e21274694d4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 20 Jun 2026 18:47:42 +0200 Subject: [PATCH 053/122] fix(ai): expose OpenRouter GLM-5.2 xhigh effort closes #5770 --- packages/ai/CHANGELOG.md | 1 + packages/ai/scripts/generate-models.ts | 3 + packages/ai/src/models.generated.ts | 79 +++++++------------------- 3 files changed, 26 insertions(+), 57 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index dc1a931b..567a2273 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed OpenRouter GLM-5.2 metadata to expose `xhigh` reasoning and send OpenRouter's native `xhigh` effort ([#5770](https://github.com/earendil-works/pi/issues/5770)). - Fixed GitHub Copilot OAuth model availability to use the authenticated account's model picker catalog ([#5897](https://github.com/earendil-works/pi/issues/5897)). ## [0.79.8] - 2026-06-19 diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index c1fddc92..ad9ca95b 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -374,6 +374,9 @@ function applyThinkingLevelMetadata(model: Model): void { // Pi's low/medium/high pass through verbatim; OpenRouter normalizes to Mercury's vocabulary. mergeThinkingLevelMap(model, { off: null }); } + if (model.provider === "openrouter" && model.id === "z-ai/glm-5.2") { + mergeThinkingLevelMap(model, { xhigh: "xhigh" }); + } if (model.provider === "opencode-go" && model.id === "kimi-k2.6") { // OpenCode Go exposes Kimi K2.6 thinking as on/off, not distinct effort tiers. mergeThinkingLevelMap(model, { minimal: null, low: null, medium: null }); diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 8639bf13..497f766a 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -4851,42 +4851,6 @@ export const MODELS = { contextWindow: 262144, maxTokens: 32768, } satisfies Model<"google-generative-ai">, - "gemma-4-E2B-it": { - id: "gemma-4-E2B-it", - name: "Gemma 4 E2B IT", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"MINIMAL","low":null,"medium":null,"high":"HIGH"}, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"google-generative-ai">, - "gemma-4-E4B-it": { - id: "gemma-4-E4B-it", - name: "Gemma 4 E4B IT", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"MINIMAL","low":null,"medium":null,"high":"HIGH"}, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"google-generative-ai">, }, "google-vertex": { "gemini-2.5-flash": { @@ -9190,13 +9154,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.22, - output: 0.85, + input: 0.25, + output: 0.8, cacheRead: 0.06, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262144, + maxTokens: 80000, } satisfies Model<"openai-completions">, "arcee-ai/trinity-mini": { id: "arcee-ai/trinity-mini", @@ -10573,13 +10537,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.67, + input: 0.66, output: 3.5, - cacheRead: 0.2, + cacheRead: 0.33, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262144, + maxTokens: 262142, } satisfies Model<"openai-completions">, "moonshotai/kimi-k2.7-code": { id: "moonshotai/kimi-k2.7-code", @@ -10590,13 +10554,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.74, - output: 3.5, - cacheRead: 0.15, + input: 0.612, + output: 3.069, + cacheRead: 0.1296, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 16384, + maxTokens: 262144, } satisfies Model<"openai-completions">, "nex-agi/nex-n2-pro:free": { id: "nex-agi/nex-n2-pro:free", @@ -12761,13 +12725,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.066, - output: 0.26, - cacheRead: 0.029, + input: 0.063, + output: 0.21, + cacheRead: 0.021, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262144, + maxTokens: 4096, } satisfies Model<"openai-completions">, "thedrummer/rocinante-12b": { id: "thedrummer/rocinante-12b", @@ -13069,11 +13033,11 @@ export const MODELS = { cost: { input: 0.98, output: 3.08, - cacheRead: 0.182, + cacheRead: 0.49, cacheWrite: 0, }, contextWindow: 202752, - maxTokens: 4096, + maxTokens: 65535, } satisfies Model<"openai-completions">, "z-ai/glm-5.2": { id: "z-ai/glm-5.2", @@ -13082,6 +13046,7 @@ export const MODELS = { provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text"], cost: { input: 1.2, @@ -13203,13 +13168,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.67, + input: 0.66, output: 3.5, - cacheRead: 0.2, + cacheRead: 0.33, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262144, + maxTokens: 262142, } satisfies Model<"openai-completions">, "~openai/gpt-latest": { id: "~openai/gpt-latest", @@ -15981,8 +15946,8 @@ export const MODELS = { cost: { input: 0.09, output: 0.3, - cacheRead: 0, - cacheWrite: 0.02, + cacheRead: 0.02, + cacheWrite: 0, }, contextWindow: 262114, maxTokens: 262114, From a1da88aed4a1584f4ad4e58bba2e3391760785a7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 20 Jun 2026 20:22:53 +0200 Subject: [PATCH 054/122] fix(coding-agent): make session path traversal linear Avoid quadratic path construction when walking deep session branches. On a 600k-entry pathological session, path construction improved from about 20.3s with Array.unshift() to about 35ms with push() plus reverse(). Refs #5804. --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/core/session-manager.ts | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 8813170b..1e2a54d6 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed deep session branches taking quadratic time to build context or branch paths ([#5909](https://github.com/earendil-works/pi/issues/5909)). - Fixed fuzzy `edit` matches to preserve untouched line blocks instead of rewriting the whole file through normalized content ([#5899](https://github.com/earendil-works/pi/issues/5899)). - Fixed bash commands through legacy WSL `bash.exe` to pass scripts over stdin so shell variables expand in the target bash ([#5893](https://github.com/earendil-works/pi/issues/5893)). - Fixed `/model` to hide GitHub Copilot models that are unavailable to the authenticated account ([#5897](https://github.com/earendil-works/pi/issues/5897)). diff --git a/packages/coding-agent/src/core/session-manager.ts b/packages/coding-agent/src/core/session-manager.ts index 62942480..b07968b3 100644 --- a/packages/coding-agent/src/core/session-manager.ts +++ b/packages/coding-agent/src/core/session-manager.ts @@ -357,9 +357,10 @@ export function buildSessionContext( const path: SessionEntry[] = []; let current: SessionEntry | undefined = leaf; while (current) { - path.unshift(current); + path.push(current); current = current.parentId ? byId.get(current.parentId) : undefined; } + path.reverse(); // Extract settings and find compaction let thinkingLevel = "off"; @@ -1152,9 +1153,10 @@ export class SessionManager { const startId = fromId ?? this.leafId; let current = startId ? this.byId.get(startId) : undefined; while (current) { - path.unshift(current); + path.push(current); current = current.parentId ? this.byId.get(current.parentId) : undefined; } + path.reverse(); return path; } From 5505316ea28720574f493bb4cac1b910ad705f44 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 20 Jun 2026 21:05:55 +0200 Subject: [PATCH 055/122] fix(coding-agent): cache extension imports for session switches Closes #5905. --- packages/coding-agent/CHANGELOG.md | 1 + .../src/core/extensions/loader.ts | 84 ++++++++++- .../coding-agent/src/core/resource-loader.ts | 20 ++- .../extension-factory-cache.test.ts | 131 ++++++++++++++++++ 4 files changed, 226 insertions(+), 10 deletions(-) create mode 100644 packages/coding-agent/test/suite/regressions/extension-factory-cache.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 1e2a54d6..31f4b5f0 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed same-directory session switches to reuse imported extension modules while preserving fresh extension instances and lifecycle events ([#5905](https://github.com/earendil-works/pi/issues/5905)). - Fixed deep session branches taking quadratic time to build context or branch paths ([#5909](https://github.com/earendil-works/pi/issues/5909)). - Fixed fuzzy `edit` matches to preserve untouched line blocks instead of rewriting the whole file through normalized content ([#5899](https://github.com/earendil-works/pi/issues/5899)). - Fixed bash commands through legacy WSL `bash.exe` to pass scripts over stdin so shell variables expand in the target bash ([#5893](https://github.com/earendil-works/pi/issues/5893)). diff --git a/packages/coding-agent/src/core/extensions/loader.ts b/packages/coding-agent/src/core/extensions/loader.ts index 081d2d11..dcaf389f 100644 --- a/packages/coding-agent/src/core/extensions/loader.ts +++ b/packages/coding-agent/src/core/extensions/loader.ts @@ -117,6 +117,30 @@ function getAliases(): Record { type HandlerFn = (...args: unknown[]) => Promise; +let extensionCacheCwd: string | undefined; +let extensionCacheGeneration = 0; +const extensionCache = new Map(); + +interface ExtensionCacheToken { + cwd: string; + generation: number; +} + +export function clearExtensionCache(): void { + extensionCache.clear(); + extensionCacheCwd = undefined; + extensionCacheGeneration++; +} + +function useExtensionCacheCwd(cwd: string): ExtensionCacheToken { + const resolvedCwd = resolvePath(cwd); + if (extensionCacheCwd !== undefined && extensionCacheCwd !== resolvedCwd) { + clearExtensionCache(); + } + extensionCacheCwd = resolvedCwd; + return { cwd: resolvedCwd, generation: extensionCacheGeneration }; +} + /** * Create a runtime with throwing stubs for action methods. * Runner.bindCore() replaces these with real implementations. @@ -328,7 +352,22 @@ function createExtensionAPI( return api; } -async function loadExtensionModule(extensionPath: string) { +function isCurrentCacheToken(cacheToken: ExtensionCacheToken | undefined): cacheToken is ExtensionCacheToken { + return ( + cacheToken !== undefined && + extensionCacheCwd === cacheToken.cwd && + extensionCacheGeneration === cacheToken.generation + ); +} + +async function loadExtensionModule(extensionPath: string, cacheToken?: ExtensionCacheToken) { + if (isCurrentCacheToken(cacheToken)) { + const cachedFactory = extensionCache.get(extensionPath); + if (cachedFactory) { + return cachedFactory; + } + } + const jiti = createJiti(import.meta.url, { moduleCache: false, // In Bun binary: use virtualModules for bundled packages (no filesystem resolution) @@ -339,7 +378,13 @@ async function loadExtensionModule(extensionPath: string) { const module = await jiti.import(extensionPath, { default: true }); const factory = module as ExtensionFactory; - return typeof factory !== "function" ? undefined : factory; + if (typeof factory !== "function") { + return undefined; + } + if (isCurrentCacheToken(cacheToken)) { + extensionCache.set(extensionPath, factory); + } + return factory; } /** @@ -370,11 +415,12 @@ async function loadExtension( cwd: string, eventBus: EventBus, runtime: ExtensionRuntime, + cacheToken?: ExtensionCacheToken, ): Promise<{ extension: Extension | null; error: string | null }> { const resolvedPath = resolvePath(extensionPath, cwd, { normalizeUnicodeSpaces: true }); try { - const factory = await loadExtensionModule(resolvedPath); + const factory = await loadExtensionModule(resolvedPath, cacheToken); if (!factory) { return { extension: null, error: `Extension does not export a valid factory function: ${extensionPath}` }; } @@ -410,20 +456,28 @@ export async function loadExtensionFromFactory( /** * Load extensions from paths. */ -export async function loadExtensions( +async function loadExtensionsInternal( paths: string[], cwd: string, eventBus?: EventBus, runtime?: ExtensionRuntime, + useCache = false, ): Promise { const extensions: Extension[] = []; const errors: Array<{ path: string; error: string }> = []; - const resolvedCwd = resolvePath(cwd); + const cacheToken = useCache ? useExtensionCacheCwd(cwd) : undefined; + const resolvedCwd = cacheToken?.cwd ?? resolvePath(cwd); const resolvedEventBus = eventBus ?? createEventBus(); const resolvedRuntime = runtime ?? createExtensionRuntime(); for (const extPath of paths) { - const { extension, error } = await loadExtension(extPath, resolvedCwd, resolvedEventBus, resolvedRuntime); + const { extension, error } = await loadExtension( + extPath, + resolvedCwd, + resolvedEventBus, + resolvedRuntime, + cacheToken, + ); if (error) { errors.push({ path: extPath, error }); @@ -442,6 +496,24 @@ export async function loadExtensions( }; } +export async function loadExtensions( + paths: string[], + cwd: string, + eventBus?: EventBus, + runtime?: ExtensionRuntime, +): Promise { + return loadExtensionsInternal(paths, cwd, eventBus, runtime); +} + +export async function loadExtensionsCached( + paths: string[], + cwd: string, + eventBus?: EventBus, + runtime?: ExtensionRuntime, +): Promise { + return loadExtensionsInternal(paths, cwd, eventBus, runtime, true); +} + interface PiManifest { extensions?: string[]; themes?: string[]; diff --git a/packages/coding-agent/src/core/resource-loader.ts b/packages/coding-agent/src/core/resource-loader.ts index b35787af..18486ead 100644 --- a/packages/coding-agent/src/core/resource-loader.ts +++ b/packages/coding-agent/src/core/resource-loader.ts @@ -9,7 +9,12 @@ export type { ResourceCollision, ResourceDiagnostic } from "./diagnostics.ts"; import { canonicalizePath, isLocalPath, resolvePath } from "../utils/paths.ts"; import { createEventBus, type EventBus } from "./event-bus.ts"; -import { createExtensionRuntime, loadExtensionFromFactory, loadExtensions } from "./extensions/loader.ts"; +import { + clearExtensionCache, + createExtensionRuntime, + loadExtensionFromFactory, + loadExtensionsCached, +} from "./extensions/loader.ts"; import type { Extension, ExtensionFactory, ExtensionRuntime, LoadExtensionsResult } from "./extensions/types.ts"; import { DefaultPackageManager, type PathMetadata, type ResolvedResource } from "./package-manager.ts"; import type { PromptTemplate } from "./prompt-templates.ts"; @@ -206,6 +211,7 @@ export class DefaultResourceLoader implements ResourceLoader { private extensionThemeSourceInfos: Map; private lastPromptPaths: string[]; private lastThemePaths: string[]; + private loaded: boolean; constructor(options: DefaultResourceLoaderOptions) { this.cwd = resolvePath(options.cwd); @@ -252,6 +258,7 @@ export class DefaultResourceLoader implements ResourceLoader { this.extensionThemeSourceInfos = new Map(); this.lastPromptPaths = []; this.lastThemePaths = []; + this.loaded = false; } getExtensions(): LoadExtensionsResult { @@ -331,6 +338,10 @@ export class DefaultResourceLoader implements ResourceLoader { } async reload(options?: ResourceLoaderReloadOptions): Promise { + if (this.loaded) { + clearExtensionCache(); + } + let preTrustExtensions: LoadExtensionsResult | undefined; if (options?.resolveProjectTrust) { preTrustExtensions = await this.loadProjectTrustExtensions(); @@ -475,6 +486,7 @@ export class DefaultResourceLoader implements ResourceLoader { this.appendSystemPrompt = this.appendSystemPromptOverride ? this.appendSystemPromptOverride(baseAppend) : baseAppend; + this.loaded = true; } private async loadCurrentExtensionSet(options: { includeInlineFactories: boolean }): Promise { @@ -487,7 +499,7 @@ export class DefaultResourceLoader implements ResourceLoader { const extensionPaths = this.noExtensions ? cliEnabledExtensions : this.mergePaths(cliEnabledExtensions, enabledExtensions); - const extensionsResult = await loadExtensions(extensionPaths, this.cwd, this.eventBus); + const extensionsResult = await loadExtensionsCached(extensionPaths, this.cwd, this.eventBus); if (!options.includeInlineFactories) { return extensionsResult; } @@ -507,7 +519,7 @@ export class DefaultResourceLoader implements ResourceLoader { preTrustExtensions: LoadExtensionsResult | undefined, ): Promise { if (!preTrustExtensions) { - const extensionsResult = await loadExtensions(extensionPaths, this.cwd, this.eventBus); + const extensionsResult = await loadExtensionsCached(extensionPaths, this.cwd, this.eventBus); const inlineExtensions = await this.loadExtensionFactories(extensionsResult.runtime); extensionsResult.extensions.push(...inlineExtensions.extensions); extensionsResult.errors.push(...inlineExtensions.errors); @@ -527,7 +539,7 @@ export class DefaultResourceLoader implements ResourceLoader { const resolvedPath = this.resolveExtensionLoadPath(path); return !preloadedByPath.has(resolvedPath) && !failedPreloadPaths.has(resolvedPath); }); - const remainingExtensions = await loadExtensions( + const remainingExtensions = await loadExtensionsCached( remainingPaths, this.cwd, this.eventBus, diff --git a/packages/coding-agent/test/suite/regressions/extension-factory-cache.test.ts b/packages/coding-agent/test/suite/regressions/extension-factory-cache.test.ts new file mode 100644 index 00000000..ced01224 --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/extension-factory-cache.test.ts @@ -0,0 +1,131 @@ +import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { clearExtensionCache, loadExtensions, loadExtensionsCached } from "../../../src/core/extensions/loader.ts"; +import { DefaultResourceLoader } from "../../../src/core/resource-loader.ts"; + +interface TestState { + moduleLoads?: number; + factoryRuns?: number; +} + +function state(): TestState { + const global = globalThis as typeof globalThis & { __extensionFactoryCacheTest?: TestState }; + if (!global.__extensionFactoryCacheTest) { + global.__extensionFactoryCacheTest = {}; + } + return global.__extensionFactoryCacheTest; +} + +function resetState(): void { + delete (globalThis as typeof globalThis & { __extensionFactoryCacheTest?: TestState }).__extensionFactoryCacheTest; +} + +function writeCountingExtension(filePath: string): void { + writeFileSync( + filePath, + ` +const state = (globalThis.__extensionFactoryCacheTest ??= {}); +state.moduleLoads = (state.moduleLoads ?? 0) + 1; + +export default function () { + state.factoryRuns = (state.factoryRuns ?? 0) + 1; +} +`, + "utf-8", + ); +} + +describe("extension factory cache", () => { + const roots: string[] = []; + + function fixture(name: string) { + const root = join(tmpdir(), `pi-extension-cache-${name}-${Date.now()}-${Math.random().toString(36).slice(2)}`); + const cwd = join(root, "project"); + const agentDir = join(root, "agent"); + mkdirSync(cwd, { recursive: true }); + mkdirSync(agentDir, { recursive: true }); + roots.push(root); + return { root, cwd, agentDir }; + } + + beforeEach(() => { + resetState(); + clearExtensionCache(); + }); + + afterEach(() => { + while (roots.length > 0) { + const root = roots.pop(); + if (root && existsSync(root)) { + rmSync(root, { recursive: true, force: true }); + } + } + resetState(); + clearExtensionCache(); + }); + + it("caches extension modules for cached same-cwd loads but reruns factories", async () => { + const { root, cwd } = fixture("same-cwd"); + const extensionPath = join(root, "counting.ts"); + writeCountingExtension(extensionPath); + + const first = await loadExtensionsCached([extensionPath], cwd); + const second = await loadExtensionsCached([extensionPath], cwd); + + expect(state().moduleLoads).toBe(1); + expect(state().factoryRuns).toBe(2); + expect(first.extensions[0]).not.toBe(second.extensions[0]); + expect(first.runtime).not.toBe(second.runtime); + }); + + it("does not cache direct loadExtensions calls", async () => { + const { root, cwd } = fixture("direct"); + const extensionPath = join(root, "counting.ts"); + writeCountingExtension(extensionPath); + + await loadExtensions([extensionPath], cwd); + await loadExtensions([extensionPath], cwd); + + expect(state().moduleLoads).toBe(2); + expect(state().factoryRuns).toBe(2); + }); + + it("clears the cache on resource loader reload", async () => { + const { cwd, agentDir } = fixture("reload"); + const extensionDir = join(agentDir, "extensions"); + mkdirSync(extensionDir, { recursive: true }); + writeCountingExtension(join(extensionDir, "counting.ts")); + const loader = new DefaultResourceLoader({ + cwd, + agentDir, + noSkills: true, + noPromptTemplates: true, + noThemes: true, + }); + + await loader.reload(); + await loader.reload(); + + expect(state().moduleLoads).toBe(2); + expect(state().factoryRuns).toBe(2); + }); + + it("keeps the cache scoped to one cwd", async () => { + const { root } = fixture("cross-cwd"); + const firstCwd = join(root, "first"); + const secondCwd = join(root, "second"); + mkdirSync(firstCwd, { recursive: true }); + mkdirSync(secondCwd, { recursive: true }); + const extensionPath = join(root, "counting.ts"); + writeCountingExtension(extensionPath); + + await loadExtensionsCached([extensionPath], firstCwd); + await loadExtensionsCached([extensionPath], secondCwd); + await loadExtensionsCached([extensionPath], secondCwd); + + expect(state().moduleLoads).toBe(2); + expect(state().factoryRuns).toBe(3); + }); +}); From 500b568b04cc54109480920ab391e76e525339c0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 20 Jun 2026 21:31:31 +0200 Subject: [PATCH 056/122] fix(ai): use OpenAI endpoint for Fireworks GLM-5.2 closes #5923 --- packages/ai/CHANGELOG.md | 1 + packages/ai/scripts/generate-models.ts | 9 ++++++++- packages/ai/src/models.generated.ts | 9 +++++---- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 567a2273..90a1b3bb 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed Fireworks GLM-5.2 metadata to use the OpenAI-compatible Chat Completions endpoint with `reasoning_effort` support ([#5923](https://github.com/earendil-works/pi/issues/5923)). - Fixed OpenRouter GLM-5.2 metadata to expose `xhigh` reasoning and send OpenRouter's native `xhigh` effort ([#5770](https://github.com/earendil-works/pi/issues/5770)). - Fixed GitHub Copilot OAuth model availability to use the authenticated account's model picker catalog ([#5897](https://github.com/earendil-works/pi/issues/5897)). diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index ad9ca95b..3e85db82 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -377,6 +377,9 @@ function applyThinkingLevelMetadata(model: Model): void { if (model.provider === "openrouter" && model.id === "z-ai/glm-5.2") { mergeThinkingLevelMap(model, { xhigh: "xhigh" }); } + if (model.provider === "fireworks" && model.id === "accounts/fireworks/models/glm-5p2") { + mergeThinkingLevelMap(model, { off: "none", minimal: null, low: "high", medium: "high", xhigh: "max" }); + } if (model.provider === "opencode-go" && model.id === "kimi-k2.6") { // OpenCode Go exposes Kimi K2.6 thinking as on/off, not distinct effort tiers. mergeThinkingLevelMap(model, { minimal: null, low: null, medium: null }); @@ -1514,7 +1517,11 @@ async function generateModels() { candidate.cost.output = 1.9; candidate.cost.cacheRead = 0.119; } - + if (candidate.provider === "fireworks" && candidate.id === "accounts/fireworks/models/glm-5p2") { + candidate.api = "openai-completions"; + candidate.baseUrl = "https://api.fireworks.ai/inference/v1"; + candidate.compat = { supportsStore: false, supportsDeveloperRole: false }; + } } diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 497f766a..32894f60 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -3930,11 +3930,12 @@ export const MODELS = { "accounts/fireworks/models/glm-5p2": { id: "accounts/fireworks/models/glm-5p2", name: "GLM 5.2", - api: "anthropic-messages", + api: "openai-completions", provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + baseUrl: "https://api.fireworks.ai/inference/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false}, reasoning: true, + thinkingLevelMap: {"off":"none","minimal":null,"low":"high","medium":"high","xhigh":"max"}, input: ["text"], cost: { input: 1.4, @@ -3944,7 +3945,7 @@ export const MODELS = { }, contextWindow: 1048576, maxTokens: 131072, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "accounts/fireworks/models/gpt-oss-120b": { id: "accounts/fireworks/models/gpt-oss-120b", name: "GPT OSS 120B", From 350ac3f3440367368510151c60f4cbb18ff6a7fe Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 20 Jun 2026 21:48:37 +0200 Subject: [PATCH 057/122] fix: remove inprogress from auto-closed issues --- .github/workflows/issue-triage-labels.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/issue-triage-labels.yml b/.github/workflows/issue-triage-labels.yml index cf17c39e..ebe5500c 100644 --- a/.github/workflows/issue-triage-labels.yml +++ b/.github/workflows/issue-triage-labels.yml @@ -18,6 +18,7 @@ jobs: const NO_ACTION_LABEL = 'no-action'; const LAST_READ_LABEL = 'last-read'; const TO_DISCUSS_LABEL = 'to-discuss'; + const INPROGRESS_LABEL = 'inprogress'; function issueHasLabel(issue, labelName) { return (issue.labels ?? []).some((label) => label.name === labelName); @@ -124,6 +125,8 @@ jobs: }); console.log(`Closed #${issue.number} as not planned`); + await removeLabelIfPresent(issue.number, issue, INPROGRESS_LABEL); + await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, From 1aa79b9b29faacea6158ab958725032b16ef346f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 20 Jun 2026 22:00:00 +0200 Subject: [PATCH 058/122] docs: update unreleased changelog audit --- packages/coding-agent/CHANGELOG.md | 8 ++++++++ packages/tui/CHANGELOG.md | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 31f4b5f0..5e0b229b 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,14 +2,22 @@ ## [Unreleased] +### New Features + +- **Chat-template thinking compatibility** - OpenAI-compatible custom providers can map Pi thinking levels into `chat_template_kwargs`, enabling vLLM/Hugging Face chat-template models such as DeepSeek to use provider-native thinking controls. See [Custom Provider API Types](docs/custom-provider.md#api-types) and [OpenAI Compatibility](docs/models.md#openai-compatibility). +- **GLM-5.2 provider improvements** - GLM-5.2 now has corrected Fireworks OpenAI-compatible routing and OpenRouter `xhigh` thinking support, improving `/model` behavior and high-effort reasoning for GLM-5.2 users. See [Model Options](docs/usage.md#model-options). + ### Added - Added inherited configurable `chat-template` thinking support for OpenAI-compatible providers that use `chat_template_kwargs`, such as DeepSeek models behind vLLM ([#5673](https://github.com/earendil-works/pi/issues/5673)). ### Fixed +- Fixed inherited Fireworks GLM-5.2 metadata to use the OpenAI-compatible Chat Completions endpoint with `reasoning_effort` support ([#5923](https://github.com/earendil-works/pi/issues/5923)). - Fixed same-directory session switches to reuse imported extension modules while preserving fresh extension instances and lifecycle events ([#5905](https://github.com/earendil-works/pi/issues/5905)). - Fixed deep session branches taking quadratic time to build context or branch paths ([#5909](https://github.com/earendil-works/pi/issues/5909)). +- Fixed inherited OpenRouter GLM-5.2 metadata to expose `xhigh` reasoning and send OpenRouter's native `xhigh` effort ([#5770](https://github.com/earendil-works/pi/issues/5770)). +- Fixed inherited Markdown streaming code fence rendering so partial closing fences no longer make code blocks shrink or flicker while content streams ([#5846](https://github.com/earendil-works/pi/pull/5846) by [@xl0](https://github.com/xl0)). - Fixed fuzzy `edit` matches to preserve untouched line blocks instead of rewriting the whole file through normalized content ([#5899](https://github.com/earendil-works/pi/issues/5899)). - Fixed bash commands through legacy WSL `bash.exe` to pass scripts over stdin so shell variables expand in the target bash ([#5893](https://github.com/earendil-works/pi/issues/5893)). - Fixed `/model` to hide GitHub Copilot models that are unavailable to the authenticated account ([#5897](https://github.com/earendil-works/pi/issues/5897)). diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 2e9f6d31..065f1321 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed Markdown streaming code fence rendering so partial closing fences no longer make code blocks shrink or flicker while content streams ([#5846](https://github.com/earendil-works/pi/pull/5846) by [@xl0](https://github.com/xl0)). + ## [0.79.8] - 2026-06-19 ## [0.79.7] - 2026-06-18 From 615bf2f87444c0f48756d487323519349304906c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 20 Jun 2026 22:15:37 +0200 Subject: [PATCH 059/122] Release v0.79.9 --- package-lock.json | 26 ++++++------- packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 +- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- packages/ai/src/models.generated.ts | 38 +++++++++---------- packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/gondolin/package-lock.json | 4 +- .../examples/extensions/gondolin/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/npm-shrinkwrap.json | 24 ++++++------ packages/coding-agent/package.json | 8 ++-- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- 20 files changed, 69 insertions(+), 69 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1f9d4e66..9bec652f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5111,10 +5111,10 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.79.8", + "version": "0.79.9", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.8", + "@earendil-works/pi-ai": "^0.79.9", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -5463,7 +5463,7 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.79.8", + "version": "0.79.9", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -5769,12 +5769,12 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.8", + "version": "0.79.9", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.8", - "@earendil-works/pi-ai": "^0.79.8", - "@earendil-works/pi-tui": "^0.79.8", + "@earendil-works/pi-agent-core": "^0.79.9", + "@earendil-works/pi-ai": "^0.79.9", + "@earendil-works/pi-tui": "^0.79.9", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -5815,32 +5815,32 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.79.8", + "version": "0.79.9", "dependencies": { "@anthropic-ai/sdk": "0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.79.8" + "version": "0.79.9" }, "packages/coding-agent/examples/extensions/gondolin": { "name": "pi-extension-gondolin", - "version": "0.79.8", + "version": "0.79.9", "dependencies": { "@earendil-works/gondolin": "0.12.0" } }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.9.8", + "version": "1.9.9", "dependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.79.8", + "version": "0.79.9", "dependencies": { "ms": "2.1.3" }, @@ -6136,7 +6136,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.79.8", + "version": "0.79.9", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 18d0e561..6c98303b 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.9] - 2026-06-20 ### Fixed diff --git a/packages/agent/package.json b/packages/agent/package.json index b3aa9bae..75a73cb8 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.79.8", + "version": "0.79.9", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -33,7 +33,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.79.8", + "@earendil-works/pi-ai": "^0.79.9", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 90a1b3bb..2c7ff556 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.9] - 2026-06-20 ### Added diff --git a/packages/ai/package.json b/packages/ai/package.json index 830df19e..77272079 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.79.8", + "version": "0.79.9", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 32894f60..afdea285 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -9657,7 +9657,7 @@ export const MODELS = { cacheRead: 0.2, cacheWrite: 0.375, }, - contextWindow: 65536, + contextWindow: 131072, maxTokens: 32768, } satisfies Model<"openai-completions">, "google/gemini-3.1-flash-lite": { @@ -9845,7 +9845,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 8192, + maxTokens: 32768, } satisfies Model<"openai-completions">, "ibm-granite/granite-4.1-8b": { id: "ibm-granite/granite-4.1-8b", @@ -9947,8 +9947,8 @@ export const MODELS = { cacheRead: 0.06, cacheWrite: 0, }, - contextWindow: 256000, - maxTokens: 80000, + contextWindow: 262144, + maxTokens: 144000, } satisfies Model<"openai-completions">, "liquid/lfm-2.5-1.2b-thinking:free": { id: "liquid/lfm-2.5-1.2b-thinking:free", @@ -10169,7 +10169,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 512000, + maxTokens: 4096, } satisfies Model<"openai-completions">, "mistralai/codestral-2508": { id: "mistralai/codestral-2508", @@ -10505,7 +10505,7 @@ export const MODELS = { cost: { input: 0.6, output: 2.5, - cacheRead: 0, + cacheRead: 0.6, cacheWrite: 0, }, contextWindow: 262144, @@ -10884,7 +10884,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 1047576, - maxTokens: 32768, + maxTokens: 4096, } satisfies Model<"openai-completions">, "openai/gpt-4.1-nano": { id: "openai/gpt-4.1-nano", @@ -10914,7 +10914,7 @@ export const MODELS = { cost: { input: 2.5, output: 10, - cacheRead: 0, + cacheRead: 1.25, cacheWrite: 0, }, contextWindow: 128000, @@ -11050,11 +11050,11 @@ export const MODELS = { cost: { input: 0.25, output: 2, - cacheRead: 0.025, + cacheRead: 0.03, cacheWrite: 0, }, contextWindow: 400000, - maxTokens: 128000, + maxTokens: 4096, } satisfies Model<"openai-completions">, "openai/gpt-5-nano": { id: "openai/gpt-5-nano", @@ -11118,11 +11118,11 @@ export const MODELS = { cost: { input: 1.25, output: 10, - cacheRead: 0.13, + cacheRead: 0.125, cacheWrite: 0, }, contextWindow: 128000, - maxTokens: 32000, + maxTokens: 16384, } satisfies Model<"openai-completions">, "openai/gpt-5.1-codex": { id: "openai/gpt-5.1-codex", @@ -11209,7 +11209,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 128000, - maxTokens: 16384, + maxTokens: 32000, } satisfies Model<"openai-completions">, "openai/gpt-5.2-codex": { id: "openai/gpt-5.2-codex", @@ -12422,11 +12422,11 @@ export const MODELS = { cost: { input: 0.14, output: 1, - cacheRead: 0, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262144, + maxTokens: 81920, } satisfies Model<"openai-completions">, "qwen/qwen3.5-397b-a17b": { id: "qwen/qwen3.5-397b-a17b", @@ -12459,8 +12459,8 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 262144, - maxTokens: 262144, + contextWindow: 256000, + maxTokens: 32768, } satisfies Model<"openai-completions">, "qwen/qwen3.5-flash-02-23": { id: "qwen/qwen3.5-flash-02-23", @@ -13034,11 +13034,11 @@ export const MODELS = { cost: { input: 0.98, output: 3.08, - cacheRead: 0.49, + cacheRead: 0.182, cacheWrite: 0, }, contextWindow: 202752, - maxTokens: 65535, + maxTokens: 4096, } satisfies Model<"openai-completions">, "z-ai/glm-5.2": { id: "z-ai/glm-5.2", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 5e0b229b..48f64d23 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.9] - 2026-06-20 ### New Features diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index 436f151f..9a131279 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.79.8", + "version": "0.79.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.79.8", + "version": "0.79.9", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index aa509051..8b4182e7 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.79.8", + "version": "0.79.9", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index 51de298b..779d519c 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.79.8", + "version": "0.79.9", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/gondolin/package-lock.json b/packages/coding-agent/examples/extensions/gondolin/package-lock.json index 3f126430..01ee2f61 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package-lock.json +++ b/packages/coding-agent/examples/extensions/gondolin/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-gondolin", - "version": "0.79.8", + "version": "0.79.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-gondolin", - "version": "0.79.8", + "version": "0.79.9", "dependencies": { "@earendil-works/gondolin": "0.12.0" } diff --git a/packages/coding-agent/examples/extensions/gondolin/package.json b/packages/coding-agent/examples/extensions/gondolin/package.json index 1d42d841..67a9ac1e 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package.json +++ b/packages/coding-agent/examples/extensions/gondolin/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-gondolin", "private": true, - "version": "0.79.8", + "version": "0.79.9", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 2a587f88..01aad382 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.9.8", + "version": "1.9.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.9.8", + "version": "1.9.9", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index c9d4aa3f..b8dc74ab 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.9.8", + "version": "1.9.9", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index 2dc3506b..89961b12 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.79.8", + "version": "0.79.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.79.8", + "version": "0.79.9", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index 1c409ca4..3ed465d2 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.79.8", + "version": "0.79.9", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index 13ff9161..c311b556 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -1,17 +1,17 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.8", + "version": "0.79.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.8", + "version": "0.79.9", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.8", - "@earendil-works/pi-ai": "^0.79.8", - "@earendil-works/pi-tui": "^0.79.8", + "@earendil-works/pi-agent-core": "^0.79.9", + "@earendil-works/pi-ai": "^0.79.9", + "@earendil-works/pi-tui": "^0.79.9", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -474,11 +474,11 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.79.8", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.8.tgz", + "version": "0.79.9", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.9.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.8", + "@earendil-works/pi-ai": "^0.79.9", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -488,8 +488,8 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.79.8", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.8.tgz", + "version": "0.79.9", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.9.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -512,8 +512,8 @@ } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.79.8", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.8.tgz", + "version": "0.79.9", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.9.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 4aa30dd2..490a7751 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.8", + "version": "0.79.9", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -36,9 +36,9 @@ "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.8", - "@earendil-works/pi-ai": "^0.79.8", - "@earendil-works/pi-tui": "^0.79.8", + "@earendil-works/pi-agent-core": "^0.79.9", + "@earendil-works/pi-ai": "^0.79.9", + "@earendil-works/pi-tui": "^0.79.9", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 065f1321..e7cf9b0f 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.9] - 2026-06-20 ### Fixed diff --git a/packages/tui/package.json b/packages/tui/package.json index 36f002a7..d9d53e0e 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.79.8", + "version": "0.79.9", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", From b4f31408b67bde605e9c2fe0eb365ef2b73690f9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 20 Jun 2026 22:15:40 +0200 Subject: [PATCH 060/122] Add [Unreleased] section for next cycle --- packages/agent/CHANGELOG.md | 2 ++ packages/ai/CHANGELOG.md | 2 ++ packages/coding-agent/CHANGELOG.md | 2 ++ packages/tui/CHANGELOG.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 6c98303b..ff48ad2c 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.9] - 2026-06-20 ### Fixed diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 2c7ff556..586260cf 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.9] - 2026-06-20 ### Added diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 48f64d23..ba9b1ae7 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.9] - 2026-06-20 ### New Features diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index e7cf9b0f..96ff6d5c 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.9] - 2026-06-20 ### Fixed From d93b92baca8fe2f9eac048185669a403d4e5ba8b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 20 Jun 2026 22:23:25 +0200 Subject: [PATCH 061/122] fix(coding-agent): show changelog URL in update notice --- packages/coding-agent/src/modes/interactive/interactive-mode.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 962c414f..613d588c 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -3714,7 +3714,7 @@ export class InteractiveMode { const updateInstruction = theme.fg("muted", `New version ${release.version} is available. Run `) + action; const changelogUrl = "https://pi.dev/changelog"; const changelogLink = getCapabilities().hyperlinks - ? hyperlink(theme.fg("accent", "open changelog"), changelogUrl) + ? hyperlink(theme.fg("accent", changelogUrl), changelogUrl) : theme.fg("accent", changelogUrl); const changelogLine = theme.fg("muted", "Changelog: ") + changelogLink; const note = release.note?.trim(); From bc0db643502ba0bf1b227a97d9d5885cefc2b909 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 21 Jun 2026 14:46:07 +0200 Subject: [PATCH 062/122] fix(coding-agent): install checked pi update version --- packages/coding-agent/CHANGELOG.md | 4 ++ packages/coding-agent/src/config.ts | 42 +++++++++----- .../coding-agent/src/package-manager-cli.ts | 55 ++++++++++++------- packages/coding-agent/test/config.test.ts | 23 ++++++++ .../test/package-command-paths.test.ts | 23 +++++--- 5 files changed, 105 insertions(+), 42 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index ba9b1ae7..92bdd6ba 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed `pi update` to install the exact version returned by the Pi update check, make `--force` reinstall that checked version, fail instead of falling back to an unversioned reinstall when no version is available, and report both the old and updated versions. + ## [0.79.9] - 2026-06-20 ### New Features diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index 75b2efaf..38049f05 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -38,6 +38,18 @@ export interface SelfUpdateCommand extends SelfUpdateCommandStep { steps?: SelfUpdateCommandStep[]; } +export type SelfUpdatePackageTarget = string | { packageName: string; installSpec?: string }; + +function normalizeSelfUpdatePackageTarget(target: SelfUpdatePackageTarget): { + packageName: string; + installSpec: string; +} { + if (typeof target === "string") { + return { packageName: target, installSpec: target }; + } + return { packageName: target.packageName, installSpec: target.installSpec ?? target.packageName }; +} + function makeSelfUpdateCommand( installStep: SelfUpdateCommandStep, uninstallStep?: SelfUpdateCommandStep, @@ -103,9 +115,10 @@ function getInferredNpmInstall(): { root: string; prefix: string } | undefined { function getSelfUpdateCommandForMethod( method: InstallMethod, installedPackageName: string, - updatePackageName = installedPackageName, + updatePackageTarget: SelfUpdatePackageTarget = installedPackageName, npmCommand?: string[], ): SelfUpdateCommand | undefined { + const target = normalizeSelfUpdatePackageTarget(updatePackageTarget); switch (method) { case "bun-binary": return undefined; @@ -123,17 +136,17 @@ function getSelfUpdateCommandForMethod( "--ignore-scripts", "--config.minimumReleaseAge=0", ...binDirArgs, - updatePackageName, + target.installSpec, ]), - updatePackageName === installedPackageName + target.packageName === installedPackageName ? undefined : makeSelfUpdateCommandStep("pnpm", ["remove", "-g", ...binDirArgs, installedPackageName]), ); } case "yarn": return makeSelfUpdateCommand( - makeSelfUpdateCommandStep("yarn", ["global", "add", "--ignore-scripts", updatePackageName]), - updatePackageName === installedPackageName + makeSelfUpdateCommandStep("yarn", ["global", "add", "--ignore-scripts", target.installSpec]), + target.packageName === installedPackageName ? undefined : makeSelfUpdateCommandStep("yarn", ["global", "remove", installedPackageName]), ); @@ -144,9 +157,9 @@ function getSelfUpdateCommandForMethod( "-g", "--ignore-scripts", "--minimum-release-age=0", - updatePackageName, + target.installSpec, ]), - updatePackageName === installedPackageName + target.packageName === installedPackageName ? undefined : makeSelfUpdateCommandStep("bun", ["uninstall", "-g", installedPackageName]), ); @@ -160,10 +173,10 @@ function getSelfUpdateCommandForMethod( "-g", "--ignore-scripts", "--min-release-age=0", - updatePackageName, + target.installSpec, ]); const uninstallStep = - updatePackageName === installedPackageName + target.packageName === installedPackageName ? undefined : makeSelfUpdateCommandStep(command, [...prefixArgs, "uninstall", "-g", installedPackageName]); return makeSelfUpdateCommand(installStep, uninstallStep); @@ -302,10 +315,10 @@ function isManagedByGlobalPackageManager(method: InstallMethod, packageName: str export function getSelfUpdateCommand( packageName: string, npmCommand?: string[], - updatePackageName = packageName, + updatePackageTarget: SelfUpdatePackageTarget = packageName, ): SelfUpdateCommand | undefined { const method = detectInstallMethod(); - const command = getSelfUpdateCommandForMethod(method, packageName, updatePackageName, npmCommand); + const command = getSelfUpdateCommandForMethod(method, packageName, updatePackageTarget, npmCommand); if (!command || !isManagedByGlobalPackageManager(method, packageName, npmCommand) || !isSelfUpdatePathWritable()) { return undefined; } @@ -315,20 +328,21 @@ export function getSelfUpdateCommand( export function getSelfUpdateUnavailableInstruction( packageName: string, npmCommand?: string[], - updatePackageName = packageName, + updatePackageTarget: SelfUpdatePackageTarget = packageName, ): string { const method = detectInstallMethod(); + const target = normalizeSelfUpdatePackageTarget(updatePackageTarget); if (method === "bun-binary") { return `Download from: https://github.com/earendil-works/pi-mono/releases/latest`; } - const command = getSelfUpdateCommandForMethod(method, packageName, updatePackageName, npmCommand); + const command = getSelfUpdateCommandForMethod(method, packageName, target, npmCommand); if (command) { if (isManagedByGlobalPackageManager(method, packageName, npmCommand) && !isSelfUpdatePathWritable()) { return `This installation is managed by a global ${method} install, but the install path is not writable. Update it yourself with: ${command.display}`; } return `This installation is not managed by a global ${method} install. Update it with the package manager, wrapper, or source checkout that provides it.`; } - return `Update ${updatePackageName} using the package manager, wrapper, or source checkout that provides this installation.`; + return `Update ${target.installSpec} using the package manager, wrapper, or source checkout that provides this installation.`; } export function getUpdateInstruction(packageName: string): string { diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index d47df930..32980851 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -12,6 +12,7 @@ import { getSelfUpdateUnavailableInstruction, PACKAGE_NAME, type SelfUpdateCommand, + type SelfUpdatePackageTarget, VERSION, } from "./config.ts"; import type { ExtensionFactory } from "./core/extensions/types.ts"; @@ -353,9 +354,12 @@ function updateTargetIncludesExtensions(target: UpdateTarget): boolean { return target.type === "all" || target.type === "extensions"; } -function printSelfUpdateUnavailable(npmCommand?: string[], updatePackageName = PACKAGE_NAME): void { +function printSelfUpdateUnavailable( + npmCommand?: string[], + updatePackageTarget: SelfUpdatePackageTarget = PACKAGE_NAME, +): void { console.error(`error: ${APP_NAME} cannot self-update this installation.`); - console.error(getSelfUpdateUnavailableInstruction(PACKAGE_NAME, npmCommand, updatePackageName)); + console.error(getSelfUpdateUnavailableInstruction(PACKAGE_NAME, npmCommand, updatePackageTarget)); const entrypoint = process.argv[1]; if (entrypoint) { @@ -390,27 +394,38 @@ function printSelfUpdateNote(note: string): void { interface SelfUpdatePlan { packageName: string; + installSpec: string; + version: string; shouldRun: boolean; note?: string; } async function getSelfUpdatePlan(force: boolean): Promise { - if (force) { - return { packageName: PACKAGE_NAME, shouldRun: true }; + let latestRelease: Awaited>; + try { + latestRelease = await getLatestPiRelease(VERSION); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Could not determine latest ${APP_NAME} version: ${message}`); + } + if (!latestRelease) { + throw new Error(`Could not determine latest ${APP_NAME} version.`); } - try { - const latestRelease = await getLatestPiRelease(VERSION); - const packageName = latestRelease?.packageName ?? PACKAGE_NAME; - if (!latestRelease || packageName !== PACKAGE_NAME || isNewerPackageVersion(latestRelease.version, VERSION)) { - return { packageName, shouldRun: true, ...(latestRelease?.note ? { note: latestRelease.note } : {}) }; - } - } catch { - return { packageName: PACKAGE_NAME, shouldRun: true }; + const packageName = latestRelease.packageName ?? PACKAGE_NAME; + const installSpec = `${packageName}@${latestRelease.version}`; + if (force || packageName !== PACKAGE_NAME || isNewerPackageVersion(latestRelease.version, VERSION)) { + return { + packageName, + installSpec, + version: latestRelease.version, + ...(latestRelease.note ? { note: latestRelease.note } : {}), + shouldRun: true, + }; } console.log(chalk.green(`${APP_NAME} is already up to date (v${VERSION})`)); - return { packageName: PACKAGE_NAME, shouldRun: false }; + return { packageName, installSpec, version: latestRelease.version, shouldRun: false }; } async function runSelfUpdate(command: SelfUpdateCommand): Promise { @@ -717,13 +732,13 @@ export async function handlePackageCommand( process.exitCode = 1; return true; } - const selfUpdateCommand = getSelfUpdateCommand( - PACKAGE_NAME, - selfUpdateNpmCommand, - selfUpdatePlan.packageName, - ); + const selfUpdateTarget = { + packageName: selfUpdatePlan.packageName, + installSpec: selfUpdatePlan.installSpec, + }; + const selfUpdateCommand = getSelfUpdateCommand(PACKAGE_NAME, selfUpdateNpmCommand, selfUpdateTarget); if (!selfUpdateCommand) { - printSelfUpdateUnavailable(selfUpdateNpmCommand, selfUpdatePlan.packageName); + printSelfUpdateUnavailable(selfUpdateNpmCommand, selfUpdateTarget); process.exitCode = 1; return true; } @@ -742,7 +757,7 @@ export async function handlePackageCommand( process.exitCode = 1; return true; } - console.log(chalk.green(`Updated ${APP_NAME}`)); + console.log(chalk.green(`Updated ${APP_NAME} from ${VERSION} to ${selfUpdatePlan.version}`)); } return true; } diff --git a/packages/coding-agent/test/config.test.ts b/packages/coding-agent/test/config.test.ts index cbfa044e..49448cf1 100644 --- a/packages/coding-agent/test/config.test.ts +++ b/packages/coding-agent/test/config.test.ts @@ -188,6 +188,29 @@ describe("detectInstallMethod", () => { }); }); + test("self-updates exact npm versions without uninstalling the current package", () => { + const { prefix } = createNpmPrefixInstall(); + + const command = getSelfUpdateCommand("@earendil-works/pi-coding-agent", undefined, { + packageName: "@earendil-works/pi-coding-agent", + installSpec: "@earendil-works/pi-coding-agent@1.2.3", + }); + + expect(command).toEqual({ + command: "npm", + args: [ + "--prefix", + prefix, + "install", + "-g", + "--ignore-scripts", + "--min-release-age=0", + "@earendil-works/pi-coding-agent@1.2.3", + ], + display: `npm --prefix ${prefix} install -g --ignore-scripts --min-release-age=0 @earendil-works/pi-coding-agent@1.2.3`, + }); + }); + test("self-updates renamed packages from the current install prefix", () => { const { prefix } = createNpmPrefixInstall(); diff --git a/packages/coding-agent/test/package-command-paths.test.ts b/packages/coding-agent/test/package-command-paths.test.ts index df87cf34..6df65ded 100644 --- a/packages/coding-agent/test/package-command-paths.test.ts +++ b/packages/coding-agent/test/package-command-paths.test.ts @@ -374,7 +374,7 @@ describe("package commands", () => { } }); - it("uses global npmCommand and current package name for forced self updates without checking the api", async () => { + it("uses the update check version for forced self updates even when current", async () => { const globalPrefix = join(tempDir, "global-prefix"); const projectPrefix = join(tempDir, "project-prefix"); const selfPackageDir = join(globalPrefix, "lib", "node_modules", "@earendil-works", "pi-coding-agent"); @@ -402,7 +402,7 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args)); value: join(selfPackageDir, "dist", "cli.js"), configurable: true, }); - const fetchMock = vi.fn(); + const fetchMock = vi.fn(async () => Response.json({ version: VERSION })); vi.stubGlobal("fetch", fetchMock); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); @@ -413,11 +413,14 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args)); expect(process.exitCode).toBeUndefined(); expect(errorSpy).not.toHaveBeenCalled(); - expect(fetchMock).not.toHaveBeenCalled(); + expect(fetchMock).toHaveBeenCalledOnce(); + const stdout = logSpy.mock.calls.map(([message]) => String(message)).join("\n"); const recordedArgs = JSON.parse(readFileSync(recordPath, "utf-8")) as string[]; expect(recordedArgs).toContain(globalPrefix); - expect(recordedArgs).toContain(PACKAGE_NAME); + expect(recordedArgs).toContain(`${PACKAGE_NAME}@${VERSION}`); + expect(recordedArgs).not.toContain(PACKAGE_NAME); expect(recordedArgs).not.toContain(projectPrefix); + expect(stdout).toContain(`Updated pi from ${VERSION} to ${VERSION}`); } finally { logSpy.mockRestore(); errorSpy.mockRestore(); @@ -446,7 +449,8 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args)); value: join(selfPackageDir, "dist", "cli.js"), configurable: true, }); - const fetchMock = vi.fn(async () => Response.json({ version: getNewerPatchVersion() })); + const targetVersion = getNewerPatchVersion(); + const fetchMock = vi.fn(async () => Response.json({ version: targetVersion })); vi.stubGlobal("fetch", fetchMock); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); @@ -458,8 +462,11 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args)); expect(process.exitCode).toBeUndefined(); expect(errorSpy).not.toHaveBeenCalled(); expect(fetchMock).toHaveBeenCalledOnce(); + const stdout = logSpy.mock.calls.map(([message]) => String(message)).join("\n"); const recordedArgs = JSON.parse(readFileSync(recordPath, "utf-8")) as string[]; - expect(recordedArgs).toContain(PACKAGE_NAME); + expect(recordedArgs).toContain(`${PACKAGE_NAME}@${targetVersion}`); + expect(recordedArgs).not.toContain(PACKAGE_NAME); + expect(stdout).toContain(`Updated pi from ${VERSION} to ${targetVersion}`); } finally { logSpy.mockRestore(); errorSpy.mockRestore(); @@ -509,7 +516,7 @@ else { const recordedCalls = JSON.parse(readFileSync(recordPath, "utf-8")) as string[][]; expect(recordedCalls).toEqual([ expect.arrayContaining(["uninstall", "-g", PACKAGE_NAME]), - expect.arrayContaining(["install", "-g", activePackageName]), + expect.arrayContaining(["install", "-g", `${activePackageName}@0.73.0`]), ]); } finally { logSpy.mockRestore(); @@ -565,7 +572,7 @@ if(args.includes("install")) process.exit(23); const recordedCalls = JSON.parse(readFileSync(recordPath, "utf-8")) as string[][]; expect(recordedCalls).toEqual([ expect.arrayContaining(["uninstall", "-g", PACKAGE_NAME]), - expect.arrayContaining(["install", "-g", activePackageName]), + expect.arrayContaining(["install", "-g", `${activePackageName}@0.73.0`]), ]); } finally { logSpy.mockRestore(); From 542683b29ab2865976dddb006b4d70cffe315e25 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 21 Jun 2026 22:10:17 +0200 Subject: [PATCH 063/122] fix(coding-agent): fix plan-mode example closes #5940 --- packages/coding-agent/CHANGELOG.md | 1 + .../examples/extensions/plan-mode/README.md | 5 +- .../examples/extensions/plan-mode/index.ts | 128 ++++++++++---- .../test/plan-mode-extension.test.ts | 167 ++++++++++++++++++ 4 files changed, 260 insertions(+), 41 deletions(-) create mode 100644 packages/coding-agent/test/plan-mode-extension.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 92bdd6ba..20d13deb 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed the plan-mode example to preserve active custom tools, skip the action prompt when no plan is found, and queue refinement/execution follow-ups correctly from `agent_end` ([#5940](https://github.com/earendil-works/pi/issues/5940)). - Fixed `pi update` to install the exact version returned by the Pi update check, make `--force` reinstall that checked version, fail instead of falling back to an unversioned reinstall when no version is available, and report both the old and updated versions. ## [0.79.9] - 2026-06-20 diff --git a/packages/coding-agent/examples/extensions/plan-mode/README.md b/packages/coding-agent/examples/extensions/plan-mode/README.md index 549e3473..2568a684 100644 --- a/packages/coding-agent/examples/extensions/plan-mode/README.md +++ b/packages/coding-agent/examples/extensions/plan-mode/README.md @@ -4,7 +4,7 @@ Read-only exploration mode for safe code analysis. ## Features -- **Read-only tools**: Restricts available tools to read, bash, grep, find, ls, question +- **Built-in write tools disabled**: Disables edit/write while preserving other active tools - **Bash allowlist**: Only read-only bash commands are allowed - **Plan extraction**: Extracts numbered steps from `Plan:` sections - **Progress tracking**: Widget shows completion status during execution @@ -37,7 +37,8 @@ Plan: ## How It Works ### Plan Mode (Read-Only) -- Only read-only tools available +- Built-in edit/write tools disabled +- Other active tools remain available - Bash commands filtered through allowlist - Agent creates a plan without making changes diff --git a/packages/coding-agent/examples/extensions/plan-mode/index.ts b/packages/coding-agent/examples/extensions/plan-mode/index.ts index 40db408c..737ce56a 100644 --- a/packages/coding-agent/examples/extensions/plan-mode/index.ts +++ b/packages/coding-agent/examples/extensions/plan-mode/index.ts @@ -2,7 +2,7 @@ * Plan Mode Extension * * Read-only exploration mode for safe code analysis. - * When enabled, only read-only tools are available. + * When enabled, built-in write tools are disabled. * * Features: * - /plan command or Ctrl+Alt+P to toggle @@ -21,6 +21,15 @@ import { extractTodoItems, isSafeCommand, markCompletedSteps, type TodoItem } fr // Tools const PLAN_MODE_TOOLS = ["read", "bash", "grep", "find", "ls", "questionnaire"]; const NORMAL_MODE_TOOLS = ["read", "bash", "edit", "write"]; +const PLAN_MODE_DISABLED_TOOLS = new Set(["edit", "write"]); +const PLAN_MANAGED_TOOLS = new Set([...PLAN_MODE_TOOLS, ...NORMAL_MODE_TOOLS]); + +interface PlanModeState { + enabled: boolean; + todos?: TodoItem[]; + executing?: boolean; + toolsBeforePlanMode?: string[]; +} // Type guard for assistant messages function isAssistantMessage(m: AgentMessage): m is AssistantMessage { @@ -39,6 +48,7 @@ export default function planModeExtension(pi: ExtensionAPI): void { let planModeEnabled = false; let executionMode = false; let todoItems: TodoItem[] = []; + let toolsBeforePlanMode: string[] | undefined; pi.registerFlag("plan", { description: "Start in plan mode (read-only exploration)", @@ -73,19 +83,34 @@ export default function planModeExtension(pi: ExtensionAPI): void { } } - function togglePlanMode(ctx: ExtensionContext): void { - planModeEnabled = !planModeEnabled; - executionMode = false; - todoItems = []; + function uniqueToolNames(toolNames: string[]): string[] { + return [...new Set(toolNames)]; + } - if (planModeEnabled) { - pi.setActiveTools(PLAN_MODE_TOOLS); - ctx.ui.notify(`Plan mode enabled. Tools: ${PLAN_MODE_TOOLS.join(", ")}`); - } else { - pi.setActiveTools(NORMAL_MODE_TOOLS); - ctx.ui.notify("Plan mode disabled. Full access restored."); + function getPlanModeTools(activeToolNames: string[]): string[] { + return uniqueToolNames([ + ...activeToolNames.filter((name) => !PLAN_MODE_DISABLED_TOOLS.has(name)), + ...PLAN_MODE_TOOLS, + ]); + } + + function getNormalModeTools(activeToolNames: string[]): string[] { + return uniqueToolNames([ + ...NORMAL_MODE_TOOLS, + ...activeToolNames.filter((name) => !PLAN_MANAGED_TOOLS.has(name)), + ]); + } + + function enablePlanModeTools(): void { + if (toolsBeforePlanMode === undefined) { + toolsBeforePlanMode = pi.getActiveTools(); } - updateStatus(ctx); + pi.setActiveTools(getPlanModeTools(toolsBeforePlanMode)); + } + + function restoreNormalModeTools(): void { + pi.setActiveTools(toolsBeforePlanMode ?? getNormalModeTools(pi.getActiveTools())); + toolsBeforePlanMode = undefined; } function persistState(): void { @@ -93,9 +118,26 @@ export default function planModeExtension(pi: ExtensionAPI): void { enabled: planModeEnabled, todos: todoItems, executing: executionMode, + toolsBeforePlanMode, }); } + function togglePlanMode(ctx: ExtensionContext): void { + planModeEnabled = !planModeEnabled; + executionMode = false; + todoItems = []; + + if (planModeEnabled) { + enablePlanModeTools(); + ctx.ui.notify("Plan mode enabled. Built-in write tools disabled."); + } else { + restoreNormalModeTools(); + ctx.ui.notify("Plan mode disabled. Full access restored."); + } + updateStatus(ctx); + persistState(); + } + pi.registerCommand("plan", { description: "Toggle plan mode (read-only exploration)", handler: async (_args, ctx) => togglePlanMode(ctx), @@ -165,8 +207,8 @@ export default function planModeExtension(pi: ExtensionAPI): void { You are in plan mode - a read-only exploration mode for safe code analysis. Restrictions: -- You can only use: read, bash, grep, find, ls, questionnaire -- You CANNOT use: edit, write (file modifications are disabled) +- Built-in edit and write tools are disabled +- Other currently active tools remain available - Bash is restricted to an allowlist of read-only commands Ask clarifying questions using the questionnaire tool. @@ -228,7 +270,6 @@ After completing a step, include a [DONE:n] tag in your response.`, ); executionMode = false; todoItems = []; - pi.setActiveTools(NORMAL_MODE_TOOLS); updateStatus(ctx); persistState(); // Save cleared state so resume doesn't restore old execution mode } @@ -246,43 +287,51 @@ After completing a step, include a [DONE:n] tag in your response.`, } } + if (todoItems.length === 0) return; + persistState(); + // Show plan steps and prompt for next action - if (todoItems.length > 0) { - const todoListText = todoItems.map((t, i) => `${i + 1}. ☐ ${t.text}`).join("\n"); - pi.sendMessage( - { - customType: "plan-todo-list", - content: `**Plan Steps (${todoItems.length}):**\n\n${todoListText}`, - display: true, - }, - { triggerTurn: false }, - ); - } + const todoListText = todoItems.map((t, i) => `${i + 1}. ☐ ${t.text}`).join("\n"); + const planTodoListMessage = { + customType: "plan-todo-list", + content: `**Plan Steps (${todoItems.length}):**\n\n${todoListText}`, + display: true, + }; const choice = await ctx.ui.select("Plan mode - what next?", [ - todoItems.length > 0 ? "Execute the plan (track progress)" : "Execute the plan", + "Execute the plan (track progress)", "Stay in plan mode", "Refine the plan", ]); if (choice?.startsWith("Execute")) { - planModeEnabled = false; - executionMode = todoItems.length > 0; - pi.setActiveTools(NORMAL_MODE_TOOLS); - updateStatus(ctx); + const firstTodoItem = todoItems[0]; + if (!firstTodoItem) return; - const execMessage = - todoItems.length > 0 - ? `Execute the plan. Start with: ${todoItems[0].text}` - : "Execute the plan you just created."; + planModeEnabled = false; + executionMode = true; + restoreNormalModeTools(); + updateStatus(ctx); + persistState(); + + const remainingList = todoItems.map((t) => `${t.step}. ${t.text}`).join("\n"); + const execMessage = `Execute the plan. + +Remaining steps: +${remainingList} + +Start with: ${firstTodoItem.text} +After completing a step, include a [DONE:n] tag in your response.`; + pi.sendMessage(planTodoListMessage, { deliverAs: "followUp" }); pi.sendMessage( { customType: "plan-mode-execute", content: execMessage, display: true }, - { triggerTurn: true }, + { triggerTurn: true, deliverAs: "followUp" }, ); } else if (choice === "Refine the plan") { const refinement = await ctx.ui.editor("Refine the plan:", ""); if (refinement?.trim()) { - pi.sendUserMessage(refinement.trim()); + pi.sendMessage(planTodoListMessage, { deliverAs: "followUp" }); + pi.sendUserMessage(refinement.trim(), { deliverAs: "followUp" }); } } }); @@ -298,12 +347,13 @@ After completing a step, include a [DONE:n] tag in your response.`, // Restore persisted state const planModeEntry = entries .filter((e: { type: string; customType?: string }) => e.type === "custom" && e.customType === "plan-mode") - .pop() as { data?: { enabled: boolean; todos?: TodoItem[]; executing?: boolean } } | undefined; + .pop() as { data?: PlanModeState } | undefined; if (planModeEntry?.data) { planModeEnabled = planModeEntry.data.enabled ?? planModeEnabled; todoItems = planModeEntry.data.todos ?? todoItems; executionMode = planModeEntry.data.executing ?? executionMode; + toolsBeforePlanMode = planModeEntry.data.toolsBeforePlanMode ?? toolsBeforePlanMode; } // On resume: re-scan messages to rebuild completion state @@ -333,7 +383,7 @@ After completing a step, include a [DONE:n] tag in your response.`, } if (planModeEnabled) { - pi.setActiveTools(PLAN_MODE_TOOLS); + enablePlanModeTools(); } updateStatus(ctx); }); diff --git a/packages/coding-agent/test/plan-mode-extension.test.ts b/packages/coding-agent/test/plan-mode-extension.test.ts new file mode 100644 index 00000000..419a707e --- /dev/null +++ b/packages/coding-agent/test/plan-mode-extension.test.ts @@ -0,0 +1,167 @@ +import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AssistantMessage } from "@earendil-works/pi-ai"; +import { describe, expect, it, vi } from "vitest"; +import planModeExtension from "../examples/extensions/plan-mode/index.ts"; +import type { ExtensionAPI, ExtensionContext } from "../src/core/extensions/index.ts"; + +type CommandHandler = (args: string, ctx: ExtensionContext) => Promise | void; +type AgentEndHandler = ( + event: { type: "agent_end"; messages: AgentMessage[] }, + ctx: ExtensionContext, +) => Promise | void; + +function createAssistantMessage(text: string): AssistantMessage { + return { + role: "assistant", + content: [{ type: "text", text }], + api: "anthropic-messages", + provider: "anthropic", + model: "mock", + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "stop", + timestamp: Date.now(), + }; +} + +function setup(options: { activeTools?: string[]; selectChoice?: string; editorText?: string } = {}) { + let activeTools = options.activeTools ?? ["read", "bash", "edit", "write"]; + const commands = new Map(); + let agentEndHandler: AgentEndHandler | undefined; + + const sendMessage = vi.fn(); + const sendUserMessage = vi.fn(); + const setActiveTools = vi.fn((toolNames) => { + activeTools = [...toolNames]; + }); + const appendEntry = vi.fn(); + + const api = { + registerFlag: vi.fn(), + registerCommand(name: string, command: { handler: CommandHandler }) { + commands.set(name, command.handler); + }, + registerShortcut: vi.fn(), + on(event: string, handler: unknown) { + if (event === "agent_end") agentEndHandler = handler as AgentEndHandler; + }, + getFlag: vi.fn(() => false), + getActiveTools: vi.fn(() => [...activeTools]), + setActiveTools, + sendMessage, + sendUserMessage, + appendEntry, + } as unknown as ExtensionAPI; + + planModeExtension(api); + + const ctx = { + hasUI: true, + ui: { + notify: vi.fn(), + select: vi.fn(async () => options.selectChoice), + editor: vi.fn(async () => options.editorText), + setStatus: vi.fn(), + setWidget: vi.fn(), + theme: { + fg: (_name: string, text: string) => text, + strikethrough: (text: string) => text, + }, + }, + sessionManager: { getEntries: () => [] }, + isIdle: () => false, + hasPendingMessages: () => false, + } as unknown as ExtensionContext; + + async function runCommand(name: string): Promise { + const command = commands.get(name); + if (!command) throw new Error(`Missing command: ${name}`); + await command("", ctx); + } + + async function triggerAgentEnd(text: string): Promise { + if (!agentEndHandler) throw new Error("Missing agent_end handler"); + await agentEndHandler({ type: "agent_end", messages: [createAssistantMessage(text)] }, ctx); + } + + return { + activeTools: () => activeTools, + appendEntry, + ctx, + runCommand, + sendMessage, + sendUserMessage, + setActiveTools, + triggerAgentEnd, + }; +} + +describe("plan-mode example extension", () => { + it("preserves custom active tools while toggling plan mode", async () => { + const { activeTools, runCommand, setActiveTools } = setup({ + activeTools: ["read", "bash", "edit", "write", "echo_tool"], + }); + + await runCommand("plan"); + + expect(activeTools()).toEqual(["read", "bash", "echo_tool", "grep", "find", "ls", "questionnaire"]); + expect(setActiveTools).toHaveBeenLastCalledWith([ + "read", + "bash", + "echo_tool", + "grep", + "find", + "ls", + "questionnaire", + ]); + + await runCommand("plan"); + + expect(activeTools()).toEqual(["read", "bash", "edit", "write", "echo_tool"]); + expect(setActiveTools).toHaveBeenLastCalledWith(["read", "bash", "edit", "write", "echo_tool"]); + }); + + it("does not prompt when the assistant response contains no plan", async () => { + const { ctx, runCommand, sendMessage, triggerAgentEnd } = setup(); + + await runCommand("plan"); + await triggerAgentEnd("This file defines the command-line argument parser."); + + expect(ctx.ui.select).not.toHaveBeenCalled(); + expect(sendMessage).not.toHaveBeenCalled(); + }); + + it("queues plan refinement as a follow-up user message", async () => { + const { runCommand, sendUserMessage, triggerAgentEnd } = setup({ + selectChoice: "Refine the plan", + editorText: "Add a regression test.", + }); + + await runCommand("plan"); + await triggerAgentEnd("Plan:\n1. Inspect the current implementation\n2. Add a regression test"); + + expect(sendUserMessage).toHaveBeenCalledWith("Add a regression test.", { deliverAs: "followUp" }); + }); + + it("queues plan execution as a follow-up custom message", async () => { + const { activeTools, runCommand, sendMessage, triggerAgentEnd } = setup({ + activeTools: ["read", "bash", "edit", "write", "echo_tool"], + selectChoice: "Execute the plan (track progress)", + }); + + await runCommand("plan"); + await triggerAgentEnd("Plan:\n1. Inspect the current implementation\n2. Add a regression test"); + + expect(activeTools()).toEqual(["read", "bash", "edit", "write", "echo_tool"]); + expect(sendMessage).toHaveBeenCalledWith(expect.objectContaining({ customType: "plan-mode-execute" }), { + triggerTurn: true, + deliverAs: "followUp", + }); + }); +}); From 2417adb46a6ddc7f15bd396a78b0ca9b83ac2d0d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 21 Jun 2026 23:17:13 +0200 Subject: [PATCH 064/122] fix(coding-agent): preserve startup extension UI closes #5943 --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/core/agent-session.ts | 3 +- .../src/modes/interactive/interactive-mode.ts | 52 ++- .../interactive-mode-clone-command.test.ts | 2 +- .../5943-session-start-notify.test.ts | 430 ++++++++++++++++++ 5 files changed, 463 insertions(+), 25 deletions(-) create mode 100644 packages/coding-agent/test/suite/regressions/5943-session-start-notify.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 20d13deb..341921f2 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed transient extension UI and session-start messages emitted during session replacement or reload so they remain visible, and kept reload input blocked until reload completes ([#5943](https://github.com/earendil-works/pi/issues/5943)). - Fixed the plan-mode example to preserve active custom tools, skip the action prompt when no plan is found, and queue refinement/execution follow-ups correctly from `agent_end` ([#5940](https://github.com/earendil-works/pi/issues/5940)). - Fixed `pi update` to install the exact version returned by the Pi update check, make `--force` reinstall that checked version, fail instead of falling back to an unversioned reinstall when no version is available, and report both the old and updated versions. diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index f8af4320..a1033128 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -2443,7 +2443,7 @@ export class AgentSession { }); } - async reload(): Promise { + async reload(options?: { beforeSessionStart?: () => void | Promise }): Promise { const previousFlagValues = this._extensionRunner.getFlagValues(); await emitSessionShutdownEvent(this._extensionRunner, { type: "session_shutdown", reason: "reload" }); await this.settingsManager.reload(); @@ -2462,6 +2462,7 @@ export class AgentSession { this._extensionShutdownHandler || this._extensionErrorListener; if (hasBindings) { + await options?.beforeSessionStart?.(); await this._extensionRunner.emit({ type: "session_start", reason: "reload" }); await this.extendResourcesFromExtensions("reload"); } diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 613d588c..100feb6b 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -395,7 +395,7 @@ export class InteractiveMode { this.resetExtensionUI(); }); this.runtimeHost.setRebindSession(async () => { - await this.rebindCurrentSession(); + await this.rebindCurrentSession({ renderBeforeBind: true }); }); this.version = VERSION; this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor()); @@ -1543,12 +1543,7 @@ export class InteractiveMode { } this.statusContainer.clear(); try { - const result = await this.runtimeHost.newSession(options); - if (!result.cancelled) { - this.renderCurrentSessionState(); - this.ui.requestRender(); - } - return result; + return await this.runtimeHost.newSession(options); } catch (error: unknown) { return this.handleFatalRuntimeError("Failed to create session", error); } @@ -1557,7 +1552,6 @@ export class InteractiveMode { try { const result = await this.runtimeHost.fork(entryId, options); if (!result.cancelled) { - this.renderCurrentSessionState(); this.editor.setText(result.selectedText ?? ""); this.showStatus("Forked to new session"); } @@ -1631,12 +1625,18 @@ export class InteractiveMode { } } - private async rebindCurrentSession(): Promise { + private async rebindCurrentSession(options: { renderBeforeBind?: boolean } = {}): Promise { this.unsubscribe?.(); this.unsubscribe = undefined; this.applyRuntimeSettings(); - await this.bindCurrentSessionExtensions(); - this.subscribeToAgent(); + if (options.renderBeforeBind) { + this.renderCurrentSessionState(); + this.subscribeToAgent(); + await this.bindCurrentSessionExtensions(); + } else { + await this.bindCurrentSessionExtensions(); + this.subscribeToAgent(); + } await this.updateAvailableProviderCount(); this.updateEditorBorderColor(); this.updateTerminalTitle(); @@ -4376,7 +4376,6 @@ export class InteractiveMode { return; } - this.renderCurrentSessionState(); this.editor.setText(result.selectedText ?? ""); done(); this.showStatus("Forked to new session"); @@ -4409,7 +4408,6 @@ export class InteractiveMode { return; } - this.renderCurrentSessionState(); this.editor.setText(""); this.showStatus("Cloned to new session"); } catch (error: unknown) { @@ -4601,7 +4599,6 @@ export class InteractiveMode { if (result.cancelled) { return result; } - this.renderCurrentSessionState(); this.showStatus("Resumed session"); return result; } catch (error: unknown) { @@ -4619,7 +4616,6 @@ export class InteractiveMode { if (result.cancelled) { return result; } - this.renderCurrentSessionState(); this.showStatus("Resumed session in current cwd"); return result; } @@ -5072,8 +5068,20 @@ export class InteractiveMode { this.ui.requestRender(); }; + let chatRestoredBeforeSessionStart = false; + let reloadBoxDismissed = false; + const restoreChatBeforeSessionStart = () => { + if (chatRestoredBeforeSessionStart) { + return; + } + this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock(); + this.rebuildChatFromMessages(); + chatRestoredBeforeSessionStart = true; + }; + try { - await this.session.reload(); + await this.session.reload({ beforeSessionStart: restoreChatBeforeSessionStart }); + restoreChatBeforeSessionStart(); configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs()); this.keybindings.reload(); const activeHeader = this.customHeader ?? this.builtInHeader; @@ -5081,7 +5089,6 @@ export class InteractiveMode { activeHeader.setExpanded(this.toolOutputExpanded); } setRegisteredThemes(this.session.resourceLoader.getThemes().themes); - this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock(); await this.themeController.applyFromSettings(); const editorPaddingX = this.settingsManager.getEditorPaddingX(); const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible(); @@ -5096,8 +5103,6 @@ export class InteractiveMode { this.setupAutocompleteProvider(); const runner = this.session.extensionRunner; this.setupExtensionShortcuts(runner); - this.rebuildChatFromMessages(); - dismissReloadBox(this.editor as Component); this.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true, @@ -5112,8 +5117,12 @@ export class InteractiveMode { ? "Reloaded keybindings, extensions, skills, prompts, themes; saved project trust" : "Reloaded keybindings, extensions, skills, prompts, themes", ); + dismissReloadBox(this.editor as Component); + reloadBoxDismissed = true; } catch (error) { - dismissReloadBox(previousEditor as Component); + if (!reloadBoxDismissed) { + dismissReloadBox(previousEditor as Component); + } this.showError(`Reload failed: ${error instanceof Error ? error.message : String(error)}`); } } @@ -5187,7 +5196,6 @@ export class InteractiveMode { this.showStatus("Import cancelled"); return; } - this.renderCurrentSessionState(); this.showStatus(`Session imported from: ${inputPath}`); } catch (error: unknown) { if (error instanceof MissingSessionCwdError) { @@ -5201,7 +5209,6 @@ export class InteractiveMode { this.showStatus("Import cancelled"); return; } - this.renderCurrentSessionState(); this.showStatus(`Session imported from: ${inputPath}`); return; } @@ -5540,7 +5547,6 @@ export class InteractiveMode { if (result.cancelled) { return; } - this.renderCurrentSessionState(); this.chatContainer.addChild(new Spacer(1)); this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1)); this.ui.requestRender(); diff --git a/packages/coding-agent/test/interactive-mode-clone-command.test.ts b/packages/coding-agent/test/interactive-mode-clone-command.test.ts index fea69c5d..69864146 100644 --- a/packages/coding-agent/test/interactive-mode-clone-command.test.ts +++ b/packages/coding-agent/test/interactive-mode-clone-command.test.ts @@ -41,7 +41,7 @@ describe("InteractiveMode /clone", () => { await interactiveModePrototype.handleCloneCommand.call(context); expect(fork).toHaveBeenCalledWith("leaf-123", { position: "at" }); - expect(renderCurrentSessionState).toHaveBeenCalled(); + expect(renderCurrentSessionState).not.toHaveBeenCalled(); expect(setText).toHaveBeenCalledWith(""); expect(showStatus).toHaveBeenCalledWith("Cloned to new session"); expect(showError).not.toHaveBeenCalled(); diff --git a/packages/coding-agent/test/suite/regressions/5943-session-start-notify.test.ts b/packages/coding-agent/test/suite/regressions/5943-session-start-notify.test.ts new file mode 100644 index 00000000..db7ce26d --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/5943-session-start-notify.test.ts @@ -0,0 +1,430 @@ +import { fauxAssistantMessage } from "@earendil-works/pi-ai"; +import { describe, expect, it, vi } from "vitest"; +import type { AgentSessionEvent } from "../../../src/core/agent-session.ts"; +import type { ExtensionUIContext } from "../../../src/core/extensions/index.ts"; +import { InteractiveMode } from "../../../src/modes/interactive/interactive-mode.ts"; +import { initTheme, type Theme, theme } from "../../../src/modes/interactive/theme/theme.ts"; +import { createHarness } from "../harness.ts"; + +function createUiContext( + onNotify: (message: string, type: "info" | "warning" | "error" | undefined) => void, +): ExtensionUIContext { + return { + select: async () => undefined, + confirm: async () => false, + input: async () => undefined, + notify: onNotify, + onTerminalInput: () => () => {}, + setStatus: () => {}, + setWorkingMessage: () => {}, + setWorkingVisible: () => {}, + setWorkingIndicator: () => {}, + setHiddenThinkingLabel: () => {}, + setWidget: () => {}, + setFooter: () => {}, + setHeader: () => {}, + setTitle: () => {}, + custom: async () => undefined as T, + pasteToEditor: () => {}, + setEditorText: () => {}, + getEditorText: () => "", + editor: async () => undefined, + addAutocompleteProvider: () => {}, + setEditorComponent: () => {}, + getEditorComponent: () => undefined, + get theme() { + return theme; + }, + getAllThemes: () => [], + getTheme: () => undefined, + setTheme: (_theme: string | Theme) => ({ success: false, error: "Theme switching not available in tests" }), + getToolsExpanded: () => false, + setToolsExpanded: () => {}, + }; +} + +type RebindContext = { + unsubscribe?: () => void; + applyRuntimeSettings: () => void; + renderCurrentSessionState: () => void; + bindCurrentSessionExtensions: () => Promise; + subscribeToAgent: () => void; + updateAvailableProviderCount: () => Promise; + updateEditorBorderColor: () => void; + updateTerminalTitle: () => void; +}; + +type ReloadCommandContext = { + hideThinkingBlock: boolean; + session: { + isStreaming: boolean; + isCompacting: boolean; + reload: (options?: { beforeSessionStart?: () => void | Promise }) => Promise; + resourceLoader: { getThemes: () => { themes: [] } }; + extensionRunner: unknown; + modelRegistry: { getError: () => string | undefined }; + }; + settingsManager: { + getHttpIdleTimeoutMs: () => number; + getHideThinkingBlock: () => boolean; + getEditorPaddingX: () => number; + getAutocompleteMaxVisible: () => number; + getShowHardwareCursor: () => boolean; + getClearOnShrink: () => boolean; + }; + keybindings: { reload: () => void }; + customHeader?: unknown; + builtInHeader?: unknown; + editorContainer: { clear: () => void; addChild: (component: unknown) => void }; + ui: { + setFocus: (component: unknown) => void; + requestRender: (force?: boolean) => void; + setShowHardwareCursor: (enabled: boolean) => void; + setClearOnShrink: (enabled: boolean) => void; + }; + editor: unknown; + defaultEditor: { setPaddingX: (padding: number) => void; setAutocompleteMaxVisible: (maxVisible: number) => void }; + themeController: { applyFromSettings: () => Promise }; + resetExtensionUI: () => void; + rebuildChatFromMessages: () => void; + setupAutocompleteProvider: () => void; + setupExtensionShortcuts: (runner: unknown) => void; + showLoadedResources: (options: unknown) => void; + maybeSaveImplicitProjectTrustAfterReload: () => boolean; + showStatus: (message: string) => void; + showWarning: (message: string) => void; + showError: (message: string) => void; +}; + +type InteractiveModePrototype = { + rebindCurrentSession(this: RebindContext, options?: { renderBeforeBind?: boolean }): Promise; + handleReloadCommand(this: ReloadCommandContext): Promise; +}; + +const interactiveModePrototype = InteractiveMode.prototype as unknown as InteractiveModePrototype; + +type ReloadCommandContextOverrides = Omit< + Partial, + "session" | "settingsManager" | "keybindings" | "editorContainer" | "ui" | "defaultEditor" | "themeController" +> & { + session?: Partial; + settingsManager?: Partial; + keybindings?: Partial; + editorContainer?: Partial; + ui?: Partial; + defaultEditor?: Partial; + themeController?: Partial; +}; + +function createReloadCommandContext(overrides: ReloadCommandContextOverrides = {}): ReloadCommandContext { + const editor = overrides.editor ?? {}; + return { + hideThinkingBlock: overrides.hideThinkingBlock ?? false, + session: { + isStreaming: false, + isCompacting: false, + reload: async (options) => { + await options?.beforeSessionStart?.(); + }, + resourceLoader: { getThemes: () => ({ themes: [] }) }, + extensionRunner: {}, + modelRegistry: { getError: () => undefined }, + ...overrides.session, + }, + settingsManager: { + getHttpIdleTimeoutMs: () => 0, + getHideThinkingBlock: () => false, + getEditorPaddingX: () => 1, + getAutocompleteMaxVisible: () => 10, + getShowHardwareCursor: () => false, + getClearOnShrink: () => false, + ...overrides.settingsManager, + }, + keybindings: { reload: () => {}, ...overrides.keybindings }, + editorContainer: { clear: () => {}, addChild: () => {}, ...overrides.editorContainer }, + ui: { + setFocus: () => {}, + requestRender: () => {}, + setShowHardwareCursor: () => {}, + setClearOnShrink: () => {}, + ...overrides.ui, + }, + editor, + defaultEditor: { setPaddingX: () => {}, setAutocompleteMaxVisible: () => {}, ...overrides.defaultEditor }, + themeController: { applyFromSettings: async () => {}, ...overrides.themeController }, + customHeader: overrides.customHeader, + builtInHeader: overrides.builtInHeader, + resetExtensionUI: overrides.resetExtensionUI ?? (() => {}), + rebuildChatFromMessages: overrides.rebuildChatFromMessages ?? (() => {}), + setupAutocompleteProvider: overrides.setupAutocompleteProvider ?? (() => {}), + setupExtensionShortcuts: overrides.setupExtensionShortcuts ?? (() => {}), + showLoadedResources: overrides.showLoadedResources ?? (() => {}), + maybeSaveImplicitProjectTrustAfterReload: overrides.maybeSaveImplicitProjectTrustAfterReload ?? (() => false), + showStatus: overrides.showStatus ?? (() => {}), + showWarning: overrides.showWarning ?? (() => {}), + showError: overrides.showError ?? (() => {}), + }; +} + +type MessageEvent = Extract; + +function getMessageText(event: MessageEvent): string { + const message = event.message; + if (!("content" in message)) { + return ""; + } + const content = message.content; + if (typeof content === "string") { + return content; + } + return content + .filter((part): part is { type: "text"; text: string } => part.type === "text") + .map((part) => part.text) + .join(""); +} + +describe("regression #5943: session_start transient UI", () => { + it("renders replacement session state before session_start handlers can notify", async () => { + const events: string[] = []; + const harness = await createHarness({ + extensionFactories: [ + (pi) => { + pi.on("session_start", (_event, ctx) => { + ctx.ui.notify("Hello Error", "error"); + }); + }, + ], + }); + + try { + const context: RebindContext = { + applyRuntimeSettings: () => events.push("apply"), + renderCurrentSessionState: () => events.push("render"), + bindCurrentSessionExtensions: async () => { + events.push("bind"); + await harness.session.bindExtensions({ + uiContext: createUiContext((message) => events.push(`notify:${message}`)), + mode: "tui", + }); + }, + subscribeToAgent: () => events.push("subscribe"), + updateAvailableProviderCount: async () => {}, + updateEditorBorderColor: () => {}, + updateTerminalTitle: () => {}, + }; + + await interactiveModePrototype.rebindCurrentSession.call(context, { renderBeforeBind: true }); + + expect(events).toEqual(["apply", "render", "subscribe", "bind", "notify:Hello Error"]); + } finally { + harness.cleanup(); + } + }); + + it("subscribes before replacement session_start handlers send messages", async () => { + const events: string[] = []; + const harness = await createHarness({ + extensionFactories: [ + (pi) => { + pi.on("session_start", () => { + pi.sendMessage({ + customType: "session-start", + content: "custom from start", + display: true, + }); + }); + }, + ], + }); + + try { + const context: RebindContext = { + applyRuntimeSettings: () => {}, + renderCurrentSessionState: () => events.push("render"), + bindCurrentSessionExtensions: async () => { + events.push("bind"); + await harness.session.bindExtensions({ + uiContext: createUiContext(() => {}), + mode: "tui", + }); + }, + subscribeToAgent: () => { + events.push("subscribe"); + harness.session.subscribe((event) => { + if (event.type !== "message_start" && event.type !== "message_end") { + return; + } + events.push(`${event.type}:${event.message.role}:${getMessageText(event)}`); + }); + }, + updateAvailableProviderCount: async () => {}, + updateEditorBorderColor: () => {}, + updateTerminalTitle: () => {}, + }; + + await interactiveModePrototype.rebindCurrentSession.call(context, { renderBeforeBind: true }); + + expect(events).toEqual([ + "render", + "subscribe", + "bind", + "message_start:custom:custom from start", + "message_end:custom:custom from start", + ]); + } finally { + harness.cleanup(); + } + }); + + it("subscribes before replacement session_start handlers send user messages", async () => { + const events: string[] = []; + const harness = await createHarness({ + extensionFactories: [ + (pi) => { + pi.on("session_start", () => { + pi.sendUserMessage("user from start"); + }); + }, + ], + }); + harness.setResponses([fauxAssistantMessage("assistant from start")]); + + try { + const context: RebindContext = { + applyRuntimeSettings: () => {}, + renderCurrentSessionState: () => events.push("render"), + bindCurrentSessionExtensions: async () => { + events.push("bind"); + await harness.session.bindExtensions({ + uiContext: createUiContext(() => {}), + mode: "tui", + }); + }, + subscribeToAgent: () => { + events.push("subscribe"); + harness.session.subscribe((event) => { + if (event.type !== "message_start" && event.type !== "message_end") { + return; + } + events.push(`${event.type}:${event.message.role}:${getMessageText(event)}`); + }); + }, + updateAvailableProviderCount: async () => {}, + updateEditorBorderColor: () => {}, + updateTerminalTitle: () => {}, + }; + + await interactiveModePrototype.rebindCurrentSession.call(context, { renderBeforeBind: true }); + await harness.session.agent.waitForIdle(); + + expect(events.slice(0, 3)).toEqual(["render", "subscribe", "bind"]); + expect(events).toContain("message_start:user:user from start"); + expect(events).toContain("message_end:user:user from start"); + expect(events).toContain("message_end:assistant:assistant from start"); + } finally { + harness.cleanup(); + } + }); + + it("runs the reload render hook before reload session_start handlers can notify", async () => { + const events: string[] = []; + const beforeSessionStart = vi.fn(() => { + events.push("render"); + }); + const harness = await createHarness({ + extensionFactories: [ + (pi) => { + pi.on("session_start", (event, ctx) => { + events.push(`start:${event.reason}`); + ctx.ui.notify(`notify:${event.reason}`, "error"); + }); + }, + ], + }); + + try { + await harness.session.bindExtensions({ + uiContext: createUiContext((message) => events.push(message)), + mode: "tui", + }); + expect(events).toEqual(["start:startup", "notify:startup"]); + + events.length = 0; + await harness.session.reload({ beforeSessionStart }); + + expect(beforeSessionStart).toHaveBeenCalledTimes(1); + expect(events).toEqual(["render", "start:reload", "notify:reload"]); + } finally { + harness.cleanup(); + } + }); + + it("refreshes hideThinkingBlock before rebuilding chat during reload", async () => { + initTheme("dark", false); + const events: string[] = []; + let context: ReloadCommandContext; + context = createReloadCommandContext({ + settingsManager: { getHideThinkingBlock: () => true }, + session: { + reload: async (options) => { + events.push("reload"); + await options?.beforeSessionStart?.(); + events.push(`start:${context.hideThinkingBlock}`); + }, + }, + rebuildChatFromMessages: () => { + events.push(`rebuild:${context.hideThinkingBlock}`); + }, + }); + + await interactiveModePrototype.handleReloadCommand.call(context); + + expect(context.hideThinkingBlock).toBe(true); + expect(events).toEqual(["reload", "rebuild:true", "start:true"]); + }); + + it("keeps the reload blocker focused until async reload completes", async () => { + initTheme("dark", false); + const editor = {}; + let focused: unknown; + let chatRestored = false; + let markReloadWaiting!: () => void; + let finishReload!: () => void; + const reloadWaiting = new Promise((resolve) => { + markReloadWaiting = resolve; + }); + const reloadFinished = new Promise((resolve) => { + finishReload = resolve; + }); + + const context = createReloadCommandContext({ + editor, + session: { + reload: async (options) => { + await options?.beforeSessionStart?.(); + markReloadWaiting(); + await reloadFinished; + }, + }, + ui: { + setFocus: (component) => { + focused = component; + }, + }, + rebuildChatFromMessages: () => { + chatRestored = true; + }, + }); + + const reloadPromise = interactiveModePrototype.handleReloadCommand.call(context); + await reloadWaiting; + + expect(chatRestored).toBe(true); + expect(focused).not.toBe(editor); + + finishReload(); + await reloadPromise; + + expect(focused).toBe(editor); + }); +}); From a61137a661e0d9549272e6f89aa978363fc7df66 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 22 Jun 2026 08:41:29 +0200 Subject: [PATCH 065/122] docs(coding-agent): fix plan-mode docs links closes #5957 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/docs/tui.md | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 341921f2..2610e24c 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed broken TUI documentation links to the plan-mode extension example ([#5957](https://github.com/earendil-works/pi/issues/5957)). - Fixed transient extension UI and session-start messages emitted during session replacement or reload so they remain visible, and kept reload input blocked until reload completes ([#5943](https://github.com/earendil-works/pi/issues/5943)). - Fixed the plan-mode example to preserve active custom tools, skip the action prompt when no plan is found, and queue refinement/execution follow-ups correctly from `agent_end` ([#5940](https://github.com/earendil-works/pi/issues/5940)). - Fixed `pi update` to install the exact version returned by the Pi update check, make `--force` reinstall that checked version, fail instead of falling back to an unversioned reinstall when no version is available, and report both the old and updated versions. diff --git a/packages/coding-agent/docs/tui.md b/packages/coding-agent/docs/tui.md index c8ff3554..38ef1986 100644 --- a/packages/coding-agent/docs/tui.md +++ b/packages/coding-agent/docs/tui.md @@ -742,7 +742,7 @@ ctx.ui.setStatus("my-ext", ctx.ui.theme.fg("accent", "● active")); ctx.ui.setStatus("my-ext", undefined); ``` -**Examples:** [status-line.ts](../examples/extensions/status-line.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts) +**Examples:** [status-line.ts](../examples/extensions/status-line.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts) ### Pattern 4b: Working Indicator Customization @@ -802,7 +802,7 @@ ctx.ui.setWidget("my-widget", (_tui, theme) => { ctx.ui.setWidget("my-widget", undefined); ``` -**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts) +**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts) ### Pattern 6: Custom Footer @@ -919,7 +919,7 @@ export default function (pi: ExtensionAPI) { - **Selection UI**: [examples/extensions/preset.ts](../examples/extensions/preset.ts) - SelectList with DynamicBorder framing - **Async with cancel**: [examples/extensions/qna.ts](../examples/extensions/qna.ts) - BorderedLoader for LLM calls - **Settings toggles**: [examples/extensions/tools.ts](../examples/extensions/tools.ts) - SettingsList for tool enable/disable -- **Status indicators**: [examples/extensions/plan-mode.ts](../examples/extensions/plan-mode.ts) - setStatus and setWidget +- **Status indicators**: [examples/extensions/plan-mode/index.ts](../examples/extensions/plan-mode/index.ts) - setStatus and setWidget - **Working indicator**: [examples/extensions/working-indicator.ts](../examples/extensions/working-indicator.ts) - setWorkingIndicator - **Custom footer**: [examples/extensions/custom-footer.ts](../examples/extensions/custom-footer.ts) - setFooter with stats - **Custom editor**: [examples/extensions/modal-editor.ts](../examples/extensions/modal-editor.ts) - Vim-like modal editing From 084574048bade4cd7202e1a7d45f89e77e06ebd8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 22 Jun 2026 07:44:05 +0000 Subject: [PATCH 066/122] chore: approve contributor any-victor --- .github/APPROVED_CONTRIBUTORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/APPROVED_CONTRIBUTORS b/.github/APPROVED_CONTRIBUTORS index a8db269c..7695e96f 100644 --- a/.github/APPROVED_CONTRIBUTORS +++ b/.github/APPROVED_CONTRIBUTORS @@ -241,3 +241,5 @@ dangooddd pr Mearman pr dodiego pr + +any-victor pr From 7d0497fdb752a05a9327864951ae7d6fcc4a9617 Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Mon, 22 Jun 2026 10:14:59 +0200 Subject: [PATCH 067/122] fix(ai): preserve early reasoning details closes #5114 --- packages/ai/CHANGELOG.md | 4 + .../ai/src/providers/openai-completions.ts | 47 +++++-- ...enai-completions-reasoning-details.test.ts | 118 ++++++++++++++++++ 3 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 packages/ai/test/openai-completions-reasoning-details.test.ts diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 586260cf..828e3433 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed OpenAI-compatible streaming to preserve encrypted `reasoning_details` that arrive before matching tool call deltas ([#5114](https://github.com/earendil-works/pi/issues/5114)). + ## [0.79.9] - 2026-06-20 ### Added diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 10c8e7e6..b257f96e 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -78,6 +78,20 @@ function isImageContentBlock(block: { type: string }): block is ImageContent { return block.type === "image"; } +function isEncryptedReasoningDetail(detail: unknown): detail is OpenAIEncryptedReasoningDetail { + if (typeof detail !== "object" || detail === null) { + return false; + } + const candidate = detail as Record; + return ( + candidate.type === "reasoning.encrypted" && + typeof candidate.id === "string" && + candidate.id.length > 0 && + typeof candidate.data === "string" && + candidate.data.length > 0 + ); +} + export interface OpenAICompletionsOptions extends StreamOptions { toolChoice?: "auto" | "none" | "required" | { type: "function"; function: { name: string } }; reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh"; @@ -96,6 +110,12 @@ type ResolvedChatTemplateKwargValue = string | number | boolean | null; type ChatCompletionInstructionMessageParam = ChatCompletionDeveloperMessageParam | ChatCompletionSystemMessageParam; +type OpenAIEncryptedReasoningDetail = { + type: "reasoning.encrypted"; + id: string; + data: string; +}; + type ChatCompletionTextPartWithCacheControl = ChatCompletionContentPartText & { cache_control?: OpenAICompatCacheControl; }; @@ -177,6 +197,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA let hasFinishReason = false; const toolCallBlocksByIndex = new Map(); const toolCallBlocksById = new Map(); + const pendingReasoningDetailsByToolCallId = new Map(); const blocks = output.content as StreamingBlock[]; const getContentIndex = (block: StreamingBlock) => blocks.indexOf(block); const finishBlock = (block: StreamingBlock) => { @@ -232,6 +253,16 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA } return thinkingBlock; }; + const applyPendingReasoningDetail = (block: StreamingToolCallBlock) => { + if (!block.id) { + return; + } + const pendingReasoningDetail = pendingReasoningDetailsByToolCallId.get(block.id); + if (pendingReasoningDetail) { + block.thoughtSignature = pendingReasoningDetail; + pendingReasoningDetailsByToolCallId.delete(block.id); + } + }; const ensureToolCallBlock = (toolCall: StreamingToolCallDelta) => { const streamIndex = typeof toolCall.index === "number" ? toolCall.index : undefined; let block = streamIndex !== undefined ? toolCallBlocksByIndex.get(streamIndex) : undefined; @@ -267,6 +298,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA if (toolCall.id) { toolCallBlocksById.set(toolCall.id, block); } + applyPendingReasoningDetail(block); return block; }; @@ -376,15 +408,16 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA } } - const reasoningDetails = (choice.delta as any).reasoning_details; - if (reasoningDetails && Array.isArray(reasoningDetails)) { + const reasoningDetails = (choice.delta as { reasoning_details?: unknown }).reasoning_details; + if (Array.isArray(reasoningDetails)) { for (const detail of reasoningDetails) { - if (detail.type === "reasoning.encrypted" && detail.id && detail.data) { - const matchingToolCall = output.content.find( - (b) => b.type === "toolCall" && b.id === detail.id, - ) as ToolCall | undefined; + if (isEncryptedReasoningDetail(detail)) { + const serializedDetail = JSON.stringify(detail); + const matchingToolCall = toolCallBlocksById.get(detail.id); if (matchingToolCall) { - matchingToolCall.thoughtSignature = JSON.stringify(detail); + matchingToolCall.thoughtSignature = serializedDetail; + } else { + pendingReasoningDetailsByToolCallId.set(detail.id, serializedDetail); } } } diff --git a/packages/ai/test/openai-completions-reasoning-details.test.ts b/packages/ai/test/openai-completions-reasoning-details.test.ts new file mode 100644 index 00000000..c07b1acf --- /dev/null +++ b/packages/ai/test/openai-completions-reasoning-details.test.ts @@ -0,0 +1,118 @@ +import { Type } from "typebox"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { streamOpenAICompletions } from "../src/providers/openai-completions.ts"; +import type { AssistantMessage, Model, Tool } from "../src/types.ts"; + +const mockState = vi.hoisted(() => ({ + chunkSets: [] as unknown[][], + payloads: [] as unknown[], +})); + +vi.mock("openai", () => { + class FakeOpenAI { + chat = { + completions: { + create: (payload: unknown) => { + mockState.payloads.push(payload); + const chunks = mockState.chunkSets.shift() ?? []; + const stream = { + async *[Symbol.asyncIterator]() { + for (const chunk of chunks) { + yield chunk; + } + }, + }; + const result = Promise.resolve(stream) as Promise & { + withResponse: () => Promise<{ data: typeof stream; response: { status: number; headers: Headers } }>; + }; + result.withResponse = async () => ({ + data: stream, + response: { status: 200, headers: new Headers() }, + }); + return result; + }, + }, + }; + } + return { default: FakeOpenAI }; +}); + +const reasoningDetail = { type: "reasoning.encrypted", id: "call_1", data: "encrypted-signature" }; +const readTool: Tool = { + name: "read", + description: "Read a file", + parameters: Type.Object({ path: Type.String() }), +}; + +function model(): Model<"openai-completions"> { + return { + id: "google/gemini-test", + name: "Gemini Test", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 100_000, + maxTokens: 4096, + }; +} + +function chunk(delta: Record, finishReason: string | null = null): unknown { + return { + id: "chatcmpl-test", + model: "google/gemini-test", + choices: [{ index: 0, delta, finish_reason: finishReason }], + }; +} + +function toolCallChunk(): unknown { + return chunk({ + tool_calls: [ + { + index: 0, + id: "call_1", + type: "function", + function: { name: "read", arguments: '{"path":"README.md"}' }, + }, + ], + }); +} + +async function runOpenAICompletionsStream(messages: AssistantMessage[] = []): Promise { + return await streamOpenAICompletions(model(), { messages, tools: [readTool] }, { apiKey: "test" }).result(); +} + +function getAssistantPayload(payload: unknown): { reasoning_details?: unknown } | undefined { + const messages = (payload as { messages?: Array<{ role?: string; reasoning_details?: unknown }> }).messages ?? []; + return messages.find((message) => message.role === "assistant"); +} + +describe("openai-completions reasoning_details streaming", () => { + beforeEach(() => { + mockState.chunkSets = []; + mockState.payloads = []; + }); + + it("preserves reasoning_details that arrive before their matching tool call", async () => { + mockState.chunkSets = [ + [chunk({ reasoning_details: [reasoningDetail] }), toolCallChunk(), chunk({}, "tool_calls")], + [chunk({ content: "ok" }), chunk({}, "stop")], + ]; + + const assistantMessage = await runOpenAICompletionsStream(); + const toolCall = assistantMessage.content.find((block) => block.type === "toolCall"); + expect(toolCall).toMatchObject({ + type: "toolCall", + id: "call_1", + name: "read", + arguments: { path: "README.md" }, + thoughtSignature: JSON.stringify(reasoningDetail), + }); + + await runOpenAICompletionsStream([assistantMessage]); + + expect(getAssistantPayload(mockState.payloads[1])?.reasoning_details).toEqual([reasoningDetail]); + }); +}); From 5b9b70d29cdfa3e71b45c26698c2226c21d762f9 Mon Sep 17 00:00:00 2001 From: Enrico <5575513+PizzaMarinara@users.noreply.github.com> Date: Mon, 22 Jun 2026 10:36:16 +0200 Subject: [PATCH 068/122] feat(coding-agent): add compaction reason and willRetry to extension compact events (#5962) session_before_compact and session_compact now carry reason ("manual" | "threshold" | "overflow") and willRetry, matching what the RPC protocol already exposes via auto_compaction_start/end. Extensions can now distinguish manual /compact from threshold auto-compaction and overflow recovery through the public API instead of monkeypatching AgentSession internals. --- packages/coding-agent/docs/compaction.md | 4 +- packages/coding-agent/docs/extensions.md | 7 +- .../coding-agent/src/core/agent-session.ts | 8 ++ .../coding-agent/src/core/extensions/types.ts | 8 ++ .../5217-compaction-reason.test.ts | 95 +++++++++++++++++++ 5 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 packages/coding-agent/test/suite/regressions/5217-compaction-reason.test.ts diff --git a/packages/coding-agent/docs/compaction.md b/packages/coding-agent/docs/compaction.md index babcddc6..5e0d4eef 100644 --- a/packages/coding-agent/docs/compaction.md +++ b/packages/coding-agent/docs/compaction.md @@ -276,7 +276,7 @@ Fired before auto-compaction or `/compact`. Can cancel or provide custom summary ```typescript pi.on("session_before_compact", async (event, ctx) => { - const { preparation, branchEntries, customInstructions, signal } = event; + const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event; // preparation.messagesToSummarize - messages to summarize // preparation.turnPrefixMessages - split turn prefix (if isSplitTurn) @@ -287,6 +287,8 @@ pi.on("session_before_compact", async (event, ctx) => { // preparation.settings - compaction settings // branchEntries - all entries on current branch (for custom state) + // reason - "manual" (/compact), "threshold", or "overflow" + // willRetry - whether the aborted turn is retried after compaction (overflow recovery) // signal - AbortSignal (pass to LLM calls) // Cancel: diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index 5a559813..a9271ee8 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -437,7 +437,10 @@ Fired on compaction. See [compaction.md](compaction.md) for details. ```typescript pi.on("session_before_compact", async (event, ctx) => { - const { preparation, branchEntries, customInstructions, signal } = event; + const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event; + + // reason - "manual" (/compact), "threshold", or "overflow" + // willRetry - whether the aborted turn is retried after compaction (overflow recovery) // Cancel: return { cancel: true }; @@ -455,6 +458,8 @@ pi.on("session_before_compact", async (event, ctx) => { pi.on("session_compact", async (event, ctx) => { // event.compactionEntry - the saved compaction // event.fromExtension - whether extension provided it + // event.reason - "manual" (/compact), "threshold", or "overflow" + // event.willRetry - whether the aborted turn is retried after compaction (overflow recovery) }); ``` diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index a1033128..395d8161 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -1684,6 +1684,8 @@ export class AgentSession { preparation, branchEntries: pathEntries, customInstructions, + reason: "manual", + willRetry: false, signal: this._compactionAbortController.signal, })) as SessionBeforeCompactResult | undefined; @@ -1747,6 +1749,8 @@ export class AgentSession { type: "session_compact", compactionEntry: savedCompactionEntry, fromExtension, + reason: "manual", + willRetry: false, }); } @@ -1945,6 +1949,8 @@ export class AgentSession { preparation, branchEntries: pathEntries, customInstructions: undefined, + reason, + willRetry, signal: this._autoCompactionAbortController.signal, })) as SessionBeforeCompactResult | undefined; @@ -2022,6 +2028,8 @@ export class AgentSession { type: "session_compact", compactionEntry: savedCompactionEntry, fromExtension, + reason, + willRetry, }); } diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index a869a55d..7234d4e4 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -571,6 +571,10 @@ export interface SessionBeforeCompactEvent { preparation: CompactionPreparation; branchEntries: SessionEntry[]; customInstructions?: string; + /** What triggered the compaction: manual /compact, the context threshold, or context overflow recovery */ + reason: "manual" | "threshold" | "overflow"; + /** True when the aborted turn is retried after this compaction (overflow recovery) */ + willRetry: boolean; signal: AbortSignal; } @@ -579,6 +583,10 @@ export interface SessionCompactEvent { type: "session_compact"; compactionEntry: CompactionEntry; fromExtension: boolean; + /** What triggered the compaction: manual /compact, the context threshold, or context overflow recovery */ + reason: "manual" | "threshold" | "overflow"; + /** True when the aborted turn is retried after this compaction (overflow recovery) */ + willRetry: boolean; } /** Fired before an extension runtime is torn down due to quit, reload, or session replacement. */ diff --git a/packages/coding-agent/test/suite/regressions/5217-compaction-reason.test.ts b/packages/coding-agent/test/suite/regressions/5217-compaction-reason.test.ts new file mode 100644 index 00000000..9af206fe --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/5217-compaction-reason.test.ts @@ -0,0 +1,95 @@ +import { fauxAssistantMessage } from "@earendil-works/pi-ai"; +import { afterEach, describe, expect, it } from "vitest"; +import type { ExtensionFactory } from "../../../src/index.ts"; +import { createHarness, type Harness } from "../harness.ts"; + +type SessionWithCompactionInternals = { + _runAutoCompaction: (reason: "overflow" | "threshold", willRetry: boolean) => Promise; +}; + +interface RecordedCompactionEvent { + type: "session_before_compact" | "session_compact"; + reason: "manual" | "threshold" | "overflow"; + willRetry: boolean; +} + +function recordingExtension(recorded: RecordedCompactionEvent[]): ExtensionFactory { + return (pi) => { + pi.on("session_before_compact", async (event) => { + recorded.push({ type: event.type, reason: event.reason, willRetry: event.willRetry }); + return { + compaction: { + summary: "summary from extension", + firstKeptEntryId: event.preparation.firstKeptEntryId, + tokensBefore: event.preparation.tokensBefore, + details: {}, + }, + }; + }); + pi.on("session_compact", async (event) => { + recorded.push({ type: event.type, reason: event.reason, willRetry: event.willRetry }); + }); + }; +} + +async function createCompactionHarness(recorded: RecordedCompactionEvent[]): Promise { + const harness = await createHarness({ + settings: { compaction: { keepRecentTokens: 1 } }, + extensionFactories: [recordingExtension(recorded)], + }); + harness.setResponses([fauxAssistantMessage("one"), fauxAssistantMessage("two")]); + await harness.session.prompt("first"); + await harness.session.prompt("second"); + return harness; +} + +describe("issue #5217 compaction reason on extension events", () => { + const harnesses: Harness[] = []; + + afterEach(() => { + while (harnesses.length > 0) { + harnesses.pop()?.cleanup(); + } + }); + + it("reports manual reason for compact()", async () => { + const recorded: RecordedCompactionEvent[] = []; + const harness = await createCompactionHarness(recorded); + harnesses.push(harness); + + await harness.session.compact(); + + expect(recorded).toEqual([ + { type: "session_before_compact", reason: "manual", willRetry: false }, + { type: "session_compact", reason: "manual", willRetry: false }, + ]); + }); + + it("reports threshold reason for auto-compaction", async () => { + const recorded: RecordedCompactionEvent[] = []; + const harness = await createCompactionHarness(recorded); + harnesses.push(harness); + const sessionInternals = harness.session as unknown as SessionWithCompactionInternals; + + await sessionInternals._runAutoCompaction("threshold", false); + + expect(recorded).toEqual([ + { type: "session_before_compact", reason: "threshold", willRetry: false }, + { type: "session_compact", reason: "threshold", willRetry: false }, + ]); + }); + + it("reports overflow reason and willRetry for overflow recovery", async () => { + const recorded: RecordedCompactionEvent[] = []; + const harness = await createCompactionHarness(recorded); + harnesses.push(harness); + const sessionInternals = harness.session as unknown as SessionWithCompactionInternals; + + await sessionInternals._runAutoCompaction("overflow", true); + + expect(recorded).toEqual([ + { type: "session_before_compact", reason: "overflow", willRetry: true }, + { type: "session_compact", reason: "overflow", willRetry: true }, + ]); + }); +}); From b7908b49748ffae23de019b0111a7a45d828fb0b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 22 Jun 2026 10:37:20 +0200 Subject: [PATCH 069/122] docs(coding-agent): document slash command table closes #5959 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/README.md | 3 ++- packages/coding-agent/docs/usage.md | 4 +++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 2610e24c..0cc0a0e8 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed the usage docs slash command table to include `/trust` and `/import` ([#5959](https://github.com/earendil-works/pi/issues/5959)). - Fixed broken TUI documentation links to the plan-mode extension example ([#5957](https://github.com/earendil-works/pi/issues/5957)). - Fixed transient extension UI and session-start messages emitted during session replacement or reload so they remain visible, and kept reload input blocked until reload completes ([#5943](https://github.com/earendil-works/pi/issues/5943)). - Fixed the plan-mode example to preserve active custom tools, skip the action prompt when no plan is found, and queue refinement/execution follow-ups correctly from `agent_end` ([#5940](https://github.com/earendil-works/pi/issues/5940)). diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 68e17a62..5dc991cc 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -186,7 +186,8 @@ Type `/` in the editor to trigger commands. [Extensions](#extensions) can regist | `/clone` | Duplicate the current active branch into a new session | | `/compact [prompt]` | Manually compact context, optional custom instructions | | `/copy` | Copy last assistant message to clipboard | -| `/export [file]` | Export session to HTML file | +| `/export [file]` | Export session to HTML or JSONL file | +| `/import ` | Import and resume a session from a JSONL file | | `/share` | Upload as private GitHub gist with shareable HTML link | | `/reload` | Reload keybindings, extensions, skills, prompts, and context files (themes hot-reload automatically) | | `/hotkeys` | Show all keyboard shortcuts | diff --git a/packages/coding-agent/docs/usage.md b/packages/coding-agent/docs/usage.md index 5ff0cf73..ccd3ee41 100644 --- a/packages/coding-agent/docs/usage.md +++ b/packages/coding-agent/docs/usage.md @@ -44,11 +44,13 @@ Type `/` in the editor to open command completion. Extensions can register custo | `/name ` | Set session display name | | `/session` | Show session file, ID, messages, tokens, and cost | | `/tree` | Jump to any point in the session and continue from there | +| `/trust` | Save project trust decision for future sessions | | `/fork` | Create a new session from a previous user message | | `/clone` | Duplicate the current active branch into a new session | | `/compact [prompt]` | Manually compact context, optionally with custom instructions | | `/copy` | Copy last assistant message to clipboard | -| `/export [file]` | Export session to HTML | +| `/export [file]` | Export session to HTML or JSONL | +| `/import ` | Import and resume a session from a JSONL file | | `/share` | Upload as private GitHub gist with shareable HTML link | | `/reload` | Reload keybindings, extensions, skills, prompts, and context files | | `/hotkeys` | Show all keyboard shortcuts | From 5641d6bac5aa501308f7a700624e81c6cdc52209 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 22 Jun 2026 10:40:46 +0200 Subject: [PATCH 070/122] fix: clear untriaged when no-action is added --- .github/workflows/issue-triage-labels.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/issue-triage-labels.yml b/.github/workflows/issue-triage-labels.yml index ebe5500c..0e6204d5 100644 --- a/.github/workflows/issue-triage-labels.yml +++ b/.github/workflows/issue-triage-labels.yml @@ -53,6 +53,11 @@ jobs: return; } + if (context.payload.action === 'labeled' && context.payload.label?.name === NO_ACTION_LABEL) { + await removeLabelIfPresent(context.issue.number, context.payload.issue, UNTRIAGED_LABEL); + return; + } + if (context.payload.action !== 'labeled' || context.payload.label?.name !== LAST_READ_LABEL) { console.log('Not a last-read label event'); return; From 756a4e8f6f5eb2861555539145b80633e0f1d445 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 22 Jun 2026 10:57:39 +0200 Subject: [PATCH 071/122] fix(coding-agent): respect nested repo ignore boundaries in find closes #5960 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/core/tools/find.ts | 29 ++++++++++++-------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0cc0a0e8..12352557 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed the `find` tool to respect nested git repository boundaries when parent `.gitignore` rules ignore the nested repo ([#5960](https://github.com/earendil-works/pi/issues/5960)). - Fixed the usage docs slash command table to include `/trust` and `/import` ([#5959](https://github.com/earendil-works/pi/issues/5959)). - Fixed broken TUI documentation links to the plan-mode extension example ([#5957](https://github.com/earendil-works/pi/issues/5957)). - Fixed transient extension UI and session-start messages emitted during session replacement or reload so they remain visible, and kept reload input blocked until reload completes ([#5943](https://github.com/earendil-works/pi/issues/5943)). diff --git a/packages/coding-agent/src/core/tools/find.ts b/packages/coding-agent/src/core/tools/find.ts index 6f852f61..e03b728a 100644 --- a/packages/coding-agent/src/core/tools/find.ts +++ b/packages/coding-agent/src/core/tools/find.ts @@ -221,17 +221,24 @@ export function createFindToolDefinition( return; } - // Build fd arguments. --no-require-git makes fd apply hierarchical .gitignore - // semantics whether or not the search path is inside a git repository, without - // leaking sibling-directory rules the way --ignore-file (a global source) would. - const args: string[] = [ - "--glob", - "--color=never", - "--hidden", - "--no-require-git", - "--max-results", - String(effectiveLimit), - ]; + const args: string[] = ["--glob", "--color=never", "--hidden"]; + + // fd normally ignores .gitignore outside git repos, so keep --no-require-git + // there. Inside repos, use fd's default git-aware behavior so parent + // .gitignore rules stop at nested repo boundaries: + // https://github.com/earendil-works/pi/issues/5960 + let insideGitRepo = false; + for (let current = searchPath; ; ) { + if (await pathExists(path.join(current, ".git"))) { + insideGitRepo = true; + break; + } + const parent = path.dirname(current); + if (parent === current) break; + current = parent; + } + if (!insideGitRepo) args.push("--no-require-git"); + args.push("--max-results", String(effectiveLimit)); // fd --glob matches against the basename unless --full-path is set; in --full-path // mode it matches against the absolute candidate path, so a path-containing From 5df5a1ce768de6e1a2b613d148c1357a91d4e58b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 22 Jun 2026 11:11:25 +0200 Subject: [PATCH 072/122] docs(coding-agent): audit unreleased changelog --- packages/coding-agent/CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 12352557..59e4cd27 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,14 +2,25 @@ ## [Unreleased] +### New Features + +- **Extension compaction event context** - Extension `session_before_compact` and `session_compact` events now include `reason` and `willRetry`, so extensions can distinguish manual `/compact`, threshold auto-compaction, and overflow retry flows. See [session_before_compact / session_compact](docs/extensions.md#session_before_compact--session_compact) and [Custom Summarization via Extensions](docs/compaction.md#custom-summarization-via-extensions). +- **Safer update flow** - `pi update` installs the exact checked Pi version, and update notices show the changelog URL, making upgrades more predictable. See [Install and Manage](docs/packages.md#install-and-manage). + +### Added + +- Added `reason` and `willRetry` metadata to extension `session_before_compact` and `session_compact` events so extensions can distinguish manual, threshold, and overflow compaction flows ([#5962](https://github.com/earendil-works/pi/pull/5962) by [@PizzaMarinara](https://github.com/PizzaMarinara)). + ### Fixed - Fixed the `find` tool to respect nested git repository boundaries when parent `.gitignore` rules ignore the nested repo ([#5960](https://github.com/earendil-works/pi/issues/5960)). - Fixed the usage docs slash command table to include `/trust` and `/import` ([#5959](https://github.com/earendil-works/pi/issues/5959)). +- Fixed inherited OpenAI-compatible streaming to preserve encrypted `reasoning_details` that arrive before matching tool call deltas ([#5114](https://github.com/earendil-works/pi/issues/5114)). - Fixed broken TUI documentation links to the plan-mode extension example ([#5957](https://github.com/earendil-works/pi/issues/5957)). - Fixed transient extension UI and session-start messages emitted during session replacement or reload so they remain visible, and kept reload input blocked until reload completes ([#5943](https://github.com/earendil-works/pi/issues/5943)). - Fixed the plan-mode example to preserve active custom tools, skip the action prompt when no plan is found, and queue refinement/execution follow-ups correctly from `agent_end` ([#5940](https://github.com/earendil-works/pi/issues/5940)). - Fixed `pi update` to install the exact version returned by the Pi update check, make `--force` reinstall that checked version, fail instead of falling back to an unversioned reinstall when no version is available, and report both the old and updated versions. +- Fixed update notifications to display the actual changelog URL as the hyperlink text. ## [0.79.9] - 2026-06-20 From 8e1900666f3cb83c281297d8f787fae6ee2bd0e6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 22 Jun 2026 11:14:43 +0200 Subject: [PATCH 073/122] Release v0.79.10 --- package-lock.json | 26 ++--- packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 +- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- packages/ai/src/models.generated.ts | 94 +++++++++---------- packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/gondolin/package-lock.json | 4 +- .../examples/extensions/gondolin/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/npm-shrinkwrap.json | 24 ++--- packages/coding-agent/package.json | 8 +- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- 20 files changed, 97 insertions(+), 97 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9bec652f..ba0eac39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5111,10 +5111,10 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.79.9", + "version": "0.79.10", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.9", + "@earendil-works/pi-ai": "^0.79.10", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -5463,7 +5463,7 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.79.9", + "version": "0.79.10", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -5769,12 +5769,12 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.9", + "version": "0.79.10", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.9", - "@earendil-works/pi-ai": "^0.79.9", - "@earendil-works/pi-tui": "^0.79.9", + "@earendil-works/pi-agent-core": "^0.79.10", + "@earendil-works/pi-ai": "^0.79.10", + "@earendil-works/pi-tui": "^0.79.10", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -5815,32 +5815,32 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.79.9", + "version": "0.79.10", "dependencies": { "@anthropic-ai/sdk": "0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.79.9" + "version": "0.79.10" }, "packages/coding-agent/examples/extensions/gondolin": { "name": "pi-extension-gondolin", - "version": "0.79.9", + "version": "0.79.10", "dependencies": { "@earendil-works/gondolin": "0.12.0" } }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.9.9", + "version": "1.9.10", "dependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.79.9", + "version": "0.79.10", "dependencies": { "ms": "2.1.3" }, @@ -6136,7 +6136,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.79.9", + "version": "0.79.10", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index ff48ad2c..86c6fda5 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.10] - 2026-06-22 ## [0.79.9] - 2026-06-20 diff --git a/packages/agent/package.json b/packages/agent/package.json index 75a73cb8..a3f236a4 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.79.9", + "version": "0.79.10", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -33,7 +33,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.79.9", + "@earendil-works/pi-ai": "^0.79.10", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 828e3433..e7778d9c 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.10] - 2026-06-22 ### Fixed diff --git a/packages/ai/package.json b/packages/ai/package.json index 77272079..240209ae 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.79.9", + "version": "0.79.10", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index afdea285..cfc69879 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -8885,23 +8885,6 @@ export const MODELS = { contextWindow: 200000, maxTokens: 4096, } satisfies Model<"openai-completions">, - "anthropic/claude-3.5-haiku": { - id: "anthropic/claude-3.5-haiku", - name: "Anthropic: Claude 3.5 Haiku", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"openai-completions">, "anthropic/claude-fable-5": { id: "anthropic/claude-fable-5", name: "Anthropic: Claude Fable 5", @@ -9657,7 +9640,7 @@ export const MODELS = { cacheRead: 0.2, cacheWrite: 0.375, }, - contextWindow: 131072, + contextWindow: 65536, maxTokens: 32768, } satisfies Model<"openai-completions">, "google/gemini-3.1-flash-lite": { @@ -9845,7 +9828,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 32768, + maxTokens: 8192, } satisfies Model<"openai-completions">, "ibm-granite/granite-4.1-8b": { id: "ibm-granite/granite-4.1-8b", @@ -9947,8 +9930,8 @@ export const MODELS = { cacheRead: 0.06, cacheWrite: 0, }, - contextWindow: 262144, - maxTokens: 144000, + contextWindow: 256000, + maxTokens: 80000, } satisfies Model<"openai-completions">, "liquid/lfm-2.5-1.2b-thinking:free": { id: "liquid/lfm-2.5-1.2b-thinking:free", @@ -10169,7 +10152,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 4096, + maxTokens: 512000, } satisfies Model<"openai-completions">, "mistralai/codestral-2508": { id: "mistralai/codestral-2508", @@ -10505,7 +10488,7 @@ export const MODELS = { cost: { input: 0.6, output: 2.5, - cacheRead: 0.6, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, @@ -10539,12 +10522,12 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.66, - output: 3.5, - cacheRead: 0.33, + output: 3.41, + cacheRead: 0.144, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262142, + maxTokens: 262144, } satisfies Model<"openai-completions">, "moonshotai/kimi-k2.7-code": { id: "moonshotai/kimi-k2.7-code", @@ -10578,7 +10561,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262144, + maxTokens: 256000, } satisfies Model<"openai-completions">, "nvidia/llama-3.3-nemotron-super-49b-v1.5": { id: "nvidia/llama-3.3-nemotron-super-49b-v1.5", @@ -10884,7 +10867,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 1047576, - maxTokens: 4096, + maxTokens: 32768, } satisfies Model<"openai-completions">, "openai/gpt-4.1-nano": { id: "openai/gpt-4.1-nano", @@ -10914,7 +10897,7 @@ export const MODELS = { cost: { input: 2.5, output: 10, - cacheRead: 1.25, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 128000, @@ -11050,11 +11033,11 @@ export const MODELS = { cost: { input: 0.25, output: 2, - cacheRead: 0.03, + cacheRead: 0.025, cacheWrite: 0, }, contextWindow: 400000, - maxTokens: 4096, + maxTokens: 128000, } satisfies Model<"openai-completions">, "openai/gpt-5-nano": { id: "openai/gpt-5-nano", @@ -11118,11 +11101,11 @@ export const MODELS = { cost: { input: 1.25, output: 10, - cacheRead: 0.125, + cacheRead: 0.13, cacheWrite: 0, }, contextWindow: 128000, - maxTokens: 16384, + maxTokens: 32000, } satisfies Model<"openai-completions">, "openai/gpt-5.1-codex": { id: "openai/gpt-5.1-codex", @@ -11209,7 +11192,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 128000, - maxTokens: 32000, + maxTokens: 16384, } satisfies Model<"openai-completions">, "openai/gpt-5.2-codex": { id: "openai/gpt-5.2-codex", @@ -12422,11 +12405,11 @@ export const MODELS = { cost: { input: 0.14, output: 1, - cacheRead: 0.05, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 81920, + maxTokens: 262144, } satisfies Model<"openai-completions">, "qwen/qwen3.5-397b-a17b": { id: "qwen/qwen3.5-397b-a17b", @@ -12459,8 +12442,8 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 256000, - maxTokens: 32768, + contextWindow: 262144, + maxTokens: 262144, } satisfies Model<"openai-completions">, "qwen/qwen3.5-flash-02-23": { id: "qwen/qwen3.5-flash-02-23", @@ -13034,11 +13017,11 @@ export const MODELS = { cost: { input: 0.98, output: 3.08, - cacheRead: 0.182, + cacheRead: 0.49, cacheWrite: 0, }, contextWindow: 202752, - maxTokens: 4096, + maxTokens: 65535, } satisfies Model<"openai-completions">, "z-ai/glm-5.2": { id: "z-ai/glm-5.2", @@ -13050,13 +13033,13 @@ export const MODELS = { thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text"], cost: { - input: 1.2, - output: 4.1, - cacheRead: 0.2, + input: 1, + output: 4, + cacheRead: 0.18, cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 131072, + maxTokens: 32768, } satisfies Model<"openai-completions">, "~anthropic/claude-fable-latest": { id: "~anthropic/claude-fable-latest", @@ -13170,12 +13153,12 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.66, - output: 3.5, - cacheRead: 0.33, + output: 3.41, + cacheRead: 0.144, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262142, + maxTokens: 262144, } satisfies Model<"openai-completions">, "~openai/gpt-latest": { id: "~openai/gpt-latest", @@ -15936,6 +15919,23 @@ export const MODELS = { contextWindow: 200000, maxTokens: 8000, } satisfies Model<"anthropic-messages">, + "sakana/fugu-ultra": { + id: "sakana/fugu-ultra", + name: "Fugu Ultra", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 1000000, + } satisfies Model<"anthropic-messages">, "stepfun/step-3.5-flash": { id: "stepfun/step-3.5-flash", name: "StepFun 3.5 Flash", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 59e4cd27..4014d25b 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.10] - 2026-06-22 ### New Features diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index 9a131279..c138594f 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.79.9", + "version": "0.79.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.79.9", + "version": "0.79.10", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index 8b4182e7..11dfb54a 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.79.9", + "version": "0.79.10", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index 779d519c..a1d1808b 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.79.9", + "version": "0.79.10", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/gondolin/package-lock.json b/packages/coding-agent/examples/extensions/gondolin/package-lock.json index 01ee2f61..7d7f5932 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package-lock.json +++ b/packages/coding-agent/examples/extensions/gondolin/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-gondolin", - "version": "0.79.9", + "version": "0.79.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-gondolin", - "version": "0.79.9", + "version": "0.79.10", "dependencies": { "@earendil-works/gondolin": "0.12.0" } diff --git a/packages/coding-agent/examples/extensions/gondolin/package.json b/packages/coding-agent/examples/extensions/gondolin/package.json index 67a9ac1e..e473cb1a 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package.json +++ b/packages/coding-agent/examples/extensions/gondolin/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-gondolin", "private": true, - "version": "0.79.9", + "version": "0.79.10", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 01aad382..0558de01 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.9.9", + "version": "1.9.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.9.9", + "version": "1.9.10", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index b8dc74ab..5957ad4b 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.9.9", + "version": "1.9.10", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index 89961b12..dac510c0 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.79.9", + "version": "0.79.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.79.9", + "version": "0.79.10", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index 3ed465d2..e23e480f 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.79.9", + "version": "0.79.10", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index c311b556..1e9fa157 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -1,17 +1,17 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.9", + "version": "0.79.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.9", + "version": "0.79.10", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.9", - "@earendil-works/pi-ai": "^0.79.9", - "@earendil-works/pi-tui": "^0.79.9", + "@earendil-works/pi-agent-core": "^0.79.10", + "@earendil-works/pi-ai": "^0.79.10", + "@earendil-works/pi-tui": "^0.79.10", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -474,11 +474,11 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.79.9", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.9.tgz", + "version": "0.79.10", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.10.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.9", + "@earendil-works/pi-ai": "^0.79.10", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -488,8 +488,8 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.79.9", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.9.tgz", + "version": "0.79.10", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.10.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -512,8 +512,8 @@ } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.79.9", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.9.tgz", + "version": "0.79.10", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.10.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 490a7751..81489b28 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.9", + "version": "0.79.10", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -36,9 +36,9 @@ "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.9", - "@earendil-works/pi-ai": "^0.79.9", - "@earendil-works/pi-tui": "^0.79.9", + "@earendil-works/pi-agent-core": "^0.79.10", + "@earendil-works/pi-ai": "^0.79.10", + "@earendil-works/pi-tui": "^0.79.10", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 96ff6d5c..dd175ffd 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.10] - 2026-06-22 ## [0.79.9] - 2026-06-20 diff --git a/packages/tui/package.json b/packages/tui/package.json index d9d53e0e..c5d0e480 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.79.9", + "version": "0.79.10", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", From 329dceb5f3806654c59343949768a2973d752036 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 22 Jun 2026 11:14:46 +0200 Subject: [PATCH 074/122] Add [Unreleased] section for next cycle --- packages/agent/CHANGELOG.md | 2 ++ packages/ai/CHANGELOG.md | 2 ++ packages/coding-agent/CHANGELOG.md | 2 ++ packages/tui/CHANGELOG.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 86c6fda5..0d6e2f67 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.10] - 2026-06-22 ## [0.79.9] - 2026-06-20 diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index e7778d9c..93a85598 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.10] - 2026-06-22 ### Fixed diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 4014d25b..a405f908 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.10] - 2026-06-22 ### New Features diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index dd175ffd..0a34ae38 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.10] - 2026-06-22 ## [0.79.9] - 2026-06-20 From 717a8f958a1ac6ae245091a905672acebd2f3fe4 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 22 Jun 2026 12:54:12 +0200 Subject: [PATCH 075/122] fix(ai): revert selective pi-ai base entrypoints --- packages/agent/README.md | 18 - packages/agent/package.json | 4 - packages/agent/src/agent-loop.ts | 2 +- packages/agent/src/agent.ts | 2 +- packages/agent/src/base.ts | 39 -- packages/agent/src/harness/agent-harness.ts | 2 +- .../compaction/branch-summarization.ts | 3 +- .../src/harness/compaction/compaction.ts | 10 +- .../agent/src/harness/compaction/utils.ts | 2 +- packages/agent/src/harness/messages.ts | 2 +- packages/agent/src/harness/session/session.ts | 2 +- packages/agent/src/harness/types.ts | 4 +- packages/agent/src/index.ts | 49 +- packages/agent/src/proxy.ts | 2 +- packages/agent/src/types.ts | 2 +- packages/agent/vitest.config.ts | 6 - packages/agent/vitest.harness.config.ts | 6 - packages/ai/README.md | 32 +- packages/ai/package.json | 12 - packages/ai/src/base.ts | 45 -- packages/ai/src/bedrock-provider.ts | 4 +- packages/ai/src/images-api-registry.ts | 4 - packages/ai/src/images.ts | 2 + packages/ai/src/index.ts | 49 +- packages/ai/src/providers/amazon-bedrock.ts | 9 - packages/ai/src/providers/anthropic.ts | 9 - .../src/providers/azure-openai-responses.ts | 9 - packages/ai/src/providers/google-vertex.ts | 9 - packages/ai/src/providers/google.ts | 9 - .../ai/src/providers/images/openrouter.ts | 8 - .../src/providers/images/register-builtins.ts | 50 ++ packages/ai/src/providers/mistral.ts | 9 - .../src/providers/openai-codex-responses.ts | 9 - .../ai/src/providers/openai-completions.ts | 9 - packages/ai/src/providers/openai-responses.ts | 9 - .../ai/src/providers/register-builtins.ts | 458 +++++++++++------- packages/ai/src/stream.ts | 4 + packages/ai/test/abort.test.ts | 2 +- .../anthropic-eager-tool-input-e2e.test.ts | 2 +- ...ic-empty-thinking-signature-compat.test.ts | 2 +- .../anthropic-force-adaptive-thinking.test.ts | 2 +- ...anthropic-long-cache-retention-e2e.test.ts | 2 +- .../ai/test/anthropic-opus-4-8-smoke.test.ts | 2 +- .../test/anthropic-temperature-compat.test.ts | 2 +- .../test/anthropic-thinking-disable.test.ts | 2 +- .../anthropic-tool-name-normalization.test.ts | 2 +- packages/ai/test/base-entrypoint.test.ts | 68 --- packages/ai/test/bedrock-models.test.ts | 2 +- packages/ai/test/cache-retention.test.ts | 2 +- packages/ai/test/context-overflow.test.ts | 2 +- .../ai/test/cross-provider-handoff.test.ts | 2 +- packages/ai/test/empty.test.ts | 2 +- .../ai/test/google-thinking-disable.test.ts | 2 +- packages/ai/test/interleaved-thinking.test.ts | 2 +- packages/ai/test/lazy-module-load.test.ts | 52 +- .../ai/test/mistral-reasoning-mode.test.ts | 2 +- packages/ai/test/mistral-tool-schema.test.ts | 2 +- .../openai-codex-cache-affinity-e2e.test.ts | 2 +- .../openai-completions-empty-tools.test.ts | 2 +- .../openai-completions-response-model.test.ts | 2 +- .../openai-completions-tool-choice.test.ts | 2 +- ...penai-responses-cache-affinity-e2e.test.ts | 2 +- ...nai-responses-reasoning-replay-e2e.test.ts | 2 +- .../test/openrouter-cache-write-repro.test.ts | 2 +- packages/ai/test/openrouter-images.test.ts | 12 - packages/ai/test/responseid.test.ts | 2 +- packages/ai/test/stream.test.ts | 2 +- packages/ai/test/tokens.test.ts | 2 +- .../test/tool-call-id-normalization.test.ts | 2 +- .../ai/test/tool-call-without-result.test.ts | 2 +- packages/ai/test/total-tokens.test.ts | 2 +- packages/ai/test/unicode-surrogate.test.ts | 2 +- packages/ai/test/xhigh.test.ts | 2 +- ...ms-anthropic-empty-signature-smoke.test.ts | 2 +- packages/ai/test/zen.test.ts | 2 +- packages/coding-agent/vitest.config.ts | 9 - scripts/check-browser-smoke.mjs | 44 -- tsconfig.json | 1 - 78 files changed, 492 insertions(+), 668 deletions(-) delete mode 100644 packages/agent/src/base.ts delete mode 100644 packages/ai/src/base.ts create mode 100644 packages/ai/src/providers/images/register-builtins.ts delete mode 100644 packages/ai/test/base-entrypoint.test.ts diff --git a/packages/agent/README.md b/packages/agent/README.md index ca4c1080..f0fedb81 100644 --- a/packages/agent/README.md +++ b/packages/agent/README.md @@ -31,24 +31,6 @@ agent.subscribe((event) => { await agent.prompt("Hello!"); ``` -## Base Entry Point - -Use `@earendil-works/pi-agent-core/base` with `@earendil-works/pi-ai/base` when bundling applications that should include only selected provider transports: - -```typescript -import { Agent } from "@earendil-works/pi-agent-core/base"; -import { getModel } from "@earendil-works/pi-ai/base"; -import { register } from "@earendil-works/pi-ai/anthropic"; - -register(); - -const agent = new Agent({ - initialState: { model: getModel("anthropic", "claude-sonnet-4-6") }, -}); -``` - -The default `@earendil-works/pi-agent-core` entry point remains batteries-included and registers pi-ai's lazy built-in transports for backward compatibility. - ## Core Concepts ### AgentMessage vs LLM Message diff --git a/packages/agent/package.json b/packages/agent/package.json index a3f236a4..9322d7c6 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -10,10 +10,6 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js" }, - "./base": { - "types": "./dist/base.d.ts", - "import": "./dist/base.js" - }, "./node": { "types": "./dist/node.d.ts", "import": "./dist/node.js" diff --git a/packages/agent/src/agent-loop.ts b/packages/agent/src/agent-loop.ts index 8a8f20f1..77bd9568 100644 --- a/packages/agent/src/agent-loop.ts +++ b/packages/agent/src/agent-loop.ts @@ -10,7 +10,7 @@ import { streamSimple, type ToolResultMessage, validateToolArguments, -} from "@earendil-works/pi-ai/base"; +} from "@earendil-works/pi-ai"; import type { AgentContext, AgentEvent, diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index ef26e213..db6684a8 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -7,7 +7,7 @@ import { type TextContent, type ThinkingBudgets, type Transport, -} from "@earendil-works/pi-ai/base"; +} from "@earendil-works/pi-ai"; import { runAgentLoop, runAgentLoopContinue } from "./agent-loop.ts"; import type { AfterToolCallContext, diff --git a/packages/agent/src/base.ts b/packages/agent/src/base.ts deleted file mode 100644 index ad1abafa..00000000 --- a/packages/agent/src/base.ts +++ /dev/null @@ -1,39 +0,0 @@ -export * from "./agent.ts"; -export * from "./agent-loop.ts"; -export * from "./harness/agent-harness.ts"; -export { - type BranchPreparation, - type BranchSummaryDetails, - type CollectEntriesResult, - collectEntriesForBranchSummary, - generateBranchSummary, - prepareBranchEntries, -} from "./harness/compaction/branch-summarization.ts"; -export { - calculateContextTokens, - compact, - DEFAULT_COMPACTION_SETTINGS, - estimateContextTokens, - estimateTokens, - findCutPoint, - findTurnStartIndex, - generateSummary, - getLastAssistantUsage, - prepareCompaction, - serializeConversation, - shouldCompact, -} from "./harness/compaction/compaction.ts"; -export * from "./harness/messages.ts"; -export * from "./harness/prompt-templates.ts"; -export * from "./harness/session/jsonl-repo.ts"; -export * from "./harness/session/memory-repo.ts"; -export * from "./harness/session/repo-utils.ts"; -export * from "./harness/session/session.ts"; -export { uuidv7 } from "./harness/session/uuid.ts"; -export * from "./harness/skills.ts"; -export * from "./harness/system-prompt.ts"; -export * from "./harness/types.ts"; -export * from "./harness/utils/shell-output.ts"; -export * from "./harness/utils/truncate.ts"; -export * from "./proxy.ts"; -export * from "./types.ts"; diff --git a/packages/agent/src/harness/agent-harness.ts b/packages/agent/src/harness/agent-harness.ts index f097ee75..96563465 100644 --- a/packages/agent/src/harness/agent-harness.ts +++ b/packages/agent/src/harness/agent-harness.ts @@ -4,7 +4,7 @@ import { type Model, streamSimple, type UserMessage, -} from "@earendil-works/pi-ai/base"; +} from "@earendil-works/pi-ai"; import { runAgentLoop } from "../agent-loop.ts"; import type { AgentContext, diff --git a/packages/agent/src/harness/compaction/branch-summarization.ts b/packages/agent/src/harness/compaction/branch-summarization.ts index 60ffe771..c1824ebf 100644 --- a/packages/agent/src/harness/compaction/branch-summarization.ts +++ b/packages/agent/src/harness/compaction/branch-summarization.ts @@ -1,4 +1,5 @@ -import { completeSimple, type Model } from "@earendil-works/pi-ai/base"; +import type { Model } from "@earendil-works/pi-ai"; +import { completeSimple } from "@earendil-works/pi-ai"; import type { AgentMessage } from "../../types.ts"; import { convertToLlm, diff --git a/packages/agent/src/harness/compaction/compaction.ts b/packages/agent/src/harness/compaction/compaction.ts index d619b34a..dba753d7 100644 --- a/packages/agent/src/harness/compaction/compaction.ts +++ b/packages/agent/src/harness/compaction/compaction.ts @@ -1,11 +1,5 @@ -import { - type AssistantMessage, - completeSimple, - type ImageContent, - type Model, - type TextContent, - type Usage, -} from "@earendil-works/pi-ai/base"; +import type { AssistantMessage, ImageContent, Model, TextContent, Usage } from "@earendil-works/pi-ai"; +import { completeSimple } from "@earendil-works/pi-ai"; import type { AgentMessage, ThinkingLevel } from "../../types.ts"; import { convertToLlm, diff --git a/packages/agent/src/harness/compaction/utils.ts b/packages/agent/src/harness/compaction/utils.ts index 388dd84a..07535b79 100644 --- a/packages/agent/src/harness/compaction/utils.ts +++ b/packages/agent/src/harness/compaction/utils.ts @@ -1,4 +1,4 @@ -import type { Message } from "@earendil-works/pi-ai/base"; +import type { Message } from "@earendil-works/pi-ai"; import type { AgentMessage } from "../../types.ts"; /** File paths touched by a session branch or compaction range. */ diff --git a/packages/agent/src/harness/messages.ts b/packages/agent/src/harness/messages.ts index 8368dd34..36ce96a1 100644 --- a/packages/agent/src/harness/messages.ts +++ b/packages/agent/src/harness/messages.ts @@ -1,4 +1,4 @@ -import type { ImageContent, Message, TextContent } from "@earendil-works/pi-ai/base"; +import type { ImageContent, Message, TextContent } from "@earendil-works/pi-ai"; import type { AgentMessage } from "../types.ts"; export const COMPACTION_SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary: diff --git a/packages/agent/src/harness/session/session.ts b/packages/agent/src/harness/session/session.ts index 3fd76c6b..6f136208 100644 --- a/packages/agent/src/harness/session/session.ts +++ b/packages/agent/src/harness/session/session.ts @@ -1,4 +1,4 @@ -import type { ImageContent, TextContent } from "@earendil-works/pi-ai/base"; +import type { ImageContent, TextContent } from "@earendil-works/pi-ai"; import type { AgentMessage } from "../../types.ts"; import { createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage } from "../messages.ts"; import type { diff --git a/packages/agent/src/harness/types.ts b/packages/agent/src/harness/types.ts index 8f281570..4756ca84 100644 --- a/packages/agent/src/harness/types.ts +++ b/packages/agent/src/harness/types.ts @@ -1,5 +1,5 @@ -import type { ImageContent, Model, SimpleStreamOptions, TextContent, Transport } from "@earendil-works/pi-ai/base"; -import type { AgentEvent, AgentMessage, AgentTool, QueueMode, ThinkingLevel } from "../types.ts"; +import type { ImageContent, Model, SimpleStreamOptions, TextContent, Transport } from "@earendil-works/pi-ai"; +import type { AgentEvent, AgentMessage, AgentTool, QueueMode, ThinkingLevel } from "../index.ts"; import type { Session } from "./session/session.ts"; /** Result of a fallible operation. Expected failures are returned as `ok: false` instead of thrown. */ diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index 08b7b9cb..fd3c2b9f 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -1,5 +1,44 @@ -// Import the default pi-ai entrypoint so that all built-in providers register -// automatically. Unlike "@earendil-works/pi-ai/base" which does not. -import "@earendil-works/pi-ai"; - -export * from "./base.ts"; +// Core Agent +export * from "./agent.ts"; +// Loop functions +export * from "./agent-loop.ts"; +export * from "./harness/agent-harness.ts"; +export { + type BranchPreparation, + type BranchSummaryDetails, + type CollectEntriesResult, + collectEntriesForBranchSummary, + generateBranchSummary, + prepareBranchEntries, +} from "./harness/compaction/branch-summarization.ts"; +export { + calculateContextTokens, + compact, + DEFAULT_COMPACTION_SETTINGS, + estimateContextTokens, + estimateTokens, + findCutPoint, + findTurnStartIndex, + generateSummary, + getLastAssistantUsage, + prepareCompaction, + serializeConversation, + shouldCompact, +} from "./harness/compaction/compaction.ts"; +export * from "./harness/messages.ts"; +export * from "./harness/prompt-templates.ts"; +export * from "./harness/session/jsonl-repo.ts"; +export * from "./harness/session/memory-repo.ts"; +export * from "./harness/session/repo-utils.ts"; +export * from "./harness/session/session.ts"; +export { uuidv7 } from "./harness/session/uuid.ts"; +export * from "./harness/skills.ts"; +export * from "./harness/system-prompt.ts"; +// Harness +export * from "./harness/types.ts"; +export * from "./harness/utils/shell-output.ts"; +export * from "./harness/utils/truncate.ts"; +// Proxy utilities +export * from "./proxy.ts"; +// Types +export * from "./types.ts"; diff --git a/packages/agent/src/proxy.ts b/packages/agent/src/proxy.ts index 8e5cb2c2..5f0925c9 100644 --- a/packages/agent/src/proxy.ts +++ b/packages/agent/src/proxy.ts @@ -14,7 +14,7 @@ import { type SimpleStreamOptions, type StopReason, type ToolCall, -} from "@earendil-works/pi-ai/base"; +} from "@earendil-works/pi-ai"; // Create stream class matching ProxyMessageEventStream class ProxyMessageEventStream extends EventStream { diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts index 541d8d63..cb99a79a 100644 --- a/packages/agent/src/types.ts +++ b/packages/agent/src/types.ts @@ -9,7 +9,7 @@ import type { TextContent, Tool, ToolResultMessage, -} from "@earendil-works/pi-ai/base"; +} from "@earendil-works/pi-ai"; import type { Static, TSchema } from "typebox"; /** diff --git a/packages/agent/vitest.config.ts b/packages/agent/vitest.config.ts index 297fbd50..bcc497fa 100644 --- a/packages/agent/vitest.config.ts +++ b/packages/agent/vitest.config.ts @@ -1,12 +1,6 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ - resolve: { - alias: { - "@earendil-works/pi-ai/base": new URL("../ai/src/base.ts", import.meta.url).pathname, - "@earendil-works/pi-ai": new URL("../ai/src/index.ts", import.meta.url).pathname, - }, - }, test: { globals: true, environment: "node", diff --git a/packages/agent/vitest.harness.config.ts b/packages/agent/vitest.harness.config.ts index 37acc34d..9421e5a9 100644 --- a/packages/agent/vitest.harness.config.ts +++ b/packages/agent/vitest.harness.config.ts @@ -1,12 +1,6 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ - resolve: { - alias: { - "@earendil-works/pi-ai/base": new URL("../ai/src/base.ts", import.meta.url).pathname, - "@earendil-works/pi-ai": new URL("../ai/src/index.ts", import.meta.url).pathname, - }, - }, test: { globals: true, environment: "node", diff --git a/packages/ai/README.md b/packages/ai/README.md index bec437aa..428ef259 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -8,7 +8,6 @@ Unified LLM API with automatic model discovery, provider configuration, token an - [Supported Providers](#supported-providers) - [Installation](#installation) -- [Base Entry Point](#base-entry-point) - [Quick Start](#quick-start) - [Tools](#tools) - [Defining Tools](#defining-tools) @@ -89,30 +88,6 @@ npm install @earendil-works/pi-ai TypeBox exports are re-exported from `@earendil-works/pi-ai`: `Type`, `Static`, and `TSchema`. -## Base Entry Point - -Use `@earendil-works/pi-ai/base` when a bundler should include only explicitly selected transport implementations. The base entry point exposes model discovery, registries, generic dispatch, environment-key helpers, and provider-neutral utilities without registering built-in transports during module initialization. Generic dispatch still resolves configured environment keys when called. - -Import each selected transport directly and call its `register()` function: - -```typescript -import { getModel, streamSimple } from '@earendil-works/pi-ai/base'; -import { register as registerAnthropic } from '@earendil-works/pi-ai/anthropic'; - -registerAnthropic(); - -const model = getModel('anthropic', 'claude-sonnet-4-6'); -const response = await streamSimple(model, { - messages: [{ role: 'user', content: 'Hello!' }] -}, { - apiKey: process.env.ANTHROPIC_API_KEY -}).result(); -``` - -Direct transport imports bundle their implementation and SDK code. Register only the transports used by the application. For example, OpenRouter, Groq, xAI, and DeepSeek chat models share the `@earendil-works/pi-ai/openai-completions` transport. - -Use `@earendil-works/pi-ai` for the batteries-included behavior. The root entry point remains backward-compatible: it registers lazy wrappers for all built-in transports and resolves environment API keys automatically during dispatch. - ## Quick Start ```typescript @@ -463,7 +438,7 @@ Do not use `stream()` or `complete()` for image generation. Image generation is ### Basic Image Generation ```typescript -import { getImageModel, generateImages } from '@earendil-works/pi-ai'; +import { getImageModel, generateImages } from '@mariozechner/pi-ai'; const model = getImageModel('openrouter', 'google/gemini-2.5-flash-image'); @@ -1123,7 +1098,6 @@ const response = await complete(model, { - OAuth login flows are not supported in browser environments. Use the `@earendil-works/pi-ai/oauth` entry point in Node.js. - In browser builds, Bedrock can still appear in model lists. Calls to Bedrock models fail at runtime. - Use a server-side proxy or backend service if you need Bedrock or OAuth-based auth from a web app. -- Use `@earendil-works/pi-ai/base` plus explicit direct transport registration when a browser bundle should exclude unused provider SDK implementations. ### Environment Variables (Node.js only) @@ -1360,7 +1334,6 @@ Create a new provider file (for example `amazon-bedrock.ts`) that exports: - `stream()` function returning `AssistantMessageEventStream` - `streamSimple()` for `SimpleStreamOptions` mapping -- `register()` for explicit direct transport registration from `@earendil-works/pi-ai/base` - Provider-specific options interface - Message conversion functions to transform `Context` to provider format - Tool conversion if the provider supports tools @@ -1371,8 +1344,7 @@ Create a new provider file (for example `amazon-bedrock.ts`) that exports: - Register the API with `registerApiProvider()` - Add a package subpath export in `package.json` for the provider module (`./dist/providers/.js`) - Add lazy loader wrappers in `src/providers/register-builtins.ts`, do not statically import provider implementation modules there -- Add any root-level `export type` re-exports in `src/index.ts` and `src/base.ts` that should remain available from `@earendil-works/pi-ai` and `@earendil-works/pi-ai/base` -- Keep `src/base.ts` free of built-in registration imports +- Add any root-level `export type` re-exports in `src/index.ts` that should remain available from `@earendil-works/pi-ai` - Add credential detection in `env-api-keys.ts` for the new provider - Ensure `streamSimple` handles auth lookup via `getEnvApiKey()` or provider-specific auth diff --git a/packages/ai/package.json b/packages/ai/package.json index 240209ae..5ba97133 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -10,14 +10,6 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js" }, - "./base": { - "types": "./dist/base.d.ts", - "import": "./dist/base.js" - }, - "./amazon-bedrock": { - "types": "./dist/providers/amazon-bedrock.d.ts", - "import": "./dist/providers/amazon-bedrock.js" - }, "./anthropic": { "types": "./dist/providers/anthropic.d.ts", "import": "./dist/providers/anthropic.js" @@ -50,10 +42,6 @@ "types": "./dist/providers/openai-responses.d.ts", "import": "./dist/providers/openai-responses.js" }, - "./openrouter-images": { - "types": "./dist/providers/images/openrouter.d.ts", - "import": "./dist/providers/images/openrouter.js" - }, "./oauth": { "types": "./dist/oauth.d.ts", "import": "./dist/oauth.js" diff --git a/packages/ai/src/base.ts b/packages/ai/src/base.ts deleted file mode 100644 index f1a8a110..00000000 --- a/packages/ai/src/base.ts +++ /dev/null @@ -1,45 +0,0 @@ -export type { Static, TSchema } from "typebox"; -export { Type } from "typebox"; - -export * from "./api-registry.ts"; -export * from "./env-api-keys.ts"; -export * from "./image-models.ts"; -export * from "./images.ts"; -export * from "./images-api-registry.ts"; -export * from "./models.ts"; -export type { BedrockOptions, BedrockThinkingDisplay } from "./providers/amazon-bedrock.ts"; -export type { AnthropicEffort, AnthropicOptions, AnthropicThinkingDisplay } from "./providers/anthropic.ts"; -export type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses.ts"; -export * from "./providers/faux.ts"; -export type { GoogleOptions } from "./providers/google.ts"; -export type { GoogleThinkingLevel } from "./providers/google-shared.ts"; -export type { GoogleVertexOptions } from "./providers/google-vertex.ts"; -export type { MistralOptions } from "./providers/mistral.ts"; -export type { - OpenAICodexResponsesOptions, - OpenAICodexWebSocketDebugStats, -} from "./providers/openai-codex-responses.ts"; -export type { OpenAICompletionsOptions } from "./providers/openai-completions.ts"; -export type { OpenAIResponsesOptions } from "./providers/openai-responses.ts"; -export * from "./session-resources.ts"; -export * from "./stream.ts"; -export * from "./types.ts"; -export * from "./utils/diagnostics.ts"; -export * from "./utils/event-stream.ts"; -export * from "./utils/json-parse.ts"; -export type { - OAuthAuthInfo, - OAuthCredentials, - OAuthDeviceCodeInfo, - OAuthLoginCallbacks, - OAuthPrompt, - OAuthProvider, - OAuthProviderId, - OAuthProviderInfo, - OAuthProviderInterface, - OAuthSelectOption, - OAuthSelectPrompt, -} from "./utils/oauth/types.ts"; -export * from "./utils/overflow.ts"; -export * from "./utils/typebox-helpers.ts"; -export * from "./utils/validation.ts"; diff --git a/packages/ai/src/bedrock-provider.ts b/packages/ai/src/bedrock-provider.ts index f5aa3998..cf08b33e 100644 --- a/packages/ai/src/bedrock-provider.ts +++ b/packages/ai/src/bedrock-provider.ts @@ -1,6 +1,4 @@ -import { register, streamBedrock, streamSimpleBedrock } from "./providers/amazon-bedrock.ts"; - -export { register }; +import { streamBedrock, streamSimpleBedrock } from "./providers/amazon-bedrock.ts"; export const bedrockProviderModule = { streamBedrock, diff --git a/packages/ai/src/images-api-registry.ts b/packages/ai/src/images-api-registry.ts index 5335a926..4758439b 100644 --- a/packages/ai/src/images-api-registry.ts +++ b/packages/ai/src/images-api-registry.ts @@ -51,7 +51,3 @@ export function registerImagesApiProvider, project: string, diff --git a/packages/ai/src/providers/google.ts b/packages/ai/src/providers/google.ts index 62ad9cfd..a270792a 100644 --- a/packages/ai/src/providers/google.ts +++ b/packages/ai/src/providers/google.ts @@ -4,7 +4,6 @@ import { GoogleGenAI, type ThinkingConfig, } from "@google/genai"; -import { registerApiProvider } from "../api-registry.ts"; import { calculateCost, clampThinkingLevel } from "../models.ts"; import type { Api, @@ -316,14 +315,6 @@ export const streamSimpleGoogle: StreamFunction<"google-generative-ai", SimpleSt } satisfies GoogleOptions); }; -export function register(): void { - registerApiProvider({ - api: "google-generative-ai", - stream: streamGoogle, - streamSimple: streamSimpleGoogle, - }); -} - function createClient( model: Model<"google-generative-ai">, apiKey?: string, diff --git a/packages/ai/src/providers/images/openrouter.ts b/packages/ai/src/providers/images/openrouter.ts index d1b2c7e8..54caeaf0 100644 --- a/packages/ai/src/providers/images/openrouter.ts +++ b/packages/ai/src/providers/images/openrouter.ts @@ -6,7 +6,6 @@ import type { ChatCompletionContentPartText, ChatCompletionCreateParamsNonStreaming, } from "openai/resources/chat/completions.js"; -import { registerImagesApiProvider } from "../../images-api-registry.ts"; import type { AssistantImages, ImageContent, @@ -104,13 +103,6 @@ export const generateImagesOpenRouter: ImagesFunction<"openrouter-images", Image } }; -export function register(): void { - registerImagesApiProvider({ - api: "openrouter-images", - generateImages: generateImagesOpenRouter, - }); -} - function createClient( model: ImagesModel<"openrouter-images">, apiKey: string, diff --git a/packages/ai/src/providers/images/register-builtins.ts b/packages/ai/src/providers/images/register-builtins.ts new file mode 100644 index 00000000..e3decbb9 --- /dev/null +++ b/packages/ai/src/providers/images/register-builtins.ts @@ -0,0 +1,50 @@ +import { registerImagesApiProvider } from "../../images-api-registry.ts"; +import type { AssistantImages, ImagesContext, ImagesFunction, ImagesModel, ImagesOptions } from "../../types.ts"; +import type { generateImagesOpenRouter as generateImagesOpenRouterFunction } from "./openrouter.ts"; + +interface OpenRouterImagesProviderModule { + generateImagesOpenRouter: typeof generateImagesOpenRouterFunction; +} + +let openRouterImagesProviderModulePromise: Promise | undefined; + +function createLazyLoadErrorImages(model: ImagesModel<"openrouter-images">, error: unknown): AssistantImages { + return { + api: model.api, + provider: model.provider, + model: model.id, + output: [], + stopReason: "error", + errorMessage: error instanceof Error ? error.message : String(error), + timestamp: Date.now(), + }; +} + +function loadOpenRouterImagesProviderModule(): Promise { + openRouterImagesProviderModulePromise ||= import("./openrouter.ts").then( + (module) => module as OpenRouterImagesProviderModule, + ); + return openRouterImagesProviderModulePromise; +} + +export const generateImagesOpenRouter: ImagesFunction<"openrouter-images", ImagesOptions> = async ( + model: ImagesModel<"openrouter-images">, + context: ImagesContext, + options?: ImagesOptions, +) => { + try { + const module = await loadOpenRouterImagesProviderModule(); + return await module.generateImagesOpenRouter(model, context, options); + } catch (error) { + return createLazyLoadErrorImages(model, error); + } +}; + +export function registerBuiltInImagesApiProviders(): void { + registerImagesApiProvider({ + api: "openrouter-images", + generateImages: generateImagesOpenRouter, + }); +} + +registerBuiltInImagesApiProviders(); diff --git a/packages/ai/src/providers/mistral.ts b/packages/ai/src/providers/mistral.ts index ba331af4..6a132236 100644 --- a/packages/ai/src/providers/mistral.ts +++ b/packages/ai/src/providers/mistral.ts @@ -6,7 +6,6 @@ import type { ContentChunk, FunctionTool, } from "@mistralai/mistralai/models/components"; -import { registerApiProvider } from "../api-registry.ts"; import { calculateCost, clampThinkingLevel } from "../models.ts"; import type { AssistantMessage, @@ -131,14 +130,6 @@ export const streamSimpleMistral: StreamFunction<"mistral-conversations", Simple } satisfies MistralOptions); }; -export function register(): void { - registerApiProvider({ - api: "mistral-conversations", - stream: streamMistral, - streamSimple: streamSimpleMistral, - }); -} - function createOutput(model: Model<"mistral-conversations">): AssistantMessage { return { role: "assistant", diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index 1bc2879a..19a2f5d7 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -20,7 +20,6 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version }); } -import { registerApiProvider } from "../api-registry.ts"; import { clampThinkingLevel } from "../models.ts"; import { registerSessionResourceCleanup } from "../session-resources.ts"; import type { @@ -429,14 +428,6 @@ export const streamSimpleOpenAICodexResponses: StreamFunction<"openai-codex-resp } satisfies OpenAICodexResponsesOptions); }; -export function register(): void { - registerApiProvider({ - api: "openai-codex-responses", - stream: streamOpenAICodexResponses, - streamSimple: streamSimpleOpenAICodexResponses, - }); -} - // ============================================================================ // Request Building // ============================================================================ diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index b257f96e..2909678c 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -10,7 +10,6 @@ import type { ChatCompletionSystemMessageParam, ChatCompletionToolMessageParam, } from "openai/resources/chat/completions.js"; -import { registerApiProvider } from "../api-registry.ts"; import { calculateCost, clampThinkingLevel } from "../models.ts"; import type { AssistantMessage, @@ -486,14 +485,6 @@ export const streamSimpleOpenAICompletions: StreamFunction<"openai-completions", } satisfies OpenAICompletionsOptions); }; -export function register(): void { - registerApiProvider({ - api: "openai-completions", - stream: streamOpenAICompletions, - streamSimple: streamSimpleOpenAICompletions, - }); -} - function createClient( model: Model<"openai-completions">, context: Context, diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index b1692978..014233d5 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -1,6 +1,5 @@ import OpenAI from "openai"; import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js"; -import { registerApiProvider } from "../api-registry.ts"; import { clampThinkingLevel } from "../models.ts"; import type { Api, @@ -182,14 +181,6 @@ export const streamSimpleOpenAIResponses: StreamFunction<"openai-responses", Sim } satisfies OpenAIResponsesOptions); }; -export function register(): void { - registerApiProvider({ - api: "openai-responses", - stream: streamOpenAIResponses, - streamSimple: streamSimpleOpenAIResponses, - }); -} - function createClient( model: Model<"openai-responses">, context: Context, diff --git a/packages/ai/src/providers/register-builtins.ts b/packages/ai/src/providers/register-builtins.ts index 6192f8a2..8fdcaaf0 100644 --- a/packages/ai/src/providers/register-builtins.ts +++ b/packages/ai/src/providers/register-builtins.ts @@ -1,14 +1,9 @@ -import { type ApiProvider, clearApiProviders, getApiProvider, registerApiProvider } from "../api-registry.ts"; -import { getImagesApiProvider, type ImagesApiProvider, registerImagesApiProvider } from "../images-api-registry.ts"; +import { clearApiProviders, registerApiProvider } from "../api-registry.ts"; import type { Api, - AssistantImages, AssistantMessage, AssistantMessageEvent, - ImagesApi, - ImagesContext, - ImagesModel, - ImagesOptions, + Context, Model, SimpleStreamOptions, StreamFunction, @@ -25,47 +20,70 @@ import type { OpenAICodexResponsesOptions } from "./openai-codex-responses.ts"; import type { OpenAICompletionsOptions } from "./openai-completions.ts"; import type { OpenAIResponsesOptions } from "./openai-responses.ts"; -interface RegisteringProviderModule { - register(): void; +interface LazyProviderModule< + TApi extends Api, + TOptions extends StreamOptions, + TSimpleOptions extends SimpleStreamOptions, +> { + stream: (model: Model, context: Context, options?: TOptions) => AsyncIterable; + streamSimple: ( + model: Model, + context: Context, + options?: TSimpleOptions, + ) => AsyncIterable; } -function createLazyLoadErrorImages(model: ImagesModel<"openrouter-images">, error: unknown): AssistantImages { - return { - api: model.api, - provider: model.provider, - model: model.id, - output: [], - stopReason: "error", - errorMessage: error instanceof Error ? error.message : String(error), - timestamp: Date.now(), - }; +interface AnthropicProviderModule { + streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOptions>; + streamSimpleAnthropic: StreamFunction<"anthropic-messages", SimpleStreamOptions>; } -function createLazyImagesApiProvider( - api: TApi, - loadModule: () => Promise, -): ImagesApiProvider { - return { - api, - generateImages: async (model: ImagesModel, context: ImagesContext, options?: TOptions) => { - try { - const module = await loadModule(); - module.register(); - const provider = getImagesApiProvider(api); - if (!provider) { - throw new Error(`No API provider registered for api: ${api}`); - } - return await provider.generateImages(model, context, options); - } catch (error) { - return createLazyLoadErrorImages(model as ImagesModel<"openrouter-images">, error); - } - }, - }; +interface AzureOpenAIResponsesProviderModule { + streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses", AzureOpenAIResponsesOptions>; + streamSimpleAzureOpenAIResponses: StreamFunction<"azure-openai-responses", SimpleStreamOptions>; +} + +interface GoogleProviderModule { + streamGoogle: StreamFunction<"google-generative-ai", GoogleOptions>; + streamSimpleGoogle: StreamFunction<"google-generative-ai", SimpleStreamOptions>; +} + +interface GoogleVertexProviderModule { + streamGoogleVertex: StreamFunction<"google-vertex", GoogleVertexOptions>; + streamSimpleGoogleVertex: StreamFunction<"google-vertex", SimpleStreamOptions>; +} + +interface MistralProviderModule { + streamMistral: StreamFunction<"mistral-conversations", MistralOptions>; + streamSimpleMistral: StreamFunction<"mistral-conversations", SimpleStreamOptions>; +} + +interface OpenAICodexResponsesProviderModule { + streamOpenAICodexResponses: StreamFunction<"openai-codex-responses", OpenAICodexResponsesOptions>; + streamSimpleOpenAICodexResponses: StreamFunction<"openai-codex-responses", SimpleStreamOptions>; +} + +interface OpenAICompletionsProviderModule { + streamOpenAICompletions: StreamFunction<"openai-completions", OpenAICompletionsOptions>; + streamSimpleOpenAICompletions: StreamFunction<"openai-completions", SimpleStreamOptions>; +} + +interface OpenAIResponsesProviderModule { + streamOpenAIResponses: StreamFunction<"openai-responses", OpenAIResponsesOptions>; + streamSimpleOpenAIResponses: StreamFunction<"openai-responses", SimpleStreamOptions>; } interface BedrockProviderModule { - streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOptions>; - streamSimpleBedrock: StreamFunction<"bedrock-converse-stream", SimpleStreamOptions>; + streamBedrock: ( + model: Model<"bedrock-converse-stream">, + context: Context, + options?: BedrockOptions, + ) => AsyncIterable; + streamSimpleBedrock: ( + model: Model<"bedrock-converse-stream">, + context: Context, + options?: SimpleStreamOptions, + ) => AsyncIterable; } const importNodeOnlyProvider = (specifier: string): Promise => { @@ -73,10 +91,42 @@ const importNodeOnlyProvider = (specifier: string): Promise => { return import(runtimeSpecifier); }; -let bedrockProviderModuleOverride: BedrockProviderModule | undefined; +let anthropicProviderModulePromise: + | Promise> + | undefined; +let azureOpenAIResponsesProviderModulePromise: + | Promise> + | undefined; +let googleProviderModulePromise: + | Promise> + | undefined; +let googleVertexProviderModulePromise: + | Promise> + | undefined; +let mistralProviderModulePromise: + | Promise> + | undefined; +let openAICodexResponsesProviderModulePromise: + | Promise> + | undefined; +let openAICompletionsProviderModulePromise: + | Promise> + | undefined; +let openAIResponsesProviderModulePromise: + | Promise> + | undefined; +let bedrockProviderModuleOverride: + | LazyProviderModule<"bedrock-converse-stream", BedrockOptions, SimpleStreamOptions> + | undefined; +let bedrockProviderModulePromise: + | Promise> + | undefined; export function setBedrockProviderModule(module: BedrockProviderModule): void { - bedrockProviderModuleOverride = module; + bedrockProviderModuleOverride = { + stream: module.streamBedrock, + streamSimple: module.streamSimpleBedrock, + }; } function forwardStream(target: AssistantMessageEventStream, source: AsyncIterable): void { @@ -109,29 +159,15 @@ function createLazyLoadErrorMessage(model: Model, error: }; } -async function loadAndRegisterProvider( - api: TApi, - loadModule: () => Promise, -) { - const module = await loadModule(); - module.register(); - const provider = getApiProvider(api); - if (!provider) { - throw new Error(`No API provider registered for api: ${api}`); - } - return provider; -} - -function createLazyStream( - api: TApi, - loadModule: () => Promise, +function createLazyStream( + loadModule: () => Promise>, ): StreamFunction { return (model, context, options) => { const outer = new AssistantMessageEventStream(); - loadAndRegisterProvider(api, loadModule) - .then((provider) => { - const inner = provider.stream(model, context, options); + loadModule() + .then((module) => { + const inner = module.stream(model, context, options); forwardStream(outer, inner); }) .catch((error) => { @@ -144,16 +180,17 @@ function createLazyStream( }; } -function createLazySimpleStream( - api: TApi, - loadModule: () => Promise, -): StreamFunction { +function createLazySimpleStream< + TApi extends Api, + TOptions extends StreamOptions, + TSimpleOptions extends SimpleStreamOptions, +>(loadModule: () => Promise>): StreamFunction { return (model, context, options) => { const outer = new AssistantMessageEventStream(); - loadAndRegisterProvider(api, loadModule) - .then((provider) => { - const inner = provider.streamSimple(model, context, options); + loadModule() + .then((module) => { + const inner = module.streamSimple(model, context, options); forwardStream(outer, inner); }) .catch((error) => { @@ -166,112 +203,199 @@ function createLazySimpleStream( }; } -function createLazyApiProvider( - api: TApi, - loadModule: () => Promise, -): ApiProvider { - return { - api, - stream: createLazyStream(api, loadModule), - streamSimple: createLazySimpleStream(api, loadModule), - }; -} - -function registerBedrockProviderModule(module: BedrockProviderModule): void { - registerApiProvider({ - api: "bedrock-converse-stream", - stream: module.streamBedrock, - streamSimple: module.streamSimpleBedrock, +function loadAnthropicProviderModule(): Promise< + LazyProviderModule<"anthropic-messages", AnthropicOptions, SimpleStreamOptions> +> { + anthropicProviderModulePromise ||= import("./anthropic.ts").then((module) => { + const provider = module as AnthropicProviderModule; + return { + stream: provider.streamAnthropic, + streamSimple: provider.streamSimpleAnthropic, + }; }); + return anthropicProviderModulePromise; } -function loadBedrockProviderModule(): Promise { - const module = bedrockProviderModuleOverride; - if (module) { - return Promise.resolve({ register: () => registerBedrockProviderModule(module) }); +function loadAzureOpenAIResponsesProviderModule(): Promise< + LazyProviderModule<"azure-openai-responses", AzureOpenAIResponsesOptions, SimpleStreamOptions> +> { + azureOpenAIResponsesProviderModulePromise ||= import("./azure-openai-responses.ts").then((module) => { + const provider = module as AzureOpenAIResponsesProviderModule; + return { + stream: provider.streamAzureOpenAIResponses, + streamSimple: provider.streamSimpleAzureOpenAIResponses, + }; + }); + return azureOpenAIResponsesProviderModulePromise; +} + +function loadGoogleProviderModule(): Promise< + LazyProviderModule<"google-generative-ai", GoogleOptions, SimpleStreamOptions> +> { + googleProviderModulePromise ||= import("./google.ts").then((module) => { + const provider = module as GoogleProviderModule; + return { + stream: provider.streamGoogle, + streamSimple: provider.streamSimpleGoogle, + }; + }); + return googleProviderModulePromise; +} + +function loadGoogleVertexProviderModule(): Promise< + LazyProviderModule<"google-vertex", GoogleVertexOptions, SimpleStreamOptions> +> { + googleVertexProviderModulePromise ||= import("./google-vertex.ts").then((module) => { + const provider = module as GoogleVertexProviderModule; + return { + stream: provider.streamGoogleVertex, + streamSimple: provider.streamSimpleGoogleVertex, + }; + }); + return googleVertexProviderModulePromise; +} + +function loadMistralProviderModule(): Promise< + LazyProviderModule<"mistral-conversations", MistralOptions, SimpleStreamOptions> +> { + mistralProviderModulePromise ||= import("./mistral.ts").then((module) => { + const provider = module as MistralProviderModule; + return { + stream: provider.streamMistral, + streamSimple: provider.streamSimpleMistral, + }; + }); + return mistralProviderModulePromise; +} + +function loadOpenAICodexResponsesProviderModule(): Promise< + LazyProviderModule<"openai-codex-responses", OpenAICodexResponsesOptions, SimpleStreamOptions> +> { + openAICodexResponsesProviderModulePromise ||= import("./openai-codex-responses.ts").then((module) => { + const provider = module as OpenAICodexResponsesProviderModule; + return { + stream: provider.streamOpenAICodexResponses, + streamSimple: provider.streamSimpleOpenAICodexResponses, + }; + }); + return openAICodexResponsesProviderModulePromise; +} + +function loadOpenAICompletionsProviderModule(): Promise< + LazyProviderModule<"openai-completions", OpenAICompletionsOptions, SimpleStreamOptions> +> { + openAICompletionsProviderModulePromise ||= import("./openai-completions.ts").then((module) => { + const provider = module as OpenAICompletionsProviderModule; + return { + stream: provider.streamOpenAICompletions, + streamSimple: provider.streamSimpleOpenAICompletions, + }; + }); + return openAICompletionsProviderModulePromise; +} + +function loadOpenAIResponsesProviderModule(): Promise< + LazyProviderModule<"openai-responses", OpenAIResponsesOptions, SimpleStreamOptions> +> { + openAIResponsesProviderModulePromise ||= import("./openai-responses.ts").then((module) => { + const provider = module as OpenAIResponsesProviderModule; + return { + stream: provider.streamOpenAIResponses, + streamSimple: provider.streamSimpleOpenAIResponses, + }; + }); + return openAIResponsesProviderModulePromise; +} + +function loadBedrockProviderModule(): Promise< + LazyProviderModule<"bedrock-converse-stream", BedrockOptions, SimpleStreamOptions> +> { + if (bedrockProviderModuleOverride) { + return Promise.resolve(bedrockProviderModuleOverride); } - return importNodeOnlyProvider("./amazon-bedrock.ts").then((provider) => provider as RegisteringProviderModule); + bedrockProviderModulePromise ||= importNodeOnlyProvider("./amazon-bedrock.ts").then((module) => { + const provider = module as BedrockProviderModule; + return { + stream: provider.streamBedrock, + streamSimple: provider.streamSimpleBedrock, + }; + }); + return bedrockProviderModulePromise; } -const anthropicProvider = createLazyApiProvider<"anthropic-messages", AnthropicOptions>( - "anthropic-messages", - () => import("./anthropic.ts"), -); -const azureOpenAIResponsesProvider = createLazyApiProvider<"azure-openai-responses", AzureOpenAIResponsesOptions>( - "azure-openai-responses", - () => import("./azure-openai-responses.ts"), -); -const googleProvider = createLazyApiProvider<"google-generative-ai", GoogleOptions>( - "google-generative-ai", - () => import("./google.ts"), -); -const googleVertexProvider = createLazyApiProvider<"google-vertex", GoogleVertexOptions>( - "google-vertex", - () => import("./google-vertex.ts"), -); -const mistralProvider = createLazyApiProvider<"mistral-conversations", MistralOptions>( - "mistral-conversations", - () => import("./mistral.ts"), -); -const openAICodexResponsesProvider = createLazyApiProvider<"openai-codex-responses", OpenAICodexResponsesOptions>( - "openai-codex-responses", - () => import("./openai-codex-responses.ts"), -); -const openAICompletionsProvider = createLazyApiProvider<"openai-completions", OpenAICompletionsOptions>( - "openai-completions", - () => import("./openai-completions.ts"), -); -const openAIResponsesProvider = createLazyApiProvider<"openai-responses", OpenAIResponsesOptions>( - "openai-responses", - () => import("./openai-responses.ts"), -); -const bedrockProvider = createLazyApiProvider<"bedrock-converse-stream", BedrockOptions>( - "bedrock-converse-stream", - loadBedrockProviderModule, -); -const openRouterImagesProvider = createLazyImagesApiProvider( - "openrouter-images", - () => import("./images/openrouter.ts"), -); - -export const generateImagesOpenRouter = openRouterImagesProvider.generateImages; -export const streamAnthropic = anthropicProvider.stream; -export const streamSimpleAnthropic = anthropicProvider.streamSimple; -export const streamAzureOpenAIResponses = azureOpenAIResponsesProvider.stream; -export const streamSimpleAzureOpenAIResponses = azureOpenAIResponsesProvider.streamSimple; -export const streamGoogle = googleProvider.stream; -export const streamSimpleGoogle = googleProvider.streamSimple; -export const streamGoogleVertex = googleVertexProvider.stream; -export const streamSimpleGoogleVertex = googleVertexProvider.streamSimple; -export const streamMistral = mistralProvider.stream; -export const streamSimpleMistral = mistralProvider.streamSimple; -export const streamOpenAICodexResponses = openAICodexResponsesProvider.stream; -export const streamSimpleOpenAICodexResponses = openAICodexResponsesProvider.streamSimple; -export const streamOpenAICompletions = openAICompletionsProvider.stream; -export const streamSimpleOpenAICompletions = openAICompletionsProvider.streamSimple; -export const streamOpenAIResponses = openAIResponsesProvider.stream; -export const streamSimpleOpenAIResponses = openAIResponsesProvider.streamSimple; - -const registerBuiltInApiProviderFunctions = [ - () => registerApiProvider(anthropicProvider), - () => registerApiProvider(openAICompletionsProvider), - () => registerApiProvider(mistralProvider), - () => registerApiProvider(openAIResponsesProvider), - () => registerApiProvider(azureOpenAIResponsesProvider), - () => registerApiProvider(openAICodexResponsesProvider), - () => registerApiProvider(googleProvider), - () => registerApiProvider(googleVertexProvider), - () => registerApiProvider(bedrockProvider), -]; - -export function registerBuiltInImagesApiProviders(): void { - registerImagesApiProvider(openRouterImagesProvider); -} +export const streamAnthropic = createLazyStream(loadAnthropicProviderModule); +export const streamSimpleAnthropic = createLazySimpleStream(loadAnthropicProviderModule); +export const streamAzureOpenAIResponses = createLazyStream(loadAzureOpenAIResponsesProviderModule); +export const streamSimpleAzureOpenAIResponses = createLazySimpleStream(loadAzureOpenAIResponsesProviderModule); +export const streamGoogle = createLazyStream(loadGoogleProviderModule); +export const streamSimpleGoogle = createLazySimpleStream(loadGoogleProviderModule); +export const streamGoogleVertex = createLazyStream(loadGoogleVertexProviderModule); +export const streamSimpleGoogleVertex = createLazySimpleStream(loadGoogleVertexProviderModule); +export const streamMistral = createLazyStream(loadMistralProviderModule); +export const streamSimpleMistral = createLazySimpleStream(loadMistralProviderModule); +export const streamOpenAICodexResponses = createLazyStream(loadOpenAICodexResponsesProviderModule); +export const streamSimpleOpenAICodexResponses = createLazySimpleStream(loadOpenAICodexResponsesProviderModule); +export const streamOpenAICompletions = createLazyStream(loadOpenAICompletionsProviderModule); +export const streamSimpleOpenAICompletions = createLazySimpleStream(loadOpenAICompletionsProviderModule); +export const streamOpenAIResponses = createLazyStream(loadOpenAIResponsesProviderModule); +export const streamSimpleOpenAIResponses = createLazySimpleStream(loadOpenAIResponsesProviderModule); +const streamBedrockLazy = createLazyStream(loadBedrockProviderModule); +const streamSimpleBedrockLazy = createLazySimpleStream(loadBedrockProviderModule); export function registerBuiltInApiProviders(): void { - for (const register of registerBuiltInApiProviderFunctions) { - register(); - } + registerApiProvider({ + api: "anthropic-messages", + stream: streamAnthropic, + streamSimple: streamSimpleAnthropic, + }); + + registerApiProvider({ + api: "openai-completions", + stream: streamOpenAICompletions, + streamSimple: streamSimpleOpenAICompletions, + }); + + registerApiProvider({ + api: "mistral-conversations", + stream: streamMistral, + streamSimple: streamSimpleMistral, + }); + + registerApiProvider({ + api: "openai-responses", + stream: streamOpenAIResponses, + streamSimple: streamSimpleOpenAIResponses, + }); + + registerApiProvider({ + api: "azure-openai-responses", + stream: streamAzureOpenAIResponses, + streamSimple: streamSimpleAzureOpenAIResponses, + }); + + registerApiProvider({ + api: "openai-codex-responses", + stream: streamOpenAICodexResponses, + streamSimple: streamSimpleOpenAICodexResponses, + }); + + registerApiProvider({ + api: "google-generative-ai", + stream: streamGoogle, + streamSimple: streamSimpleGoogle, + }); + + registerApiProvider({ + api: "google-vertex", + stream: streamGoogleVertex, + streamSimple: streamSimpleGoogleVertex, + }); + + registerApiProvider({ + api: "bedrock-converse-stream", + stream: streamBedrockLazy, + streamSimple: streamSimpleBedrockLazy, + }); } export function resetApiProviders(): void { diff --git a/packages/ai/src/stream.ts b/packages/ai/src/stream.ts index 7b6b28c6..3f333d9e 100644 --- a/packages/ai/src/stream.ts +++ b/packages/ai/src/stream.ts @@ -1,3 +1,5 @@ +import "./providers/register-builtins.ts"; + import { getApiProvider } from "./api-registry.ts"; import { getEnvApiKey } from "./env-api-keys.ts"; import type { @@ -11,6 +13,8 @@ import type { StreamOptions, } from "./types.ts"; +export { getEnvApiKey } from "./env-api-keys.ts"; + function hasExplicitApiKey(apiKey: string | undefined): apiKey is string { return typeof apiKey === "string" && apiKey.trim().length > 0; } diff --git a/packages/ai/test/abort.test.ts b/packages/ai/test/abort.test.ts index 303044ad..27c274aa 100644 --- a/packages/ai/test/abort.test.ts +++ b/packages/ai/test/abort.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { complete, stream } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { complete, stream } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts b/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts index a9862485..2483e1a4 100644 --- a/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts +++ b/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts @@ -1,8 +1,8 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { getEnvApiKey } from "../src/env-api-keys.ts"; -import { complete } from "../src/index.ts"; import { getModels, getProviders } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { Api, KnownProvider, Model, ProviderStreamOptions, Tool } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts b/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts index 5663e2a8..69e58e27 100644 --- a/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts +++ b/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { streamSimple } from "../src/index.ts"; +import { streamSimple } from "../src/stream.ts"; import type { AssistantMessage, Context, Model } from "../src/types.ts"; interface AnthropicPayload { diff --git a/packages/ai/test/anthropic-force-adaptive-thinking.test.ts b/packages/ai/test/anthropic-force-adaptive-thinking.test.ts index cef2ca48..e797c3a9 100644 --- a/packages/ai/test/anthropic-force-adaptive-thinking.test.ts +++ b/packages/ai/test/anthropic-force-adaptive-thinking.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { streamSimple } from "../src/stream.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface AnthropicThinkingPayload { diff --git a/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts b/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts index 0c9d590f..2b7667d7 100644 --- a/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts +++ b/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { getEnvApiKey } from "../src/env-api-keys.ts"; -import { complete } from "../src/index.ts"; import { getModels, getProviders } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { Api, KnownProvider, Model, ProviderStreamOptions } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/anthropic-opus-4-8-smoke.test.ts b/packages/ai/test/anthropic-opus-4-8-smoke.test.ts index 0c1288b3..bb4b739b 100644 --- a/packages/ai/test/anthropic-opus-4-8-smoke.test.ts +++ b/packages/ai/test/anthropic-opus-4-8-smoke.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { streamSimple } from "../src/stream.ts"; import type { Context } from "../src/types.ts"; interface AnthropicThinkingPayload { diff --git a/packages/ai/test/anthropic-temperature-compat.test.ts b/packages/ai/test/anthropic-temperature-compat.test.ts index 06a05698..00161ab8 100644 --- a/packages/ai/test/anthropic-temperature-compat.test.ts +++ b/packages/ai/test/anthropic-temperature-compat.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { streamSimple } from "../src/stream.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface AnthropicTemperaturePayload { diff --git a/packages/ai/test/anthropic-thinking-disable.test.ts b/packages/ai/test/anthropic-thinking-disable.test.ts index 581d3a34..13d333e5 100644 --- a/packages/ai/test/anthropic-thinking-disable.test.ts +++ b/packages/ai/test/anthropic-thinking-disable.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { streamSimple } from "../src/stream.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface AnthropicThinkingPayload { diff --git a/packages/ai/test/anthropic-tool-name-normalization.test.ts b/packages/ai/test/anthropic-tool-name-normalization.test.ts index d52ff50e..bb454081 100644 --- a/packages/ai/test/anthropic-tool-name-normalization.test.ts +++ b/packages/ai/test/anthropic-tool-name-normalization.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { stream } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { stream } from "../src/stream.ts"; import type { Context, Tool } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/base-entrypoint.test.ts b/packages/ai/test/base-entrypoint.test.ts deleted file mode 100644 index d54fa492..00000000 --- a/packages/ai/test/base-entrypoint.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { clearApiProviders, complete, getApiProvider, getApiProviders } from "../src/base.ts"; -import { register as registerAmazonBedrock } from "../src/providers/amazon-bedrock.ts"; -import { register as registerAnthropic } from "../src/providers/anthropic.ts"; -import { register as registerAzureOpenAIResponses } from "../src/providers/azure-openai-responses.ts"; -import { fauxAssistantMessage, registerFauxProvider } from "../src/providers/faux.ts"; -import { register as registerGoogle } from "../src/providers/google.ts"; -import { register as registerGoogleVertex } from "../src/providers/google-vertex.ts"; -import { register as registerMistral } from "../src/providers/mistral.ts"; -import { register as registerOpenAICodexResponses } from "../src/providers/openai-codex-responses.ts"; -import { register as registerOpenAICompletions } from "../src/providers/openai-completions.ts"; -import { register as registerOpenAIResponses } from "../src/providers/openai-responses.ts"; - -const registrations = [ - ["bedrock-converse-stream", registerAmazonBedrock], - ["anthropic-messages", registerAnthropic], - ["azure-openai-responses", registerAzureOpenAIResponses], - ["google-generative-ai", registerGoogle], - ["google-vertex", registerGoogleVertex], - ["mistral-conversations", registerMistral], - ["openai-codex-responses", registerOpenAICodexResponses], - ["openai-completions", registerOpenAICompletions], - ["openai-responses", registerOpenAIResponses], -] as const; - -afterEach(() => { - clearApiProviders(); -}); - -describe("base entrypoint", () => { - it("starts without built-in provider registrations", () => { - expect(getApiProviders()).toEqual([]); - }); - - it.each(registrations)("registers the %s transport explicitly", (api, register) => { - register(); - expect(getApiProvider(api)?.api).toBe(api); - }); - - it("dispatches custom providers", async () => { - const registration = registerFauxProvider(); - registration.setResponses([fauxAssistantMessage("hello")]); - const response = await complete(registration.getModel(), { - messages: [{ role: "user", content: "hi", timestamp: Date.now() }], - }); - expect(response.content).toEqual([{ type: "text", text: "hello" }]); - }); - - it("fails clearly when no transport is registered", async () => { - await expect( - complete( - { - id: "missing-model", - name: "Missing Model", - api: "missing-api", - provider: "missing-provider", - baseUrl: "https://example.com", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1, - maxTokens: 1, - }, - { messages: [] }, - ), - ).rejects.toThrow("No API provider registered for api: missing-api"); - }); -}); diff --git a/packages/ai/test/bedrock-models.test.ts b/packages/ai/test/bedrock-models.test.ts index 5bd5a1f3..2cfd8fb9 100644 --- a/packages/ai/test/bedrock-models.test.ts +++ b/packages/ai/test/bedrock-models.test.ts @@ -17,8 +17,8 @@ */ import { describe, expect, it } from "vitest"; -import { complete } from "../src/index.ts"; import { getModels } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { Context } from "../src/types.ts"; import { hasBedrockCredentials } from "./bedrock-utils.ts"; diff --git a/packages/ai/test/cache-retention.test.ts b/packages/ai/test/cache-retention.test.ts index b5c55198..6e2c1a5b 100644 --- a/packages/ai/test/cache-retention.test.ts +++ b/packages/ai/test/cache-retention.test.ts @@ -1,10 +1,10 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { stream } from "../src/index.ts"; import { MODELS } from "../src/models.generated.ts"; import { getModel } from "../src/models.ts"; import { streamAnthropic } from "../src/providers/anthropic.ts"; import { streamOpenAICompletions } from "../src/providers/openai-completions.ts"; import { streamOpenAIResponses } from "../src/providers/openai-responses.ts"; +import { stream } from "../src/stream.ts"; import type { Context, Model } from "../src/types.ts"; class PayloadCaptured extends Error { diff --git a/packages/ai/test/context-overflow.test.ts b/packages/ai/test/context-overflow.test.ts index 54087a29..9f021a41 100644 --- a/packages/ai/test/context-overflow.test.ts +++ b/packages/ai/test/context-overflow.test.ts @@ -14,8 +14,8 @@ import type { ChildProcess } from "child_process"; import { execSync, spawn } from "child_process"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { complete } from "../src/index.ts"; import { getModel, getModels } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { AssistantMessage, Context, Model, Usage } from "../src/types.ts"; import { isContextOverflow } from "../src/utils/overflow.ts"; import { hasAzureOpenAICredentials } from "./azure-utils.ts"; diff --git a/packages/ai/test/cross-provider-handoff.test.ts b/packages/ai/test/cross-provider-handoff.test.ts index 73fac229..23593394 100644 --- a/packages/ai/test/cross-provider-handoff.test.ts +++ b/packages/ai/test/cross-provider-handoff.test.ts @@ -25,8 +25,8 @@ import { writeFileSync } from "fs"; import { Type } from "typebox"; import { beforeAll, describe, expect, it } from "vitest"; -import { completeSimple, getEnvApiKey } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { completeSimple, getEnvApiKey } from "../src/stream.ts"; import type { Api, AssistantMessage, Message, Model, Tool, ToolResultMessage } from "../src/types.ts"; import { hasAzureOpenAICredentials } from "./azure-utils.ts"; import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.ts"; diff --git a/packages/ai/test/empty.test.ts b/packages/ai/test/empty.test.ts index fadd67a9..a8453dcc 100644 --- a/packages/ai/test/empty.test.ts +++ b/packages/ai/test/empty.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { Api, AssistantMessage, Context, Model, StreamOptions, UserMessage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/google-thinking-disable.test.ts b/packages/ai/test/google-thinking-disable.test.ts index 3920f4c8..30df3638 100644 --- a/packages/ai/test/google-thinking-disable.test.ts +++ b/packages/ai/test/google-thinking-disable.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { streamSimple } from "../src/stream.ts"; import type { Api, Context, Model, SimpleStreamOptions } from "../src/types.ts"; type SimpleOptionsWithExtras = SimpleStreamOptions & Record; diff --git a/packages/ai/test/interleaved-thinking.test.ts b/packages/ai/test/interleaved-thinking.test.ts index a2d3f5a6..4cf387ce 100644 --- a/packages/ai/test/interleaved-thinking.test.ts +++ b/packages/ai/test/interleaved-thinking.test.ts @@ -1,8 +1,8 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { getEnvApiKey } from "../src/env-api-keys.ts"; -import { completeSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { completeSimple } from "../src/stream.ts"; import type { Api, Context, Model, StopReason, Tool, ToolCall, ToolResultMessage } from "../src/types.ts"; import { StringEnum } from "../src/utils/typebox-helpers.ts"; import { hasBedrockCredentials } from "./bedrock-utils.ts"; diff --git a/packages/ai/test/lazy-module-load.test.ts b/packages/ai/test/lazy-module-load.test.ts index 14bccff0..e21f0d12 100644 --- a/packages/ai/test/lazy-module-load.test.ts +++ b/packages/ai/test/lazy-module-load.test.ts @@ -5,7 +5,6 @@ import { describe, expect, it } from "vitest"; const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const aiEntryUrl = new URL("../src/index.ts", import.meta.url).href; -const baseEntryUrl = new URL("../src/base.ts", import.meta.url).href; const SDK_SPECIFIERS = [ "@anthropic-ai/sdk", @@ -17,10 +16,9 @@ const SDK_SPECIFIERS = [ type ProbeResult = { loadedSpecifiers: string[]; - value?: unknown; }; -function runProbe(action: string, entryUrl = aiEntryUrl): ProbeResult { +function runProbe(action: string): ProbeResult { const script = ` import { registerHooks } from "node:module"; @@ -36,11 +34,9 @@ function runProbe(action: string, entryUrl = aiEntryUrl): ProbeResult { }, }); - const mod = await import(${JSON.stringify(entryUrl)}); - const value = await (async () => { - ${action} - })(); - console.log(JSON.stringify({ loadedSpecifiers: [...new Set(loaded)], value })); + const mod = await import(${JSON.stringify(aiEntryUrl)}); + ${action} + console.log(JSON.stringify({ loadedSpecifiers: [...new Set(loaded)] })); `; const result = spawnSync(process.execPath, ["--input-type=module", "--eval", script], { @@ -70,33 +66,6 @@ describe("lazy provider module loading", () => { expect(result.loadedSpecifiers).toEqual([]); }); - it("registers built-in transports when importing the root barrel", () => { - const result = runProbe(`return mod.getApiProviders().map((provider) => provider.api).sort();`); - expect(result.value).toEqual([ - "anthropic-messages", - "azure-openai-responses", - "bedrock-converse-stream", - "google-generative-ai", - "google-vertex", - "mistral-conversations", - "openai-codex-responses", - "openai-completions", - "openai-responses", - ]); - }); - - it("registers built-in image transports when importing the root barrel", () => { - const result = runProbe(`return mod.getImagesApiProvider("openrouter-images")?.api;`); - expect(result.loadedSpecifiers).toEqual([]); - expect(result.value).toBe("openrouter-images"); - }); - - it("does not load provider SDKs or register transports when importing the base barrel", () => { - const result = runProbe(`return mod.getApiProviders().map((provider) => provider.api);`, baseEntryUrl); - expect(result.loadedSpecifiers).toEqual([]); - expect(result.value).toEqual([]); - }); - it("loads only the Anthropic SDK when calling the root lazy wrapper", () => { const result = runProbe(` const model = { @@ -127,17 +96,4 @@ describe("lazy provider module loading", () => { expect(result.loadedSpecifiers).toEqual(["@anthropic-ai/sdk"]); }); - - it("dispatches through a lazy wrapper again after resetting providers", () => { - const result = runProbe(` - const model = mod.getModel("anthropic", "claude-sonnet-4-6"); - const context = { messages: [{ role: "user", content: "hi" }] }; - await mod.streamSimple(model, context).result(); - mod.resetApiProviders(); - return (await mod.streamSimple(model, context).result()).stopReason; - `); - - expect(result.loadedSpecifiers).toEqual(["@anthropic-ai/sdk"]); - expect(result.value).toBe("error"); - }); }); diff --git a/packages/ai/test/mistral-reasoning-mode.test.ts b/packages/ai/test/mistral-reasoning-mode.test.ts index 0e2f45c5..d197a56a 100644 --- a/packages/ai/test/mistral-reasoning-mode.test.ts +++ b/packages/ai/test/mistral-reasoning-mode.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { streamSimple } from "../src/stream.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface MistralPayload { diff --git a/packages/ai/test/mistral-tool-schema.test.ts b/packages/ai/test/mistral-tool-schema.test.ts index 3c490c6d..c6898fc6 100644 --- a/packages/ai/test/mistral-tool-schema.test.ts +++ b/packages/ai/test/mistral-tool-schema.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { Context, Model } from "../src/types.ts"; interface MistralToolPayload { diff --git a/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts b/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts index 62245dcf..0abb46f6 100644 --- a/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts +++ b/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { Context } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/openai-completions-empty-tools.test.ts b/packages/ai/test/openai-completions-empty-tools.test.ts index 0ecf8e13..be233670 100644 --- a/packages/ai/test/openai-completions-empty-tools.test.ts +++ b/packages/ai/test/openai-completions-empty-tools.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { streamSimple } from "../src/stream.ts"; // Empty tools arrays must NOT be serialized as `tools: []` — some OpenAI-compatible // backends (e.g. DashScope / Aliyun Qwen via compatible-mode) reject the request with diff --git a/packages/ai/test/openai-completions-response-model.test.ts b/packages/ai/test/openai-completions-response-model.test.ts index ec2abea1..d8e4d2f9 100644 --- a/packages/ai/test/openai-completions-response-model.test.ts +++ b/packages/ai/test/openai-completions-response-model.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { complete } from "../src/index.ts"; +import { complete } from "../src/stream.ts"; import type { Model } from "../src/types.ts"; // Router/virtual ids (e.g. OpenRouter `auto`) keep `model` pinned to the diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index 41601281..e3b34681 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -1,8 +1,8 @@ import { Type } from "typebox"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { stream, streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; import { convertMessages } from "../src/providers/openai-completions.ts"; +import { stream, streamSimple } from "../src/stream.ts"; import type { AssistantMessage, Model, SimpleStreamOptions, Tool, ToolResultMessage } from "../src/types.ts"; const mockState = vi.hoisted(() => ({ diff --git a/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts b/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts index 0d247b0b..d173694f 100644 --- a/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts +++ b/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { Context } from "../src/types.ts"; describe.skipIf(!process.env.OPENAI_API_KEY)("openai responses cache affinity e2e", () => { diff --git a/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts b/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts index 87945bc2..aa6f2e23 100644 --- a/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts +++ b/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { complete, getEnvApiKey } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { complete, getEnvApiKey } from "../src/stream.ts"; import type { AssistantMessage, Context, Message, Tool, ToolCall } from "../src/types.ts"; const testToolSchema = Type.Object({ diff --git a/packages/ai/test/openrouter-cache-write-repro.test.ts b/packages/ai/test/openrouter-cache-write-repro.test.ts index 2cf2a791..4bdeb286 100644 --- a/packages/ai/test/openrouter-cache-write-repro.test.ts +++ b/packages/ai/test/openrouter-cache-write-repro.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { completeSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { completeSimple } from "../src/stream.ts"; function createLongSystemPrompt(): string { const nonce = `${Date.now()}-${Math.random()}`; diff --git a/packages/ai/test/openrouter-images.test.ts b/packages/ai/test/openrouter-images.test.ts index 5811e6f4..31882bec 100644 --- a/packages/ai/test/openrouter-images.test.ts +++ b/packages/ai/test/openrouter-images.test.ts @@ -1,8 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { generateImages } from "../src/images.ts"; -import { clearImagesApiProviders, getImagesApiProvider } from "../src/images-api-registry.ts"; -import { register as registerOpenRouterImages } from "../src/providers/images/openrouter.ts"; -import { registerBuiltInImagesApiProviders } from "../src/providers/register-builtins.ts"; import type { ImagesContext, ImagesModel } from "../src/types.ts"; const mockState = vi.hoisted(() => ({ @@ -65,15 +62,6 @@ describe("openrouter images", () => { beforeEach(() => { mockState.lastParams = undefined; mockState.lastRequestOptions = undefined; - clearImagesApiProviders(); - registerBuiltInImagesApiProviders(); - }); - - it("registers the direct OpenRouter images transport explicitly", () => { - clearImagesApiProviders(); - expect(getImagesApiProvider("openrouter-images")).toBeUndefined(); - registerOpenRouterImages(); - expect(getImagesApiProvider("openrouter-images")?.api).toBe("openrouter-images"); }); it("returns text plus images in final output", async () => { diff --git a/packages/ai/test/responseid.test.ts b/packages/ai/test/responseid.test.ts index bc5d6c54..d250f5f4 100644 --- a/packages/ai/test/responseid.test.ts +++ b/packages/ai/test/responseid.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions } from "../src/types.ts"; import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index 3a381966..9a70b6c2 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -4,8 +4,8 @@ import { dirname, join } from "path"; import { Type } from "typebox"; import { fileURLToPath } from "url"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { complete, stream } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { complete, stream } from "../src/stream.ts"; import type { Api, Context, ImageContent, Model, StreamOptions, Tool, ToolResultMessage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/tokens.test.ts b/packages/ai/test/tokens.test.ts index fdf45b94..676f5a29 100644 --- a/packages/ai/test/tokens.test.ts +++ b/packages/ai/test/tokens.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { stream } from "../src/index.ts"; import { getModel, getModels } from "../src/models.ts"; +import { stream } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/tool-call-id-normalization.test.ts b/packages/ai/test/tool-call-id-normalization.test.ts index 622e5127..0672181b 100644 --- a/packages/ai/test/tool-call-id-normalization.test.ts +++ b/packages/ai/test/tool-call-id-normalization.test.ts @@ -12,8 +12,8 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { completeSimple, getEnvApiKey } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { completeSimple, getEnvApiKey } from "../src/stream.ts"; import type { AssistantMessage, Message, Tool, ToolResultMessage } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/tool-call-without-result.test.ts b/packages/ai/test/tool-call-without-result.test.ts index b21c1ce8..11b832f3 100644 --- a/packages/ai/test/tool-call-without-result.test.ts +++ b/packages/ai/test/tool-call-without-result.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions, Tool } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/total-tokens.test.ts b/packages/ai/test/total-tokens.test.ts index 4df4b2e1..972913a7 100644 --- a/packages/ai/test/total-tokens.test.ts +++ b/packages/ai/test/total-tokens.test.ts @@ -13,8 +13,8 @@ */ import { describe, expect, it } from "vitest"; -import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions, Usage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/unicode-surrogate.test.ts b/packages/ai/test/unicode-surrogate.test.ts index d8b02392..9cdddefa 100644 --- a/packages/ai/test/unicode-surrogate.test.ts +++ b/packages/ai/test/unicode-surrogate.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions, ToolResultMessage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/xhigh.test.ts b/packages/ai/test/xhigh.test.ts index 1bda5c14..3c279863 100644 --- a/packages/ai/test/xhigh.test.ts +++ b/packages/ai/test/xhigh.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { stream } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { stream } from "../src/stream.ts"; import type { Context, Model } from "../src/types.ts"; function makeContext(): Context { diff --git a/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts b/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts index 9a371b60..ab2fec30 100644 --- a/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts +++ b/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { completeSimple, getEnvApiKey, streamSimple } from "../src/index.ts"; +import { completeSimple, getEnvApiKey, streamSimple } from "../src/stream.ts"; import type { AssistantMessage, Context, Model } from "../src/types.ts"; const provider = "xiaomi-token-plan-ams"; diff --git a/packages/ai/test/zen.test.ts b/packages/ai/test/zen.test.ts index 82a2079f..8ca80014 100644 --- a/packages/ai/test/zen.test.ts +++ b/packages/ai/test/zen.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { complete } from "../src/index.ts"; import { MODELS } from "../src/models.generated.ts"; +import { complete } from "../src/stream.ts"; import type { Model } from "../src/types.ts"; describe.skipIf(!process.env.OPENCODE_API_KEY)("OpenCode Models Smoke Test", () => { diff --git a/packages/coding-agent/vitest.config.ts b/packages/coding-agent/vitest.config.ts index 5759e3c4..67ce0fca 100644 --- a/packages/coding-agent/vitest.config.ts +++ b/packages/coding-agent/vitest.config.ts @@ -2,11 +2,8 @@ import { fileURLToPath } from "node:url"; import { defineConfig } from "vitest/config"; const aiSrcIndex = fileURLToPath(new URL("../ai/src/index.ts", import.meta.url)); -const aiSrcBase = fileURLToPath(new URL("../ai/src/base.ts", import.meta.url)); const aiSrcOAuth = fileURLToPath(new URL("../ai/src/oauth.ts", import.meta.url)); -const aiOpenRouterImages = fileURLToPath(new URL("../ai/src/providers/images/openrouter.ts", import.meta.url)); const agentSrcIndex = fileURLToPath(new URL("../agent/src/index.ts", import.meta.url)); -const agentSrcBase = fileURLToPath(new URL("../agent/src/base.ts", import.meta.url)); const tuiSrcIndex = fileURLToPath(new URL("../tui/src/index.ts", import.meta.url)); export default defineConfig({ @@ -23,18 +20,12 @@ export default defineConfig({ resolve: { alias: [ { find: /^@earendil-works\/pi-ai$/, replacement: aiSrcIndex }, - { find: /^@earendil-works\/pi-ai\/base$/, replacement: aiSrcBase }, { find: /^@earendil-works\/pi-ai\/oauth$/, replacement: aiSrcOAuth }, - { find: /^@earendil-works\/pi-ai\/openrouter-images$/, replacement: aiOpenRouterImages }, { find: /^@earendil-works\/pi-agent-core$/, replacement: agentSrcIndex }, - { find: /^@earendil-works\/pi-agent-core\/base$/, replacement: agentSrcBase }, { find: /^@earendil-works\/pi-tui$/, replacement: tuiSrcIndex }, { find: /^@mariozechner\/pi-ai$/, replacement: aiSrcIndex }, - { find: /^@mariozechner\/pi-ai\/base$/, replacement: aiSrcBase }, { find: /^@mariozechner\/pi-ai\/oauth$/, replacement: aiSrcOAuth }, - { find: /^@mariozechner\/pi-ai\/openrouter-images$/, replacement: aiOpenRouterImages }, { find: /^@mariozechner\/pi-agent-core$/, replacement: agentSrcIndex }, - { find: /^@mariozechner\/pi-agent-core\/base$/, replacement: agentSrcBase }, { find: /^@mariozechner\/pi-tui$/, replacement: tuiSrcIndex }, ], }, diff --git a/scripts/check-browser-smoke.mjs b/scripts/check-browser-smoke.mjs index 906eccd3..d590988d 100644 --- a/scripts/check-browser-smoke.mjs +++ b/scripts/check-browser-smoke.mjs @@ -4,21 +4,7 @@ import { join } from "node:path"; import { build } from "esbuild"; const outputPath = join(tmpdir(), "pi-browser-smoke.js"); -const baseOutputPath = join(tmpdir(), "pi-browser-base-smoke.js"); -const selectiveOutputPath = join(tmpdir(), "pi-browser-selective-smoke.js"); const errorLogPath = join(tmpdir(), "pi-browser-smoke-errors.log"); -const providerImplementationInputs = [ - "packages/ai/src/providers/amazon-bedrock.ts", - "packages/ai/src/providers/anthropic.ts", - "packages/ai/src/providers/azure-openai-responses.ts", - "packages/ai/src/providers/google.ts", - "packages/ai/src/providers/google-vertex.ts", - "packages/ai/src/providers/images/openrouter.ts", - "packages/ai/src/providers/mistral.ts", - "packages/ai/src/providers/openai-codex-responses.ts", - "packages/ai/src/providers/openai-completions.ts", - "packages/ai/src/providers/openai-responses.ts", -]; try { await build({ @@ -29,36 +15,6 @@ try { logLevel: "silent", outfile: outputPath, }); - const baseBuild = await build({ - stdin: { - contents: `import { complete } from "@earendil-works/pi-ai/base";\nimport { Agent } from "@earendil-works/pi-agent-core/base";\nconsole.log(typeof complete, typeof Agent);\n`, - resolveDir: process.cwd(), - sourcefile: "pi-browser-base-smoke-entry.ts", - }, - bundle: true, - platform: "browser", - format: "esm", - logLevel: "silent", - metafile: true, - outfile: baseOutputPath, - }); - const bundledInputs = new Set(Object.keys(baseBuild.metafile.inputs)); - const reachableProviderImplementations = providerImplementationInputs.filter((input) => bundledInputs.has(input)); - if (reachableProviderImplementations.length > 0) { - throw new Error(`Base browser bundle reached provider implementations:\n${reachableProviderImplementations.join("\n")}`); - } - await build({ - stdin: { - contents: `import { register as registerAnthropic } from "@earendil-works/pi-ai/anthropic";\nimport { register as registerOpenAICompletions } from "@earendil-works/pi-ai/openai-completions";\nimport { register as registerOpenRouterImages } from "@earendil-works/pi-ai/openrouter-images";\nconsole.log(typeof registerAnthropic, typeof registerOpenAICompletions, typeof registerOpenRouterImages);\n`, - resolveDir: process.cwd(), - sourcefile: "pi-browser-selective-smoke-entry.ts", - }, - bundle: true, - platform: "browser", - format: "esm", - logLevel: "silent", - outfile: selectiveOutputPath, - }); process.exit(0); } catch (error) { let detailedErrors = ""; diff --git a/tsconfig.json b/tsconfig.json index bcb12eee..903f80f3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,6 @@ "*": ["./*"], "@earendil-works/pi-ai": ["./packages/ai/src/index.ts"], "@earendil-works/pi-ai/oauth": ["./packages/ai/src/oauth.ts"], - "@earendil-works/pi-ai/openrouter-images": ["./packages/ai/src/providers/images/openrouter.ts"], "@earendil-works/pi-ai/*": ["./packages/ai/src/*.ts", "./packages/ai/src/providers/*.ts"], "@earendil-works/pi-ai/dist/*": ["./packages/ai/src/*"], "@earendil-works/pi-agent-core": ["./packages/agent/src/index.ts"], From 4f71b2d3b7be1441dec4619ac19b3c2778b172a8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 22 Jun 2026 13:03:50 +0200 Subject: [PATCH 076/122] fix(coding-agent): clarify ZAI Coding Plan label closes #5965 --- packages/ai/README.md | 4 ++-- packages/coding-agent/CHANGELOG.md | 4 ++++ packages/coding-agent/README.md | 2 +- packages/coding-agent/docs/providers.md | 2 +- packages/coding-agent/src/cli/args.ts | 2 +- packages/coding-agent/src/core/provider-display-names.ts | 2 +- packages/coding-agent/test/model-registry.test.ts | 1 + 7 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/ai/README.md b/packages/ai/README.md index 428ef259..7fcd359b 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -68,7 +68,7 @@ Unified LLM API with automatic model discovery, provider configuration, token an - **xAI** - **OpenRouter** - **Vercel AI Gateway** -- **ZAI** (with separate Coding Plan China provider) +- **ZAI Coding Plan (Global)** (with separate China provider) - **MiniMax** - **Together AI** - **GitHub Copilot** (requires OAuth, see below) @@ -1123,7 +1123,7 @@ In Node.js environments, you can set environment variables to avoid passing API | Together AI | `TOGETHER_API_KEY` | | OpenRouter | `OPENROUTER_API_KEY` | | Vercel AI Gateway | `AI_GATEWAY_API_KEY` | -| zAI | `ZAI_API_KEY` | +| ZAI Coding Plan (Global) | `ZAI_API_KEY` | | ZAI Coding Plan (China) | `ZAI_CODING_CN_API_KEY` | | MiniMax | `MINIMAX_API_KEY` | | OpenCode Zen / OpenCode Go | `OPENCODE_API_KEY` | diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a405f908..989572ba 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Renamed the displayed `zai` provider label to ZAI Coding Plan (Global) for clarity ([#5965](https://github.com/earendil-works/pi/issues/5965)). + ## [0.79.10] - 2026-06-22 ### New Features diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 5dc991cc..765c7a2f 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -121,7 +121,7 @@ For each built-in provider, pi maintains a list of tool-capable models, updated - xAI - OpenRouter - Vercel AI Gateway -- ZAI +- ZAI Coding Plan (Global) - ZAI Coding Plan (China) - OpenCode Zen - OpenCode Go diff --git a/packages/coding-agent/docs/providers.md b/packages/coding-agent/docs/providers.md index 86f1de06..e97eba5d 100644 --- a/packages/coding-agent/docs/providers.md +++ b/packages/coding-agent/docs/providers.md @@ -63,7 +63,7 @@ pi | xAI | `XAI_API_KEY` | `xai` | | OpenRouter | `OPENROUTER_API_KEY` | `openrouter` | | Vercel AI Gateway | `AI_GATEWAY_API_KEY` | `vercel-ai-gateway` | -| ZAI | `ZAI_API_KEY` | `zai` | +| ZAI Coding Plan (Global) | `ZAI_API_KEY` | `zai` | | ZAI Coding Plan (China) | `ZAI_CODING_CN_API_KEY` | `zai-coding-cn` | | OpenCode Zen | `OPENCODE_API_KEY` | `opencode` | | OpenCode Go | `OPENCODE_API_KEY` | `opencode-go` | diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index 5d3c4af8..899241c4 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -352,7 +352,7 @@ ${chalk.bold("Environment Variables:")} TOGETHER_API_KEY - Together AI API key OPENROUTER_API_KEY - OpenRouter API key AI_GATEWAY_API_KEY - Vercel AI Gateway API key - ZAI_API_KEY - ZAI API key + ZAI_API_KEY - ZAI Coding Plan API key (Global) ZAI_CODING_CN_API_KEY - ZAI Coding Plan API key (China) MISTRAL_API_KEY - Mistral API key MINIMAX_API_KEY - MiniMax API key diff --git a/packages/coding-agent/src/core/provider-display-names.ts b/packages/coding-agent/src/core/provider-display-names.ts index 9b0371d2..d33c3d7d 100644 --- a/packages/coding-agent/src/core/provider-display-names.ts +++ b/packages/coding-agent/src/core/provider-display-names.ts @@ -26,7 +26,7 @@ export const BUILT_IN_PROVIDER_DISPLAY_NAMES: Record = { together: "Together AI", "vercel-ai-gateway": "Vercel AI Gateway", xai: "xAI", - zai: "ZAI", + zai: "ZAI Coding Plan (Global)", "zai-coding-cn": "ZAI Coding Plan (China)", xiaomi: "Xiaomi MiMo", "xiaomi-token-plan-cn": "Xiaomi MiMo Token Plan (China)", diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index 1479808a..bc6496d8 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -885,6 +885,7 @@ describe("ModelRegistry", () => { expect(registry.getProviderDisplayName("openai")).toBe("OpenAI"); expect(registry.getProviderDisplayName("github-copilot")).toBe("GitHub Copilot"); + expect(registry.getProviderDisplayName("zai")).toBe("ZAI Coding Plan (Global)"); expect(registry.getProviderDisplayName("unknown-provider")).toBe("unknown-provider"); registry.registerProvider("named-provider", { From 71ca9b2b925d7bde8d447c475617e83d62cfdca9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 22 Jun 2026 13:45:44 +0200 Subject: [PATCH 077/122] fix(ai): expose OpenCode Go GLM-5.2 xhigh effort closes #5967 --- packages/ai/CHANGELOG.md | 4 ++++ packages/ai/scripts/generate-models.ts | 11 +++++++++++ packages/ai/src/models.generated.ts | 1 + 3 files changed, 16 insertions(+) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 93a85598..e61d93dc 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed OpenCode Go GLM-5.2 metadata to expose `xhigh` reasoning and send `reasoning_effort: "max"` ([#5967](https://github.com/earendil-works/pi/issues/5967)). + ## [0.79.10] - 2026-06-22 ### Fixed diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 3e85db82..4f9cc78d 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -164,6 +164,14 @@ const ZAI_GLM52_THINKING_LEVEL_MAP = { high: "high", xhigh: "max", } as const; +const OPENCODE_GO_GLM52_THINKING_LEVEL_MAP = { + off: null, + minimal: null, + low: null, + medium: null, + high: "high", + xhigh: "max", +} as const; const EAGER_TOOL_INPUT_STREAMING_UNSUPPORTED_ANTHROPIC_MODELS = new Set([ "github-copilot:claude-haiku-4.5", "github-copilot:claude-sonnet-4", @@ -380,6 +388,9 @@ function applyThinkingLevelMetadata(model: Model): void { if (model.provider === "fireworks" && model.id === "accounts/fireworks/models/glm-5p2") { mergeThinkingLevelMap(model, { off: "none", minimal: null, low: "high", medium: "high", xhigh: "max" }); } + if (model.provider === "opencode-go" && model.id === "glm-5.2") { + mergeThinkingLevelMap(model, OPENCODE_GO_GLM52_THINKING_LEVEL_MAP); + } if (model.provider === "opencode-go" && model.id === "kimi-k2.6") { // OpenCode Go exposes Kimi K2.6 thinking as on/off, not distinct effort tiers. mergeThinkingLevelMap(model, { minimal: null, low: null, medium: null }); diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index cfc69879..64325817 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -8594,6 +8594,7 @@ export const MODELS = { baseUrl: "https://opencode.ai/zen/go/v1", compat: {"maxTokensField":"max_tokens"}, reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], cost: { input: 1.4, From 3b5613469470d2369784a0b49394c74b9c5355dd Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 22 Jun 2026 13:57:02 +0200 Subject: [PATCH 078/122] fix(tui): bind ctrl+j as newline by default --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/docs/keybindings.md | 2 +- packages/coding-agent/docs/terminal-setup.md | 8 +------- packages/tui/CHANGELOG.md | 4 ++++ packages/tui/src/keybindings.ts | 2 +- packages/tui/test/keybindings.test.ts | 8 ++++++++ 6 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 989572ba..69e2d8cb 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Changed +- Added `Ctrl+J` as a default newline keybinding alongside `Shift+Enter`. - Renamed the displayed `zai` provider label to ZAI Coding Plan (Global) for clarity ([#5965](https://github.com/earendil-works/pi/issues/5965)). ## [0.79.10] - 2026-06-22 diff --git a/packages/coding-agent/docs/keybindings.md b/packages/coding-agent/docs/keybindings.md index 333b94e3..a15a25a0 100644 --- a/packages/coding-agent/docs/keybindings.md +++ b/packages/coding-agent/docs/keybindings.md @@ -54,7 +54,7 @@ Modifier combinations: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `ctrl+1 | Keybinding id | Default | Description | |--------|---------|-------------| -| `tui.input.newLine` | `shift+enter` | Insert new line | +| `tui.input.newLine` | `shift+enter`, `ctrl+j` | Insert new line | | `tui.input.submit` | `enter` | Submit input | | `tui.input.tab` | `tab` | Tab / autocomplete | diff --git a/packages/coding-agent/docs/terminal-setup.md b/packages/coding-agent/docs/terminal-setup.md index ff1638b5..d2a8fb92 100644 --- a/packages/coding-agent/docs/terminal-setup.md +++ b/packages/coding-agent/docs/terminal-setup.md @@ -30,13 +30,7 @@ That mapping sends a raw linefeed byte. Inside pi, that is indistinguishable fro If Claude Code 2.x or newer is the only reason you added that mapping, you can remove it, unless you want to use Claude Code in tmux, where it still requires that Ghostty mapping. -If you want `Shift+Enter` to keep working in tmux via that remap, add `ctrl+j` to your pi `newLine` keybinding in `~/.pi/agent/keybindings.json`: - -```json -{ - "newLine": ["shift+enter", "ctrl+j"] -} -``` +Pi binds `Ctrl+J` as a default newline alias, so `Shift+Enter` keeps working in tmux via that remap without extra pi configuration. ## WezTerm diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 0a34ae38..a3cf70ef 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Added `Ctrl+J` as a default newline keybinding alongside `Shift+Enter`. + ## [0.79.10] - 2026-06-22 ## [0.79.9] - 2026-06-20 diff --git a/packages/tui/src/keybindings.ts b/packages/tui/src/keybindings.ts index 789124ad..eaf0838b 100644 --- a/packages/tui/src/keybindings.ts +++ b/packages/tui/src/keybindings.ts @@ -115,7 +115,7 @@ export const TUI_KEYBINDINGS = { "tui.editor.yank": { defaultKeys: "ctrl+y", description: "Yank" }, "tui.editor.yankPop": { defaultKeys: "alt+y", description: "Yank pop" }, "tui.editor.undo": { defaultKeys: "ctrl+-", description: "Undo" }, - "tui.input.newLine": { defaultKeys: "shift+enter", description: "Insert newline" }, + "tui.input.newLine": { defaultKeys: ["shift+enter", "ctrl+j"], description: "Insert newline" }, "tui.input.submit": { defaultKeys: "enter", description: "Submit input" }, "tui.input.tab": { defaultKeys: "tab", description: "Tab / autocomplete" }, "tui.input.copy": { defaultKeys: "ctrl+c", description: "Copy selection" }, diff --git a/packages/tui/test/keybindings.test.ts b/packages/tui/test/keybindings.test.ts index 37c67442..8bd7f438 100644 --- a/packages/tui/test/keybindings.test.ts +++ b/packages/tui/test/keybindings.test.ts @@ -3,6 +3,14 @@ import { describe, it } from "node:test"; import { KeybindingsManager, TUI_KEYBINDINGS } from "../src/keybindings.ts"; describe("KeybindingsManager", () => { + it("binds Ctrl+J as a default newline alias", () => { + const keybindings = new KeybindingsManager(TUI_KEYBINDINGS); + + assert.deepStrictEqual(keybindings.getKeys("tui.input.newLine"), ["shift+enter", "ctrl+j"]); + assert.strictEqual(keybindings.matches("\n", "tui.input.newLine"), true); + assert.strictEqual(keybindings.matches("\x1b[106;5u", "tui.input.newLine"), true); + }); + it("does not evict selector confirm when input submit is rebound", () => { const keybindings = new KeybindingsManager(TUI_KEYBINDINGS, { "tui.input.submit": ["enter", "ctrl+enter"], From d2677a6314f6b2aceed6763aef66b434d6ac3d24 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 22 Jun 2026 15:04:16 +0200 Subject: [PATCH 079/122] docs(agent): mark sync models API complete --- packages/agent/docs/models.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/agent/docs/models.md b/packages/agent/docs/models.md index 05307be5..2395b030 100644 --- a/packages/agent/docs/models.md +++ b/packages/agent/docs/models.md @@ -93,7 +93,7 @@ import { openaiProvider } from "@earendil-works/pi-ai/providers/openai"; const models = createModels(); models.setProvider(openaiProvider()); -const model = await models.getModel("openai", "gpt-4o-mini"); +const model = models.getModel("openai", "gpt-4o-mini"); if (!model) throw new Error("model not found"); const response = await models.complete(model, context); @@ -261,7 +261,7 @@ Runtime model lists are dynamic, so `models.getModel()`/`getModels()` honestly r ```ts export function hasApi(model: Model, api: TApi): model is Model; - const model = await models.getModel("anthropic", "claude-opus-4-7"); + const model = models.getModel("anthropic", "claude-opus-4-7"); if (model && hasApi(model, "anthropic-messages")) { // model: Model<"anthropic-messages">, stream options fully typed } @@ -724,7 +724,7 @@ getBuiltinModels(provider) // sync getBuiltinProviders() // sync ``` -Runtime lookup is always the async instance API: `await models.getModel(...)`. +Runtime lookup through a `Models` instance is sync over the last-known provider lists: `models.getModel(...)`. Freshness-critical callers run `await models.refresh(provider)` first. Generated catalogs are split per provider (`providers/.models.ts`) by updating `packages/ai/scripts/generate-models.ts`. If the generator change turns out too large for this pass, splitting may be deferred; `providers/all` and provider factories may temporarily import the monolithic `models.generated.ts`, relying on `sideEffects: false` for pruning. @@ -801,7 +801,7 @@ Check items off as they land. Keep this list current; it is the working state fo - [x] Rename `types.ts` `Provider` alias to `ProviderId`; fix call sites. - [x] Add `ApiOptionsMap` and `ApiStreamOptions` to `types.ts` (type-only imports). - [x] New `models.ts`: `Provider` interface, `hasApi()` guard, `ModelsError` + codes. Auth types live in `src/auth/types.ts` (`ProviderAuth` = `{ apiKey?, oauth? }`, credentials, `CredentialStore` (`read`/`modify`/`delete`, one credential per provider), `AuthResult`, `AuthContext`, `ModelAuth`, login callbacks), in-memory store in `src/auth/credential-store.ts`, default context in `src/auth/context.ts` (browser-safe node:fs trick), `lazyStream()` in `src/api/lazy.ts`. -- [x] `Models`/`MutableModels`/`createModels({ credentials?, authContext? })` with provider map, async `getModel(s)` (per-provider failure isolation), `getAuth` (decision tree, double-checked locked refresh), `stream/complete/streamSimple/completeSimple` with per-field auth merge. Tests: `packages/ai/test/models-runtime.test.ts`. +- [x] `Models`/`MutableModels`/`createModels({ credentials?, authContext? })` with provider map, sync `getModel(s)` (per-provider failure isolation), explicit async `refresh(provider?)`, `getAuth` (decision tree, double-checked locked refresh), `stream/complete/streamSimple/completeSimple` with per-field auth merge. Tests: `packages/ai/test/models-runtime.test.ts`. - [x] Keep metadata helpers: `calculateCost`, `getSupportedThinkingLevels`, `clampThinkingLevel`, `modelsAreEqual`. ### Phase 2 — `src/api/` @@ -879,7 +879,7 @@ Decisions: Ordering: -- [ ] pi-ai rework first: `Provider.getModels()` sync + optional `refreshModels()`; `Models.getModels`/`getModel` sync, `Models.refresh(provider?)` async; `createProvider` takes `models` array + optional `refreshModels` fetcher (in-flight dedupe). Reverses Phase 1's async-listing decision — see "Provider model listing" for rationale (sync-or-async unions breed latent sync assumptions; async-only breaks sync consumer surfaces like extension `find`/`getAll`). +- [x] pi-ai rework first: `Provider.getModels()` sync + optional `refreshModels()`; `Models.getModels`/`getModel` sync, `Models.refresh(provider?)` async; `createProvider` takes `models` array + optional `refreshModels` fetcher (in-flight dedupe). Reverses Phase 1's async-listing decision — see "Provider model listing" for rationale (sync-or-async unions breed latent sync assumptions; async-only breaks sync consumer surfaces like extension `find`/`getAll`). - [ ] `FileCredentialStore` (ports the auth.json lock backend, reads legacy `type: "api_key"` tags) + `--api-key` overlay + `$ENV`/`!command` resolution; tests. - [ ] Cloudflare provider auth in pi-ai factories; copilot `getModels` baseUrl wrap. - [ ] Extension-OAuth adapter (old `OAuthProviderInterface` config -> `OAuthAuth`). From 02540acd17fd82ab6f5afce6ee2901493cd4c032 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 22 Jun 2026 15:08:52 +0200 Subject: [PATCH 080/122] docs(ai): update provider README --- packages/ai/README.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/ai/README.md b/packages/ai/README.md index eaa6906e..f81f6190 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -72,15 +72,17 @@ Unified LLM API with provider collections, automatic auth resolution, token and - **OpenRouter** - **Vercel AI Gateway** - **ZAI Coding Plan (Global)** (with separate China provider) -- **MiniMax** +- **MiniMax** (with separate China provider) - **Together AI** +- **Hugging Face** +- **Moonshot AI** (with separate China provider) - **GitHub Copilot** (requires OAuth, see below) - **Amazon Bedrock** - **OpenCode Zen** - **OpenCode Go** -- **Fireworks** (uses Anthropic-compatible API) -- **Kimi For Coding** (Moonshot AI, uses Anthropic-compatible API) -- **Xiaomi MiMo** (uses Anthropic-compatible API; defaults to API billing endpoint, with separate Token Plan providers for `cn`/`ams`/`sgp` regions) +- **Fireworks** (uses OpenAI- and Anthropic-compatible APIs) +- **Kimi For Coding** (Moonshot AI subscription endpoint, uses Anthropic-compatible API) +- **Xiaomi MiMo** (defaults to API billing endpoint, with separate Token Plan providers for `cn`/`ams`/`sgp` regions) - **Any OpenAI-compatible API**: Ollama, vLLM, LM Studio, etc. ## Installation @@ -382,9 +384,12 @@ Built-in providers resolve these env vars (Node.js; in browsers pass `apiKey` ex | Together AI | `TOGETHER_API_KEY` | | OpenRouter | `OPENROUTER_API_KEY` | | Vercel AI Gateway | `AI_GATEWAY_API_KEY` | -| zAI | `ZAI_API_KEY` | +| ZAI Coding Plan (Global) | `ZAI_API_KEY` | | ZAI Coding Plan (China) | `ZAI_CODING_CN_API_KEY` | -| MiniMax | `MINIMAX_API_KEY` | +| MiniMax (Global) | `MINIMAX_API_KEY` | +| MiniMax (China) | `MINIMAX_CN_API_KEY` | +| Moonshot AI / Moonshot AI (China) | `MOONSHOT_API_KEY` | +| Hugging Face | `HF_TOKEN` | | OpenCode Zen / OpenCode Go | `OPENCODE_API_KEY` | | Kimi For Coding | `KIMI_API_KEY` | | Xiaomi MiMo (API billing) | `XIAOMI_API_KEY` | From aa6bdd778a561b8641a5451093135445e4c855e1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 22 Jun 2026 17:18:43 +0200 Subject: [PATCH 081/122] fix(coding-agent): load resume startup themes --- packages/coding-agent/CHANGELOG.md | 4 + .../coding-agent/src/cli/session-picker.ts | 9 +- packages/coding-agent/src/cli/startup-ui.ts | 90 +++++++++++++++---- .../coding-agent/src/core/settings-manager.ts | 4 +- packages/coding-agent/src/main.ts | 2 +- .../interactive/theme/theme-controller.ts | 13 +-- .../src/modes/interactive/theme/theme.ts | 23 +++++ 7 files changed, 112 insertions(+), 33 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 6795f662..e2650200 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -12,6 +12,10 @@ - Added an experimental first-time setup flow behind `PI_EXPERIMENTAL=1` that asks for a dark/light theme choice (preselecting the detected appearance) and opt-in analytics data sharing on first launch with the default agent directory; opting in stores a `trackingId` in `settings.json`. +### Fixed + +- Fixed `pi --resume` to load user package themes and resolve automatic light/dark theme settings. + ## [0.79.10] - 2026-06-22 ### New Features diff --git a/packages/coding-agent/src/cli/session-picker.ts b/packages/coding-agent/src/cli/session-picker.ts index 42dcb808..793f1d0a 100644 --- a/packages/coding-agent/src/cli/session-picker.ts +++ b/packages/coding-agent/src/cli/session-picker.ts @@ -2,10 +2,12 @@ * TUI session selector for --resume flag */ -import { ProcessTerminal, setKeybindings, TUI } from "@earendil-works/pi-tui"; +import { setKeybindings } from "@earendil-works/pi-tui"; import { KeybindingsManager } from "../core/keybindings.ts"; import type { SessionInfo, SessionListProgress } from "../core/session-manager.ts"; +import type { SettingsManager } from "../core/settings-manager.ts"; import { SessionSelectorComponent } from "../modes/interactive/components/session-selector.ts"; +import { createStartupTui, startStartupTui } from "./startup-ui.ts"; type SessionsLoader = (onProgress?: SessionListProgress) => Promise; @@ -13,9 +15,10 @@ type SessionsLoader = (onProgress?: SessionListProgress) => Promise { + const ui = await createStartupTui(settingsManager); return new Promise((resolve) => { - const ui = new TUI(new ProcessTerminal()); const keybindings = KeybindingsManager.create(); setKeybindings(keybindings); let resolved = false; @@ -47,6 +50,6 @@ export async function selectSession( ui.addChild(selector); ui.setFocus(selector.getSessionList()); - ui.start(); + startStartupTui(ui, settingsManager); }); } diff --git a/packages/coding-agent/src/cli/startup-ui.ts b/packages/coding-agent/src/cli/startup-ui.ts index 93841304..73c0271c 100644 --- a/packages/coding-agent/src/cli/startup-ui.ts +++ b/packages/coding-agent/src/cli/startup-ui.ts @@ -1,16 +1,27 @@ import { ProcessTerminal, setKeybindings, TUI } from "@earendil-works/pi-tui"; import { existsSync } from "fs"; -import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, getSettingsPath, PACKAGE_NAME } from "../config.ts"; +import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, getAgentDir, getSettingsPath, PACKAGE_NAME } from "../config.ts"; import { areExperimentalFeaturesEnabled } from "../core/experimental.ts"; import { KeybindingsManager } from "../core/keybindings.ts"; -import type { SettingsManager } from "../core/settings-manager.ts"; +import { DefaultPackageManager, type ResolvedResource } from "../core/package-manager.ts"; +import { SettingsManager } from "../core/settings-manager.ts"; import { ExtensionInputComponent } from "../modes/interactive/components/extension-input.ts"; import { ExtensionSelectorComponent } from "../modes/interactive/components/extension-selector.ts"; import { FirstTimeSetupComponent, type FirstTimeSetupResult, } from "../modes/interactive/components/first-time-setup.ts"; -import { detectTerminalBackgroundTheme, initTheme, setTheme } from "../modes/interactive/theme/theme.ts"; +import { + detectTerminalBackgroundFromEnv, + detectTerminalThemeForAuto, + initTheme, + loadThemeFromPath, + parseAutoThemeSetting, + resolveThemeSetting, + setRegisteredThemes, + setTheme, + type Theme, +} from "../modes/interactive/theme/theme.ts"; const OFFICIAL_PACKAGE_NAME = "@earendil-works/pi-coding-agent"; const OFFICIAL_APP_NAME = "pi"; @@ -30,14 +41,64 @@ function isOfficialDistribution({ packageName, appName, configDirName }: Distrib ); } -function createStartupTui(settingsManager: SettingsManager): TUI { - initTheme(settingsManager.getTheme()); +function loadThemes(resources: ResolvedResource[]): Theme[] { + const themes: Theme[] = []; + const seen = new Set(); + for (const resource of resources) { + if (!resource.enabled) continue; + try { + const loadedTheme = loadThemeFromPath(resource.path); + if (loadedTheme.name) { + if (seen.has(loadedTheme.name)) continue; + seen.add(loadedTheme.name); + } + themes.push(loadedTheme); + } catch { + // Startup prompts should not fail because a theme is broken. The normal + // resource loader reports theme diagnostics later in startup. + } + } + return themes; +} + +async function loadStartupThemes(settingsManager: SettingsManager): Promise { + const globalSettingsManager = SettingsManager.inMemory(settingsManager.getGlobalSettings(), { + projectTrusted: false, + }); + const packageManager = new DefaultPackageManager({ + cwd: process.cwd(), + agentDir: getAgentDir(), + settingsManager: globalSettingsManager, + }); + const resolvedPaths = await packageManager.resolve(async () => "skip"); + return loadThemes(resolvedPaths.themes); +} + +export async function createStartupTui(settingsManager: SettingsManager): Promise { + setRegisteredThemes(await loadStartupThemes(settingsManager)); + const terminalTheme = detectTerminalBackgroundFromEnv().theme; + initTheme(resolveThemeSetting(settingsManager.getThemeSetting(), terminalTheme) ?? terminalTheme); setKeybindings(KeybindingsManager.create()); const ui = new TUI(new ProcessTerminal(), settingsManager.getShowHardwareCursor()); ui.setClearOnShrink(settingsManager.getClearOnShrink()); return ui; } +export function startStartupTui(ui: TUI, settingsManager: SettingsManager): void { + ui.start(); + void applyDetectedStartupTheme(ui, settingsManager); +} + +async function applyDetectedStartupTheme(ui: TUI, settingsManager: SettingsManager): Promise { + const themeSetting = settingsManager.getThemeSetting(); + if (themeSetting && !parseAutoThemeSetting(themeSetting)) return; + + const terminalTheme = await detectTerminalThemeForAuto({ ui, timeoutMs: 100 }); + setTheme(resolveThemeSetting(themeSetting, terminalTheme) ?? terminalTheme); + ui.invalidate(); + ui.requestRender(); +} + async function clearStartupTui(ui: TUI): Promise { ui.clear(); ui.requestRender(); @@ -75,9 +136,8 @@ export async function showStartupSelector( title: string, options: Array<{ label: string; value: T }>, ): Promise { + const ui = await createStartupTui(settingsManager); return new Promise((resolve) => { - const ui = createStartupTui(settingsManager); - let settled = false; const finish = async (result: T | undefined) => { if (settled) { @@ -98,15 +158,14 @@ export async function showStartupSelector( ); ui.addChild(selector); ui.setFocus(selector); - ui.start(); + startStartupTui(ui, settingsManager); }); } /** Show the first-time setup dialog and persist the result */ export async function showFirstTimeSetup(settingsManager: SettingsManager): Promise { + const ui = await createStartupTui(settingsManager); return new Promise((resolve) => { - const ui = createStartupTui(settingsManager); - let settled = false; const finish = async (result: FirstTimeSetupResult | undefined) => { if (settled) { @@ -125,10 +184,10 @@ export async function showFirstTimeSetup(settingsManager: SettingsManager): Prom const showSetup = async () => { ui.start(); - const detection = await detectTerminalBackgroundTheme({ ui, timeoutMs: 100 }); - setTheme(detection.theme); + const detectedTheme = await detectTerminalThemeForAuto({ ui, timeoutMs: 100 }); + setTheme(detectedTheme); const component = new FirstTimeSetupComponent({ - detectedTheme: detection.theme, + detectedTheme, onThemePreview: (themeName) => { setTheme(themeName); ui.requestRender(); @@ -150,9 +209,8 @@ export async function showStartupInput( title: string, placeholder?: string, ): Promise { + const ui = await createStartupTui(settingsManager); return new Promise((resolve) => { - const ui = createStartupTui(settingsManager); - let settled = false; const finish = async (result: string | undefined) => { if (settled) { @@ -176,6 +234,6 @@ export async function showStartupInput( ); ui.addChild(input); ui.setFocus(input); - ui.start(); + startStartupTui(ui, settingsManager); }); } diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index a90916a4..99ae71a4 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -334,11 +334,11 @@ export class SettingsManager { } /** Create an in-memory SettingsManager (no file I/O) */ - static inMemory(settings: Partial = {}): SettingsManager { + static inMemory(settings: Partial = {}, options: SettingsManagerCreateOptions = {}): SettingsManager { const storage = new InMemorySettingsStorage(); const initialSettings = SettingsManager.migrateSettings(structuredClone(settings) as Record); storage.withLock("global", () => JSON.stringify(initialSettings, null, 2)); - return SettingsManager.fromStorage(storage); + return SettingsManager.fromStorage(storage, options); } private static loadFromStorage(storage: SettingsStorage, scope: SettingsScope, projectTrusted = true): Settings { diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index f66040bb..568d96d1 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -308,11 +308,11 @@ async function createSessionManager( } if (parsed.resume) { - initTheme(settingsManager.getTheme(), true); try { const selectedPath = await selectSession( (onProgress) => SessionManager.list(cwd, sessionDir, onProgress), (onProgress) => SessionManager.listAll(sessionDir, onProgress), + settingsManager, ); if (!selectedPath) { console.log(chalk.dim("No session selected")); diff --git a/packages/coding-agent/src/modes/interactive/theme/theme-controller.ts b/packages/coding-agent/src/modes/interactive/theme/theme-controller.ts index 9fe9e8cc..43ad620d 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme-controller.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme-controller.ts @@ -3,6 +3,7 @@ import type { SettingsManager } from "../../../core/settings-manager.ts"; import { detectTerminalBackgroundFromEnv, detectTerminalBackgroundTheme, + detectTerminalThemeForAuto, initTheme, parseAutoThemeSetting, resolveThemeSetting, @@ -37,7 +38,7 @@ export class InteractiveThemeController { const themeSetting = this.settingsManager.getThemeSetting(); const autoTheme = parseAutoThemeSetting(themeSetting); if (autoTheme) { - this.terminalTheme = await this.detectTerminalThemeForAuto(); + this.terminalTheme = await detectTerminalThemeForAuto({ ui: this.ui, timeoutMs: 100 }); this.setAutoSync(true); this.applyThemeName(this.terminalTheme === "light" ? autoTheme.lightTheme : autoTheme.darkTheme, true); return; @@ -109,16 +110,6 @@ export class InteractiveThemeController { this.ui.setTerminalColorSchemeNotifications(enabled); } - private async detectTerminalThemeForAuto(): Promise { - try { - const colorScheme = await this.ui.queryTerminalColorScheme({ timeoutMs: 100 }); - if (colorScheme) return colorScheme; - } catch { - // Fall back to OSC 11 / COLORFGBG detection when color-scheme DSR is unsupported. - } - return (await detectTerminalBackgroundTheme({ ui: this.ui, timeoutMs: 100 })).theme; - } - private applyTerminalTheme(terminalTheme: TerminalTheme): void { if (!this.autoSyncEnabled) return; this.terminalTheme = terminalTheme; diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index 58e5aac3..676bc529 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -680,11 +680,20 @@ export interface TerminalBackgroundThemeDetector { queryTerminalBackgroundColor({ timeoutMs }: { timeoutMs: number }): Promise; } +export interface TerminalAutoThemeDetector extends TerminalBackgroundThemeDetector { + queryTerminalColorScheme?({ timeoutMs }: { timeoutMs: number }): Promise; +} + export interface TerminalBackgroundThemeDetectionOptions extends TerminalThemeDetectionOptions { ui: TerminalBackgroundThemeDetector; timeoutMs: number; } +export interface TerminalAutoThemeDetectionOptions extends TerminalThemeDetectionOptions { + ui: TerminalAutoThemeDetector; + timeoutMs: number; +} + function getColorFgBgBackgroundIndex(colorfgbg: string): number | undefined { const parts = colorfgbg.split(";"); for (let i = parts.length - 1; i >= 0; i--) { @@ -755,6 +764,20 @@ export async function detectTerminalBackgroundTheme({ return detectTerminalBackgroundFromEnv({ env }); } +export async function detectTerminalThemeForAuto({ + ui, + timeoutMs, + env, +}: TerminalAutoThemeDetectionOptions): Promise { + try { + const colorScheme = await ui.queryTerminalColorScheme?.({ timeoutMs }); + if (colorScheme) return colorScheme; + } catch { + // Fall back to OSC 11 / COLORFGBG detection when color-scheme DSR is unsupported. + } + return (await detectTerminalBackgroundTheme({ ui, timeoutMs, env })).theme; +} + export function getDefaultTheme(): string { return detectTerminalBackgroundFromEnv().theme; } From f7d3331df6166f6653e6c8046bf20c2ae0ae2ff1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 22 Jun 2026 17:49:18 +0200 Subject: [PATCH 082/122] fix(ai): mock copilot models in oauth test --- packages/ai/test/oauth-auth.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/ai/test/oauth-auth.test.ts b/packages/ai/test/oauth-auth.test.ts index c008b18c..43be6f72 100644 --- a/packages/ai/test/oauth-auth.test.ts +++ b/packages/ai/test/oauth-auth.test.ts @@ -69,7 +69,11 @@ describe.sequential("OAuthAuth adapters", () => { it("github-copilot refresh preserves the enterprise domain", async () => { const fetchedUrls: string[] = []; const fetchMock = vi.fn(async (input: unknown) => { - fetchedUrls.push(typeof input === "string" ? input : String(input)); + const url = typeof input === "string" ? input : String(input); + fetchedUrls.push(url); + if (url.endsWith("/models")) { + return jsonResponse({ data: [] }); + } return jsonResponse({ token: "new-token", expires_at: 9999999999 }); }); vi.stubGlobal("fetch", fetchMock); From d0e0b84cb9d811dc30bb9dc13969c013bebff0c1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 23 Jun 2026 00:17:37 +0200 Subject: [PATCH 083/122] fix(ai): reconnect Codex websocket on connection limit closes #5973 --- packages/ai/CHANGELOG.md | 1 + packages/ai/src/api/openai-codex-responses.ts | 121 +++++++++++------- packages/ai/test/openai-codex-stream.test.ts | 62 +++++++++ 3 files changed, 137 insertions(+), 47 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 8a43f32f..694e25a7 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -20,6 +20,7 @@ ### Fixed +- Fixed OpenAI Codex Responses WebSocket sessions to reconnect once when OpenAI's connection limit is reached before output starts ([#5973](https://github.com/earendil-works/pi/issues/5973)). - Fixed OpenCode Go GLM-5.2 metadata to expose `xhigh` reasoning and send `reasoning_effort: "max"` ([#5967](https://github.com/earendil-works/pi/issues/5967)). - Fixed Claude Fable 5 thinking-off requests to omit Anthropic's unsupported `thinking.type: "disabled"` payload ([#5567](https://github.com/earendil-works/pi/pull/5567) by [@tmustier](https://github.com/tmustier)). diff --git a/packages/ai/src/api/openai-codex-responses.ts b/packages/ai/src/api/openai-codex-responses.ts index ee2503d5..6107bb22 100644 --- a/packages/ai/src/api/openai-codex-responses.ts +++ b/packages/ai/src/api/openai-codex-responses.ts @@ -61,6 +61,7 @@ const DEFAULT_SSE_HEADER_TIMEOUT_MS = 20_000; const DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS = 15_000; const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]); const WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE = 1009; +const WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE = "websocket_connection_limit_reached"; const CODEX_RESPONSE_STATUSES = new Set([ "completed", @@ -253,52 +254,62 @@ export const stream: StreamFunction<"openai-codex-responses", OpenAICodexRespons if (transport !== "sse" && !websocketDisabledForSession) { let websocketStarted = false; - try { - await processWebSocketStream( - resolveCodexWebSocketUrl(model.baseUrl), - body, - websocketHeaders, - output, - stream, - model, - () => { - websocketStarted = true; - }, - idleTimeoutMs, - websocketConnectTimeoutMs, - options, - ); + let retriedWebSocketConnectionLimit = false; + while (true) { + websocketStarted = false; + try { + await processWebSocketStream( + resolveCodexWebSocketUrl(model.baseUrl), + body, + websocketHeaders, + output, + stream, + model, + () => { + websocketStarted = true; + }, + idleTimeoutMs, + websocketConnectTimeoutMs, + options, + ); - if (options?.signal?.aborted) { - throw new Error("Request was aborted"); + if (options?.signal?.aborted) { + throw new Error("Request was aborted"); + } + stream.push({ + type: "done", + reason: output.stopReason as "stop" | "length" | "toolUse", + message: output, + }); + stream.end(); + return; + } catch (error) { + const aborted = options?.signal?.aborted; + const connectionLimitBeforeStart = !websocketStarted && isWebSocketConnectionLimitReachedError(error); + if (!aborted && connectionLimitBeforeStart && !retriedWebSocketConnectionLimit) { + retriedWebSocketConnectionLimit = true; + continue; + } + if (aborted || (isCodexNonTransportError(error) && !connectionLimitBeforeStart)) { + throw error; + } + appendAssistantMessageDiagnostic( + output, + createAssistantMessageDiagnostic("provider_transport_failure", error, { + configuredTransport: transport, + fallbackTransport: websocketStarted ? undefined : "sse", + eventsEmitted: websocketStarted, + phase: websocketStarted ? "after_message_stream_start" : "before_message_stream_start", + requestBytes: new TextEncoder().encode(bodyJson).byteLength, + }), + ); + recordWebSocketFailure(options?.sessionId, error); + if (websocketStarted) { + throw error; + } + recordWebSocketSseFallback(options?.sessionId); + break; } - stream.push({ - type: "done", - reason: output.stopReason as "stop" | "length" | "toolUse", - message: output, - }); - stream.end(); - return; - } catch (error) { - const aborted = options?.signal?.aborted; - if (aborted || isCodexNonTransportError(error)) { - throw error; - } - appendAssistantMessageDiagnostic( - output, - createAssistantMessageDiagnostic("provider_transport_failure", error, { - configuredTransport: transport, - fallbackTransport: websocketStarted ? undefined : "sse", - eventsEmitted: websocketStarted, - phase: websocketStarted ? "after_message_stream_start" : "before_message_stream_start", - requestBytes: new TextEncoder().encode(bodyJson).byteLength, - }), - ); - recordWebSocketFailure(options?.sessionId, error); - if (websocketStarted) { - throw error; - } - recordWebSocketSseFallback(options?.sessionId); } } @@ -582,16 +593,32 @@ function isCodexNonTransportError(error: unknown): boolean { return error instanceof CodexApiError || error instanceof CodexProtocolError; } +function isWebSocketConnectionLimitReachedError(error: unknown): boolean { + return error instanceof CodexApiError && error.code === WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE; +} + +function extractCodexEventError(event: Record): { code?: string; message?: string } { + const nested = event.error && typeof event.error === "object" ? (event.error as Record) : undefined; + return { + code: typeof event.code === "string" ? event.code : typeof nested?.code === "string" ? nested.code : undefined, + message: + typeof event.message === "string" + ? event.message + : typeof nested?.message === "string" + ? nested.message + : undefined, + }; +} + async function* mapCodexEvents(events: AsyncIterable>): AsyncGenerator { for await (const event of events) { const type = typeof event.type === "string" ? event.type : undefined; if (!type) continue; if (type === "error") { - const code = (event as { code?: string }).code || ""; - const message = (event as { message?: string }).message || ""; + const { code, message } = extractCodexEventError(event); throw new CodexApiError(`Codex error: ${message || code || JSON.stringify(event)}`, { - code: code || undefined, + code, payload: event, }); } diff --git a/packages/ai/test/openai-codex-stream.test.ts b/packages/ai/test/openai-codex-stream.test.ts index 9c526151..b68d5b73 100644 --- a/packages/ai/test/openai-codex-stream.test.ts +++ b/packages/ai/test/openai-codex-stream.test.ts @@ -1195,6 +1195,68 @@ describe("openai-codex streaming", () => { }); }); + it("reconnects once when the websocket connection limit is reached before output starts", async () => { + const token = mockToken(); + let connections = 0; + + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + + class MockWebSocket extends EventTarget { + private readonly limitReached = connections++ === 0; + + constructor() { + super(); + queueMicrotask(() => this.dispatchEvent(new Event("open"))); + } + + send(): void { + const event = this.limitReached + ? { type: "error", error: { code: "websocket_connection_limit_reached" } } + : { + type: "response.completed", + response: { + id: "resp_1", + status: "completed", + usage: { input_tokens: 5, output_tokens: 3, total_tokens: 8 }, + }, + }; + queueMicrotask(() => { + this.dispatchEvent(Object.assign(new Event("message"), { data: JSON.stringify(event) })); + }); + } + + close(): void {} + } + + vi.stubGlobal("WebSocket", MockWebSocket); + + const model: Model<"openai-codex-responses"> = { + id: "gpt-5.1-codex", + name: "GPT-5.1 Codex", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 400000, + maxTokens: 128000, + }; + + const result = await streamOpenAICodexResponses( + model, + { systemPrompt: "", messages: [] }, + { + apiKey: token, + }, + ).result(); + + expect(result.stopReason).toBe("stop"); + expect(connections).toBe(2); + expect(fetchMock).not.toHaveBeenCalled(); + }); + it("falls back to SSE when a websocket is idle before the first event", async () => { vi.useFakeTimers(); const token = mockToken(); From 392bae6b67d92c82608e69906283bcebf5f385ae Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Tue, 23 Jun 2026 10:36:47 +0200 Subject: [PATCH 084/122] fix(tui): avoid tall dialog redraw loops closes #5990 --- packages/tui/CHANGELOG.md | 4 ++ packages/tui/src/tui.ts | 58 ++++++++++++++++++++++++- packages/tui/test/tui-render.test.ts | 64 ++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index a3cf70ef..9230cd04 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -6,6 +6,10 @@ - Added `Ctrl+J` as a default newline keybinding alongside `Shift+Enter`. +### Fixed + +- Fixed full redraw loops when offscreen lines change above a tall visible viewport, avoiding flicker for oversized dialogs ([#5990](https://github.com/earendil-works/pi/issues/5990)). + ## [0.79.10] - 2026-06-22 ## [0.79.9] - 2026-06-20 diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index a7054888..f6e81e01 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -313,6 +313,7 @@ export class TUI extends Container { private clearOnShrink = process.env.PI_CLEAR_ON_SHRINK === "1"; // Clear empty rows when content shrinks (default: off) private maxLinesRendered = 0; // Track terminal's working area (max lines ever rendered) private previousViewportTop = 0; // Track previous viewport top for resize-aware cursor moves + private skippedOffscreenChangeRange: { first: number; last: number } | undefined; private fullRedrawCount = 0; private stopped = false; private pendingOsc11BackgroundReplies = 0; @@ -718,6 +719,7 @@ export class TUI extends Container { this.hardwareCursorRow = 0; this.maxLinesRendered = 0; this.previousViewportTop = 0; + this.skippedOffscreenChangeRange = undefined; if (this.renderTimer) { clearTimeout(this.renderTimer); this.renderTimer = undefined; @@ -1172,6 +1174,30 @@ export class TUI extends Container { return this.deleteKittyImages(ids); } + private changedRangeContainsKittyImages(firstChanged: number, lastChanged: number, newLines: string[]): boolean { + for (let i = firstChanged; i <= lastChanged; i++) { + if (extractKittyImageIds(this.previousLines[i] ?? "").length > 0) return true; + if (extractKittyImageIds(newLines[i] ?? "").length > 0) return true; + } + return false; + } + + private markSkippedOffscreenChange(first: number, last: number): void { + if (!this.skippedOffscreenChangeRange) { + this.skippedOffscreenChangeRange = { first, last }; + return; + } + this.skippedOffscreenChangeRange = { + first: Math.min(this.skippedOffscreenChangeRange.first, first), + last: Math.max(this.skippedOffscreenChangeRange.last, last), + }; + } + + private skippedOffscreenChangesIntersect(first: number, last: number): boolean { + const range = this.skippedOffscreenChangeRange; + return range !== undefined && range.first <= last && range.last >= first; + } + /** Splice overlay content into a base line at a specific column. Single-pass optimized. */ private compositeLineAt( baseLine: string, @@ -1322,6 +1348,7 @@ export class TUI extends Container { this.previousKittyImageIds = this.collectKittyImageIds(newLines); this.previousWidth = width; this.previousHeight = height; + this.skippedOffscreenChangeRange = undefined; }; const debugRedraw = process.env.PI_DEBUG_REDRAW === "1"; @@ -1349,7 +1376,18 @@ export class TUI extends Container { // Height changes normally need a full re-render to keep the visible viewport aligned, // but Termux changes height when the software keyboard shows or hides. // In that environment, a full redraw causes the entire history to replay on every toggle. - if (heightChanged && !isTermuxSession()) { + // If the new Termux viewport would reveal rows skipped by the offscreen optimization, + // redraw once so stale scrollback does not become visible. + if (heightChanged && isTermuxSession()) { + const viewportBottom = prevViewportTop + height - 1; + if (this.skippedOffscreenChangesIntersect(prevViewportTop, viewportBottom)) { + logRedraw( + `terminal height changed revealing skipped offscreen changes (${this.previousHeight} -> ${height})`, + ); + fullRender(true); + return; + } + } else if (heightChanged) { logRedraw(`terminal height changed (${this.previousHeight} -> ${height})`); fullRender(true); return; @@ -1401,6 +1439,24 @@ export class TUI extends Container { return; } + // If all changed lines are above the visible viewport and the buffer length did not change, + // the on-screen content is already correct. Avoid clearing/replaying the viewport for + // offscreen animations such as tall dialogs with an updating header/status line. + if ( + lastChanged < prevViewportTop && + newLines.length === this.previousLines.length && + !this.changedRangeContainsKittyImages(firstChanged, lastChanged, newLines) + ) { + this.markSkippedOffscreenChange(firstChanged, lastChanged); + this.positionHardwareCursor(cursorPos, newLines.length); + this.previousLines = newLines; + this.previousKittyImageIds = this.collectKittyImageIds(newLines); + this.previousWidth = width; + this.previousHeight = height; + this.previousViewportTop = prevViewportTop; + return; + } + // All changes are in deleted lines (nothing to render, just clear) if (firstChanged >= newLines.length) { if (this.previousLines.length > newLines.length) { diff --git a/packages/tui/test/tui-render.test.ts b/packages/tui/test/tui-render.test.ts index ab038ac7..f20dffd6 100644 --- a/packages/tui/test/tui-render.test.ts +++ b/packages/tui/test/tui-render.test.ts @@ -378,6 +378,44 @@ describe("TUI resize handling", () => { }); }); + it("full re-renders in Termux when height changes reveal skipped offscreen changes", async () => { + await withEnv({ TERMUX_VERSION: "1" }, async () => { + const terminal = new VirtualTerminal(20, 5); + const tui = new TUI(terminal); + const component = new TestComponent(); + tui.addChild(component); + + component.lines = Array.from({ length: 11 }, (_, i) => `Line ${i}`); + tui.start(); + await terminal.waitForRender(); + + component.lines = Array.from({ length: 11 }, (_, i) => (i === 5 ? "Changed 5" : `Line ${i}`)); + tui.requestRender(); + await terminal.waitForRender(); + + const redrawsAfterSkippedChange = tui.fullRedraws; + assert.deepStrictEqual(terminal.getViewport(), ["Line 6", "Line 7", "Line 8", "Line 9", "Line 10"]); + + terminal.resize(20, 6); + await terminal.waitForRender(); + + assert.ok( + tui.fullRedraws > redrawsAfterSkippedChange, + "Height change should redraw before skipped offscreen content becomes visible", + ); + assert.deepStrictEqual(terminal.getViewport(), [ + "Changed 5", + "Line 6", + "Line 7", + "Line 8", + "Line 9", + "Line 10", + ]); + + tui.stop(); + }); + }); + it("triggers full re-render when terminal width changes", async () => { const terminal = new VirtualTerminal(40, 10); const tui = new TUI(terminal); @@ -657,6 +695,32 @@ describe("TUI differential rendering", () => { tui.stop(); }); + it("does not full redraw for offscreen changes above the viewport", async () => { + const terminal = new VirtualTerminal(20, 5); + const tui = new TUI(terminal); + const component = new TestComponent(); + tui.addChild(component); + + const stableVisibleLines = Array.from({ length: 8 }, (_, i) => `Dialog ${i}`); + component.lines = ["Ticker 0", "Intro", "", ...stableVisibleLines]; + tui.start(); + await terminal.waitForRender(); + + const initialRedraws = tui.fullRedraws; + const initialViewport = terminal.getViewport(); + + for (let i = 1; i <= 3; i++) { + component.lines = [`Ticker ${i}`, "Intro", "", ...stableVisibleLines]; + tui.requestRender(); + await terminal.waitForRender(); + } + + assert.strictEqual(tui.fullRedraws, initialRedraws, "Offscreen changes should not force full redraws"); + assert.deepStrictEqual(terminal.getViewport(), initialViewport, "Visible viewport should remain stable"); + + tui.stop(); + }); + it("full re-renders when deleted lines move the viewport upward", async () => { const terminal = new VirtualTerminal(20, 5); const tui = new TUI(terminal); From 590482cf6d35e66b1ce3988fbc682b7a2037a232 Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Tue, 23 Jun 2026 11:48:03 +0200 Subject: [PATCH 085/122] Revert "fix(tui): avoid tall dialog redraw loops" This reverts commit 392bae6b67d92c82608e69906283bcebf5f385ae. --- packages/tui/CHANGELOG.md | 4 -- packages/tui/src/tui.ts | 58 +------------------------ packages/tui/test/tui-render.test.ts | 64 ---------------------------- 3 files changed, 1 insertion(+), 125 deletions(-) diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 9230cd04..a3cf70ef 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -6,10 +6,6 @@ - Added `Ctrl+J` as a default newline keybinding alongside `Shift+Enter`. -### Fixed - -- Fixed full redraw loops when offscreen lines change above a tall visible viewport, avoiding flicker for oversized dialogs ([#5990](https://github.com/earendil-works/pi/issues/5990)). - ## [0.79.10] - 2026-06-22 ## [0.79.9] - 2026-06-20 diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index f6e81e01..a7054888 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -313,7 +313,6 @@ export class TUI extends Container { private clearOnShrink = process.env.PI_CLEAR_ON_SHRINK === "1"; // Clear empty rows when content shrinks (default: off) private maxLinesRendered = 0; // Track terminal's working area (max lines ever rendered) private previousViewportTop = 0; // Track previous viewport top for resize-aware cursor moves - private skippedOffscreenChangeRange: { first: number; last: number } | undefined; private fullRedrawCount = 0; private stopped = false; private pendingOsc11BackgroundReplies = 0; @@ -719,7 +718,6 @@ export class TUI extends Container { this.hardwareCursorRow = 0; this.maxLinesRendered = 0; this.previousViewportTop = 0; - this.skippedOffscreenChangeRange = undefined; if (this.renderTimer) { clearTimeout(this.renderTimer); this.renderTimer = undefined; @@ -1174,30 +1172,6 @@ export class TUI extends Container { return this.deleteKittyImages(ids); } - private changedRangeContainsKittyImages(firstChanged: number, lastChanged: number, newLines: string[]): boolean { - for (let i = firstChanged; i <= lastChanged; i++) { - if (extractKittyImageIds(this.previousLines[i] ?? "").length > 0) return true; - if (extractKittyImageIds(newLines[i] ?? "").length > 0) return true; - } - return false; - } - - private markSkippedOffscreenChange(first: number, last: number): void { - if (!this.skippedOffscreenChangeRange) { - this.skippedOffscreenChangeRange = { first, last }; - return; - } - this.skippedOffscreenChangeRange = { - first: Math.min(this.skippedOffscreenChangeRange.first, first), - last: Math.max(this.skippedOffscreenChangeRange.last, last), - }; - } - - private skippedOffscreenChangesIntersect(first: number, last: number): boolean { - const range = this.skippedOffscreenChangeRange; - return range !== undefined && range.first <= last && range.last >= first; - } - /** Splice overlay content into a base line at a specific column. Single-pass optimized. */ private compositeLineAt( baseLine: string, @@ -1348,7 +1322,6 @@ export class TUI extends Container { this.previousKittyImageIds = this.collectKittyImageIds(newLines); this.previousWidth = width; this.previousHeight = height; - this.skippedOffscreenChangeRange = undefined; }; const debugRedraw = process.env.PI_DEBUG_REDRAW === "1"; @@ -1376,18 +1349,7 @@ export class TUI extends Container { // Height changes normally need a full re-render to keep the visible viewport aligned, // but Termux changes height when the software keyboard shows or hides. // In that environment, a full redraw causes the entire history to replay on every toggle. - // If the new Termux viewport would reveal rows skipped by the offscreen optimization, - // redraw once so stale scrollback does not become visible. - if (heightChanged && isTermuxSession()) { - const viewportBottom = prevViewportTop + height - 1; - if (this.skippedOffscreenChangesIntersect(prevViewportTop, viewportBottom)) { - logRedraw( - `terminal height changed revealing skipped offscreen changes (${this.previousHeight} -> ${height})`, - ); - fullRender(true); - return; - } - } else if (heightChanged) { + if (heightChanged && !isTermuxSession()) { logRedraw(`terminal height changed (${this.previousHeight} -> ${height})`); fullRender(true); return; @@ -1439,24 +1401,6 @@ export class TUI extends Container { return; } - // If all changed lines are above the visible viewport and the buffer length did not change, - // the on-screen content is already correct. Avoid clearing/replaying the viewport for - // offscreen animations such as tall dialogs with an updating header/status line. - if ( - lastChanged < prevViewportTop && - newLines.length === this.previousLines.length && - !this.changedRangeContainsKittyImages(firstChanged, lastChanged, newLines) - ) { - this.markSkippedOffscreenChange(firstChanged, lastChanged); - this.positionHardwareCursor(cursorPos, newLines.length); - this.previousLines = newLines; - this.previousKittyImageIds = this.collectKittyImageIds(newLines); - this.previousWidth = width; - this.previousHeight = height; - this.previousViewportTop = prevViewportTop; - return; - } - // All changes are in deleted lines (nothing to render, just clear) if (firstChanged >= newLines.length) { if (this.previousLines.length > newLines.length) { diff --git a/packages/tui/test/tui-render.test.ts b/packages/tui/test/tui-render.test.ts index f20dffd6..ab038ac7 100644 --- a/packages/tui/test/tui-render.test.ts +++ b/packages/tui/test/tui-render.test.ts @@ -378,44 +378,6 @@ describe("TUI resize handling", () => { }); }); - it("full re-renders in Termux when height changes reveal skipped offscreen changes", async () => { - await withEnv({ TERMUX_VERSION: "1" }, async () => { - const terminal = new VirtualTerminal(20, 5); - const tui = new TUI(terminal); - const component = new TestComponent(); - tui.addChild(component); - - component.lines = Array.from({ length: 11 }, (_, i) => `Line ${i}`); - tui.start(); - await terminal.waitForRender(); - - component.lines = Array.from({ length: 11 }, (_, i) => (i === 5 ? "Changed 5" : `Line ${i}`)); - tui.requestRender(); - await terminal.waitForRender(); - - const redrawsAfterSkippedChange = tui.fullRedraws; - assert.deepStrictEqual(terminal.getViewport(), ["Line 6", "Line 7", "Line 8", "Line 9", "Line 10"]); - - terminal.resize(20, 6); - await terminal.waitForRender(); - - assert.ok( - tui.fullRedraws > redrawsAfterSkippedChange, - "Height change should redraw before skipped offscreen content becomes visible", - ); - assert.deepStrictEqual(terminal.getViewport(), [ - "Changed 5", - "Line 6", - "Line 7", - "Line 8", - "Line 9", - "Line 10", - ]); - - tui.stop(); - }); - }); - it("triggers full re-render when terminal width changes", async () => { const terminal = new VirtualTerminal(40, 10); const tui = new TUI(terminal); @@ -695,32 +657,6 @@ describe("TUI differential rendering", () => { tui.stop(); }); - it("does not full redraw for offscreen changes above the viewport", async () => { - const terminal = new VirtualTerminal(20, 5); - const tui = new TUI(terminal); - const component = new TestComponent(); - tui.addChild(component); - - const stableVisibleLines = Array.from({ length: 8 }, (_, i) => `Dialog ${i}`); - component.lines = ["Ticker 0", "Intro", "", ...stableVisibleLines]; - tui.start(); - await terminal.waitForRender(); - - const initialRedraws = tui.fullRedraws; - const initialViewport = terminal.getViewport(); - - for (let i = 1; i <= 3; i++) { - component.lines = [`Ticker ${i}`, "Intro", "", ...stableVisibleLines]; - tui.requestRender(); - await terminal.waitForRender(); - } - - assert.strictEqual(tui.fullRedraws, initialRedraws, "Offscreen changes should not force full redraws"); - assert.deepStrictEqual(terminal.getViewport(), initialViewport, "Visible viewport should remain stable"); - - tui.stop(); - }); - it("full re-renders when deleted lines move the viewport upward", async () => { const terminal = new VirtualTerminal(20, 5); const tui = new TUI(terminal); From ce6a67fc948c63f991f7f649032ee237d530146b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 23 Jun 2026 11:51:33 +0200 Subject: [PATCH 086/122] fix(coding-agent): allow custom providers to use stored auth closes #5953 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/docs/models.md | 6 ++++-- packages/coding-agent/src/core/model-registry.ts | 6 ++---- packages/coding-agent/test/model-registry.test.ts | 3 ++- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e2650200..b71110ca 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -15,6 +15,7 @@ ### Fixed - Fixed `pi --resume` to load user package themes and resolve automatic light/dark theme settings. +- Fixed `models.json` custom providers so stored credentials can satisfy auth without a redundant provider-level `apiKey` ([#5953](https://github.com/earendil-works/pi/issues/5953)). ## [0.79.10] - 2026-06-22 diff --git a/packages/coding-agent/docs/models.md b/packages/coding-agent/docs/models.md index 5679981a..d17e9928 100644 --- a/packages/coding-agent/docs/models.md +++ b/packages/coding-agent/docs/models.md @@ -34,7 +34,7 @@ For local models (Ollama, LM Studio, vLLM), only `id` is required per model: } ``` -The `apiKey` is required but Ollama ignores it, so any value works. +The `apiKey` value is a placeholder because Ollama ignores it. pi still treats models as requiring auth before they appear in `/model`, so keyless local servers should keep a dummy value, save a key for that provider with `/login`, or pass `--api-key` when selecting the model. Some OpenAI-compatible servers do not understand the `developer` role used for reasoning-capable models. For those providers, set `compat.supportsDeveloperRole` to `false` so pi sends the system prompt as a `system` message instead. If the server also does not support `reasoning_effort`, set `compat.supportsReasoningEffort` to `false` too. @@ -135,12 +135,14 @@ Set `api` at provider level (default for all models) or model level (override pe |-------|-------------| | `baseUrl` | API endpoint URL | | `api` | API type (see above) | -| `apiKey` | API key (see value resolution below) | +| `apiKey` | Optional API key config (see value resolution below). Omit it when auth is provided by `/login`/`auth.json` or CLI `--api-key`. | | `headers` | Custom headers (see value resolution below) | | `authHeader` | Set `true` to add `Authorization: Bearer ` automatically | | `models` | Array of model configurations | | `modelOverrides` | Per-model overrides for built-in models on this provider | +For providers with `models`, non-built-in provider configs need `baseUrl` and an `api` value at either provider or model level. `apiKey` is not required to load the file: models become available when auth is configured through `/login`/`auth.json`, CLI `--api-key`, or provider `apiKey`. If no auth is configured, the models load but stay unavailable in `/model` and `--list-models`. + ### Value Resolution The `apiKey` and `headers` fields support command execution, environment interpolation, and literals: diff --git a/packages/coding-agent/src/core/model-registry.ts b/packages/coding-agent/src/core/model-registry.ts index 8f86bf6c..70f39394 100644 --- a/packages/coding-agent/src/core/model-registry.ts +++ b/packages/coding-agent/src/core/model-registry.ts @@ -546,13 +546,11 @@ export class ModelRegistry { ); } } else if (!isBuiltIn) { - // Non-built-in providers with custom models require endpoint + auth. + // Non-built-in providers with custom models require an endpoint. + // Auth can come from auth.json, --api-key, or provider request config. if (!providerConfig.baseUrl) { throw new Error(`Provider ${providerName}: "baseUrl" is required when defining custom models.`); } - if (!providerConfig.apiKey) { - throw new Error(`Provider ${providerName}: "apiKey" is required when defining custom models.`); - } } // Built-in providers with custom models: baseUrl/apiKey/api are optional, // inherited from built-in models. Auth comes from env vars / auth storage. diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index 2405e557..fb559e1d 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -246,9 +246,10 @@ describe("ModelRegistry", () => { expect(model?.baseUrl).toBe("https://openrouter.ai/api/v1"); }); - test("non-built-in provider custom models still require baseUrl and apiKey", () => { + test("non-built-in provider custom models still require baseUrl", () => { writeRawModelsJson({ "my-custom-provider": { + apiKey: "test-key", models: [ { id: "my-model", From 7c1ef877560e482774ce1cba53f24bb66fcaeb59 Mon Sep 17 00:00:00 2001 From: haoqixu Date: Tue, 23 Jun 2026 19:14:02 +0800 Subject: [PATCH 087/122] fix(coding-agent): normalize session names --- .../coding-agent/src/core/session-manager.ts | 3 +- .../src/modes/interactive/interactive-mode.ts | 6 ++- .../5996-session-name-newlines.test.ts | 40 +++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 packages/coding-agent/test/suite/regressions/5996-session-name-newlines.test.ts diff --git a/packages/coding-agent/src/core/session-manager.ts b/packages/coding-agent/src/core/session-manager.ts index c2bae164..3d31aff5 100644 --- a/packages/coding-agent/src/core/session-manager.ts +++ b/packages/coding-agent/src/core/session-manager.ts @@ -1025,12 +1025,13 @@ export class SessionManager { /** Append a session info entry (e.g., display name). Returns entry id. */ appendSessionInfo(name: string): string { + const sanitizedName = name.replace(/[\r\n]+/g, " ").trim(); const entry: SessionInfoEntry = { type: "session_info", id: generateId(this.byId), parentId: this.leafId, timestamp: new Date().toISOString(), - name: name.trim(), + name: sanitizedName, }; this._appendEntry(entry); return entry.id; diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index d50611af..80e49d78 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -5340,8 +5340,12 @@ export class InteractiveMode { } this.session.setSessionName(name); + const sessionName = this.sessionManager.getSessionName(); + if (sessionName !== name) { + this.showWarning(`Session name was normalized from ${JSON.stringify(name)} to ${JSON.stringify(sessionName)}`); + } this.chatContainer.addChild(new Spacer(1)); - this.chatContainer.addChild(new Text(theme.fg("dim", `Session name set: ${name}`), 1, 0)); + this.chatContainer.addChild(new Text(theme.fg("dim", `Session name set: ${sessionName ?? name}`), 1, 0)); this.ui.requestRender(); } diff --git a/packages/coding-agent/test/suite/regressions/5996-session-name-newlines.test.ts b/packages/coding-agent/test/suite/regressions/5996-session-name-newlines.test.ts new file mode 100644 index 00000000..c33f7760 --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/5996-session-name-newlines.test.ts @@ -0,0 +1,40 @@ +import { afterEach, describe, expect, it } from "vitest"; +import type { ExtensionAPI } from "../../../src/index.ts"; +import { createHarness, type Harness } from "../harness.ts"; + +describe("regression #5996: session names do not contain newlines", () => { + const harnesses: Harness[] = []; + + afterEach(() => { + while (harnesses.length > 0) { + harnesses.pop()?.cleanup(); + } + }); + + it("filters newlines when AgentSession.setSessionName is called", async () => { + const harness = await createHarness(); + harnesses.push(harness); + + harness.session.setSessionName("hello\nworld\r\nagain"); + + expect(harness.sessionManager.getSessionName()).toBe("hello world again"); + expect(harness.eventsOfType("session_info_changed").map((event) => event.name)).toEqual(["hello world again"]); + }); + + it("filters newlines when an extension calls pi.setSessionName", async () => { + let api: ExtensionAPI | undefined; + const harness = await createHarness({ + extensionFactories: [ + (pi) => { + api = pi; + }, + ], + }); + harnesses.push(harness); + + api?.setSessionName("from\nextension"); + + expect(harness.sessionManager.getSessionName()).toBe("from extension"); + expect(harness.eventsOfType("session_info_changed").map((event) => event.name)).toEqual(["from extension"]); + }); +}); From e8ede14f5d51bae1ef1290cbd051351760936c65 Mon Sep 17 00:00:00 2001 From: haoqixu Date: Tue, 23 Jun 2026 19:32:55 +0800 Subject: [PATCH 088/122] fix(agent): normalize session names --- packages/agent/src/harness/session/session.ts | 3 ++- packages/agent/test/harness/session.test.ts | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/agent/src/harness/session/session.ts b/packages/agent/src/harness/session/session.ts index 6f136208..ce369dff 100644 --- a/packages/agent/src/harness/session/session.ts +++ b/packages/agent/src/harness/session/session.ts @@ -234,12 +234,13 @@ export class Session { } async appendSessionName(name: string): Promise { + const sanitizedName = name.replace(/[\r\n]+/g, " ").trim(); return this.appendTypedEntry({ type: "session_info", id: await this.storage.createEntryId(), parentId: await this.storage.getLeafId(), timestamp: new Date().toISOString(), - name: name.trim(), + name: sanitizedName, } satisfies SessionInfoEntry); } diff --git a/packages/agent/test/harness/session.test.ts b/packages/agent/test/harness/session.test.ts index c9598da8..39e285b5 100644 --- a/packages/agent/test/harness/session.test.ts +++ b/packages/agent/test/harness/session.test.ts @@ -86,6 +86,12 @@ async function runSessionSuite( expect(context.messages[1]?.role).toBe("custom"); }); + it("normalizes session names", async () => { + const session = new Session(await createStorage()); + await session.appendSessionName(" hello\nworld\r\nagain "); + expect(await session.getSessionName()).toBe("hello world again"); + }); + it("supports labels and session info entries without affecting context", async () => { const session = new Session(await createStorage()); const user1 = await session.appendMessage(createUserMessage("one")); From 5a8ea0bc8192325b3e0e2f191e2997c9912066cf Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 23 Jun 2026 13:36:44 +0200 Subject: [PATCH 089/122] fix(ai): honor scoped AWS profile in Bedrock endpoint resolution --- packages/ai/src/api/bedrock-converse-stream.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ai/src/api/bedrock-converse-stream.ts b/packages/ai/src/api/bedrock-converse-stream.ts index 51d609fc..72c23dc6 100644 --- a/packages/ai/src/api/bedrock-converse-stream.ts +++ b/packages/ai/src/api/bedrock-converse-stream.ts @@ -127,7 +127,7 @@ export const stream: StreamFunction<"bedrock-converse-stream", BedrockOptions> = profile: options.profile || getProviderEnvValue("AWS_PROFILE", options.env), }; const configuredRegion = getConfiguredBedrockRegion(options); - const hasAmbientConfiguredProfile = Boolean(getProviderEnvValue("AWS_PROFILE")); + const hasAmbientConfiguredProfile = Boolean(getProviderEnvValue("AWS_PROFILE", options.env)); const endpointRegion = getStandardBedrockEndpointRegion(model.baseUrl); const useExplicitEndpoint = shouldUseExplicitBedrockEndpoint( model.baseUrl, From 2cbce395fb43386db0dc2cf4ceae89596ba81d57 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 23 Jun 2026 13:39:39 +0200 Subject: [PATCH 090/122] feat(ai): pass provider-resolved env to APIs --- packages/ai/CHANGELOG.md | 2 ++ packages/ai/src/auth/types.ts | 4 ++- packages/ai/src/images-models.ts | 6 ++-- packages/ai/src/models.ts | 5 ++-- packages/ai/src/types.ts | 5 ++++ packages/ai/test/images-models.test.ts | 39 ++++++++++++++++++++++++ packages/ai/test/providers.test.ts | 41 ++++++++++++++++++++++++++ 7 files changed, 97 insertions(+), 5 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 8a43f32f..58a32a70 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -17,9 +17,11 @@ - `fauxProvider()` returns a faux `Provider` for tests built on explicit `Models` collections. - Image generation mirrors the chat-side design: `createImagesModels()`/`ImagesProvider`/`createImagesProvider()` with sync model reads, explicit `refresh()`, provider-resolved auth, and never-rejecting `generateImages()`; `openrouterImagesProvider()` factory plus `builtinImagesProviders()`/`builtinImagesModels()` in `providers/all`. The `ImagesProvider` id type alias is renamed to `ImagesProviderId`; the old global image API stays on `/compat`. - When Amazon Bedrock rejects an unsupported data retention mode, the error now links the AWS data retention documentation ([#5561](https://github.com/earendil-works/pi/pull/5561) by [@unexge](https://github.com/unexge)). +- Provider auth results can carry provider-scoped environment values that `Models` and `ImagesModels` merge into API implementation options. ### Fixed +- Fixed Amazon Bedrock endpoint resolution to honor scoped `AWS_PROFILE` values. - Fixed OpenCode Go GLM-5.2 metadata to expose `xhigh` reasoning and send `reasoning_effort: "max"` ([#5967](https://github.com/earendil-works/pi/issues/5967)). - Fixed Claude Fable 5 thinking-off requests to omit Anthropic's unsupported `thinking.type: "disabled"` payload ([#5567](https://github.com/earendil-works/pi/pull/5567) by [@tmustier](https://github.com/tmustier)). diff --git a/packages/ai/src/auth/types.ts b/packages/ai/src/auth/types.ts index 74bb45bf..6cb285cf 100644 --- a/packages/ai/src/auth/types.ts +++ b/packages/ai/src/auth/types.ts @@ -1,4 +1,4 @@ -import type { Api, ImagesApi, ImagesModel, Model } from "../types.ts"; +import type { Api, ImagesApi, ImagesModel, Model, ProviderEnv } from "../types.ts"; import type { OAuthCredentials } from "../utils/oauth/types.ts"; /** @@ -78,6 +78,8 @@ export interface AuthContext { /** Result of resolving auth for a model. */ export interface AuthResult { auth: ModelAuth; + /** Provider-scoped environment/config values resolved from credentials and ambient context. */ + env?: ProviderEnv; /** Human-readable label for status UI: "ANTHROPIC_API_KEY", "OAuth", "~/.aws/credentials". */ source?: string; } diff --git a/packages/ai/src/images-models.ts b/packages/ai/src/images-models.ts index 67449b03..8dd655d4 100644 --- a/packages/ai/src/images-models.ts +++ b/packages/ai/src/images-models.ts @@ -192,11 +192,13 @@ class ImagesModelsImpl implements MutableImagesModels { const requestModel = auth.baseUrl ? { ...model, baseUrl: auth.baseUrl } : model; - // Explicit request options win per-field; headers merge per header. + // Explicit request options win per-field; headers/env merge per key. const apiKey = options?.apiKey ?? auth.apiKey; const headers = auth.headers || options?.headers ? { ...auth.headers, ...options?.headers } : undefined; + const env = + resolution.env || options?.env ? { ...(resolution.env ?? {}), ...(options?.env ?? {}) } : undefined; - return await provider.generateImages(requestModel, context, { ...options, apiKey, headers }); + return await provider.generateImages(requestModel, context, { ...options, apiKey, headers, env }); } catch (error) { return { api: model.api, diff --git a/packages/ai/src/models.ts b/packages/ai/src/models.ts index 47ed7c8a..de17f8ec 100644 --- a/packages/ai/src/models.ts +++ b/packages/ai/src/models.ts @@ -236,10 +236,11 @@ class ModelsImpl implements MutableModels { const requestModel = auth.baseUrl ? { ...model, baseUrl: auth.baseUrl } : model; - // Explicit request options win per-field; headers merge per header. + // Explicit request options win per-field; headers/env merge per key. const apiKey = options?.apiKey ?? auth.apiKey; const headers = auth.headers || options?.headers ? { ...auth.headers, ...options?.headers } : undefined; - const requestOptions = { ...options, apiKey, headers } as TOptions; + const env = resolution.env || options?.env ? { ...(resolution.env ?? {}), ...(options?.env ?? {}) } : undefined; + const requestOptions = { ...options, apiKey, headers, env } as TOptions; return { requestModel, requestOptions }; } diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index 48a6961e..35b79b7c 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -239,6 +239,11 @@ export interface ProviderImages { export interface ImagesOptions { signal?: AbortSignal; apiKey?: string; + /** + * Provider-scoped environment values. These take precedence over process.env for + * provider configuration such as endpoint placeholders and proxy variables. + */ + env?: ProviderEnv; /** * Optional callback for inspecting or replacing provider payloads before sending. * Return undefined to keep the payload unchanged. diff --git a/packages/ai/test/images-models.test.ts b/packages/ai/test/images-models.test.ts index 1b2e08da..0a2a9e56 100644 --- a/packages/ai/test/images-models.test.ts +++ b/packages/ai/test/images-models.test.ts @@ -102,6 +102,45 @@ describe("ImagesModels", () => { expect(calls[1].options?.apiKey).toBe("explicit"); }); + it("merges provider-resolved env into image options", async () => { + const calls: GenerateCall[] = []; + const models = createImagesModels(); + models.setProvider( + createImagesProvider({ + id: "p1", + auth: { + apiKey: { + name: "Test key", + resolve: async () => ({ + auth: { apiKey: "provider-key" }, + env: { PROVIDER_ONLY: "provider", SHARED: "provider" }, + }), + }, + }, + models: [testImageModel("p1", "model-a")], + api: { + generateImages: async (model, _context, options) => { + calls.push({ model, options }); + return okResult(model); + }, + }, + }), + ); + const model = models.getModel("p1", "model-a")!; + + await models.generateImages(model, context, { + apiKey: "request-key", + env: { REQUEST_ONLY: "request", SHARED: "request" }, + }); + + expect(calls[0].options?.apiKey).toBe("request-key"); + expect(calls[0].options?.env).toEqual({ + PROVIDER_ONLY: "provider", + REQUEST_ONLY: "request", + SHARED: "request", + }); + }); + it("returns an error result for unknown providers and unconfigured auth rejections", async () => { const models = createImagesModels({ authContext: fakeAuthContext({}) }); const ghost = await models.generateImages(testImageModel("ghost", "m"), context); diff --git a/packages/ai/test/providers.test.ts b/packages/ai/test/providers.test.ts index 3e6320a9..56a72b68 100644 --- a/packages/ai/test/providers.test.ts +++ b/packages/ai/test/providers.test.ts @@ -168,6 +168,47 @@ describe("createProvider", () => { expect(calls).toEqual(["a:model-a", "b:model-b"]); }); + it("merges provider-resolved env into stream options", async () => { + let capturedEnv: Record | undefined; + let capturedApiKey: string | undefined; + const envModel = { ...testModel("api-a", "model-a"), provider: "env-provider" }; + const provider = createProvider({ + id: "env-provider", + auth: { + apiKey: { + name: "Test", + resolve: async () => ({ + auth: { apiKey: "provider-key" }, + env: { PROVIDER_ONLY: "provider", SHARED: "provider" }, + }), + }, + }, + models: [envModel], + api: { + stream: (model, _context, options) => { + capturedEnv = options?.env; + capturedApiKey = options?.apiKey; + return recordingStreams("a", []).stream(model, _context, options); + }, + streamSimple: (model, _context, options) => { + capturedEnv = options?.env; + capturedApiKey = options?.apiKey; + return recordingStreams("a", []).streamSimple(model, _context, options); + }, + }, + }); + const models = createModels(); + models.setProvider(provider); + + await models.completeSimple(envModel, context, { + apiKey: "request-key", + env: { REQUEST_ONLY: "request", SHARED: "request" }, + }); + + expect(capturedApiKey).toBe("request-key"); + expect(capturedEnv).toEqual({ PROVIDER_ONLY: "provider", REQUEST_ONLY: "request", SHARED: "request" }); + }); + it("produces a stream error for a model whose api has no implementation", async () => { const provider = createProvider({ id: "mixed", From 8eeaa2bcbaf4d453944598c56f682b2ec7c35e42 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 23 Jun 2026 13:45:26 +0200 Subject: [PATCH 091/122] fix(ai): honor scoped env in compat API key injection --- packages/ai/src/compat.ts | 2 +- packages/ai/test/compat-env.test.ts | 74 +++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 packages/ai/test/compat-env.test.ts diff --git a/packages/ai/src/compat.ts b/packages/ai/src/compat.ts index 7ddbbaaf..fff4746e 100644 --- a/packages/ai/src/compat.ts +++ b/packages/ai/src/compat.ts @@ -100,7 +100,7 @@ function withEnvApiKey( options: TOptions | undefined, ): TOptions | undefined { if (hasExplicitApiKey(options?.apiKey)) return options; - const apiKey = getEnvApiKey(model.provider); + const apiKey = getEnvApiKey(model.provider, options?.env); if (!apiKey) return options; return { ...options, apiKey } as TOptions; } diff --git a/packages/ai/test/compat-env.test.ts b/packages/ai/test/compat-env.test.ts new file mode 100644 index 00000000..69a57da1 --- /dev/null +++ b/packages/ai/test/compat-env.test.ts @@ -0,0 +1,74 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { complete, registerApiProvider, resetApiProviders } from "../src/compat.ts"; +import type { AssistantMessage, Context, Model } from "../src/types.ts"; +import { AssistantMessageEventStream } from "../src/utils/event-stream.ts"; + +const context: Context = { messages: [{ role: "user", content: "hi", timestamp: Date.now() }] }; + +const model: Model<"openai-responses"> = { + id: "test-model", + name: "Test Model", + api: "openai-responses", + provider: "openai", + baseUrl: "https://example.test/v1", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128000, + maxTokens: 4096, +}; + +function message(): AssistantMessage { + return { + role: "assistant", + content: [{ type: "text", text: "ok" }], + api: model.api, + provider: model.provider, + model: model.id, + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "stop", + timestamp: Date.now(), + }; +} + +describe("compat env API key injection", () => { + afterEach(() => { + resetApiProviders(); + }); + + it("uses request-scoped env when injecting provider API keys", async () => { + let capturedApiKey: string | undefined; + registerApiProvider({ + api: "openai-responses", + stream: (_model, _context, options) => { + capturedApiKey = options?.apiKey; + const stream = new AssistantMessageEventStream(); + const output = message(); + stream.push({ type: "start", partial: output }); + stream.push({ type: "done", reason: "stop", message: output }); + stream.end(output); + return stream; + }, + streamSimple: (_model, _context, options) => { + capturedApiKey = options?.apiKey; + const stream = new AssistantMessageEventStream(); + const output = message(); + stream.push({ type: "start", partial: output }); + stream.push({ type: "done", reason: "stop", message: output }); + stream.end(output); + return stream; + }, + }); + + await complete(model, context, { env: { OPENAI_API_KEY: "scoped-key" } }); + + expect(capturedApiKey).toBe("scoped-key"); + }); +}); From 129eb460cdf88c1b3034620b91993decd714ed56 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 23 Jun 2026 15:29:17 +0200 Subject: [PATCH 092/122] feat(ai): complete models runtime migration --- packages/agent/docs/models.md | 15 +- packages/agent/test/e2e.test.ts | 2 +- packages/ai/CHANGELOG.md | 28 ++ packages/ai/scripts/generate-models.ts | 146 ++++++- packages/ai/src/api-registry.ts | 98 ----- packages/ai/src/api/anthropic-messages.ts | 111 +++-- .../ai/src/api/bedrock-converse-stream.ts | 6 +- packages/ai/src/api/cloudflare.ts | 21 - packages/ai/src/api/google-generative-ai.ts | 9 +- packages/ai/src/api/google-vertex.ts | 16 +- packages/ai/src/api/openai-codex-responses.ts | 13 +- packages/ai/src/api/openai-completions.ts | 199 +++------ packages/ai/src/api/openai-responses.ts | 49 ++- packages/ai/src/api/openrouter-images.ts | 10 +- packages/ai/src/auth/types.ts | 4 +- packages/ai/src/compat.ts | 136 +++++- packages/ai/src/models.ts | 5 +- packages/ai/src/providers/ant-ling.models.ts | 6 +- packages/ai/src/providers/cerebras.models.ts | 2 + .../providers/cloudflare-ai-gateway.models.ts | 30 +- .../ai/src/providers/cloudflare-ai-gateway.ts | 4 +- packages/ai/src/providers/cloudflare-auth.ts | 108 +++++ .../providers/cloudflare-workers-ai.models.ts | 26 +- .../ai/src/providers/cloudflare-workers-ai.ts | 4 +- packages/ai/src/providers/deepseek.models.ts | 4 +- packages/ai/src/providers/faux.ts | 22 +- .../ai/src/providers/huggingface.models.ts | 396 ++++++++++++++++++ .../ai/src/providers/opencode-go.models.ts | 20 +- packages/ai/src/providers/opencode.models.ts | 46 +- .../ai/src/providers/openrouter.models.ts | 326 ++++++++++++-- packages/ai/src/providers/together.models.ts | 38 +- packages/ai/src/providers/xai.models.ts | 7 + .../ai/src/providers/zai-coding-cn.models.ts | 12 +- packages/ai/src/providers/zai.models.ts | 12 +- packages/ai/src/types.ts | 7 +- packages/ai/src/utils/headers.ts | 11 + packages/ai/test/compat-env.test.ts | 10 +- .../openai-completions-empty-tools.test.ts | 38 +- packages/ai/test/providers.test.ts | 51 +++ .../src/core/provider-attribution.ts | 8 +- .../test/agent-session-runtime-events.test.ts | 2 +- .../test/sdk-openrouter-attribution.test.ts | 3 +- .../test/suite/agent-session-runtime.test.ts | 2 +- packages/coding-agent/test/suite/harness.ts | 9 +- ...753-reload-stale-resource-settings.test.ts | 2 +- .../2860-replaced-session-context.test.ts | 2 +- .../5596-missing-theme-export.test.ts | 2 +- 47 files changed, 1502 insertions(+), 576 deletions(-) delete mode 100644 packages/ai/src/api-registry.ts create mode 100644 packages/ai/src/providers/cloudflare-auth.ts diff --git a/packages/agent/docs/models.md b/packages/agent/docs/models.md index 2395b030..0be60235 100644 --- a/packages/agent/docs/models.md +++ b/packages/agent/docs/models.md @@ -702,10 +702,10 @@ Built-in provider factories use `createProvider()` internally. models.json custo `@earendil-works/pi-ai/compat` preserves the old global API surface until the coding-agent migration deletes it. New code never imports it. -Old semantics being preserved: global `stream()` dispatched purely on `model.api` via the api-registry, with env API key injection. The compat module reproduces this exactly — it does not route through a `Models` collection, so compat consumers get zero behavioral drift (a `Models`-routed variant was considered and dropped: a model with a known provider id but a different api would dispatch wrong, and auth semantics would shift mid-migration). The harness `Models` instance (Phase 6/7) is where new-path streaming happens. +Old semantics being preserved: global `stream()` can still dispatch by `model.api` through the legacy api-registry for custom providers, mutated models, and tests/extensions that override a built-in API implementation. -- `stream/complete/streamSimple/completeSimple(model, ctx, opts)`: api-dispatch via the api-registry plus `getEnvApiKey` injection, verbatim old behavior. -- The builtin api registration side effect moves from the root barrel into compat. It skips api ids that already have a registration, since compat may load after a test or extension registered an override. `registerApiProvider()/unregisterApiProviders()` keep feeding the registry; `resetApiProviders()` clears and re-registers builtins. +- `stream/complete/streamSimple/completeSimple(model, ctx, opts)`: real built-in provider/model/api matches route through a singleton `builtinModels()` collection, so provider auth/env/baseUrl behavior is shared with the new runtime. Unknown providers, mutated models, or overridden API registrations fall back to api-registry dispatch plus `getEnvApiKey` injection. +- The builtin api registration side effect moves from the root barrel into compat. It skips api ids that already have a registration, since compat may load after a test or extension has already registered an override. `registerApiProvider()/unregisterApiProviders()` keep feeding the compat-local registry; `resetApiProviders()` clears and re-registers builtins. - Sync `getModel/getModels/getProviders` are deprecated aliases of `getBuiltinModel/getBuiltinModels/getBuiltinProviders` from `providers/all` (they were always pure generated-catalog reads — verified: nothing ever mutated the old `modelRegistry`). - Re-exports the per-API lazy stream wrappers (incl. `setBedrockProviderModule`), `env-api-keys.ts`, and the image-generation registry/catalogs; none of these stay on the root barrel. - `export * from "./index.ts"`: compat is a strict superset of the core entrypoint, so consumers switch a file's import path wholesale without symbol surgery. @@ -873,15 +873,16 @@ Decisions: - Runtime `--api-key` overrides are a 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` become async, delegating to the collection (no materialized snapshot, no sync lies; dynamic providers like llama.cpp work). The extension-facing `modelRegistry` surface changes accordingly (breaking, changelogged); 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. The api-registry `registerApiProvider` dual-write stays for compat consumers (extensions calling global `complete()`); it dies with compat. +- 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. - 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 metadata/env -> `ModelAuth.baseUrl`); impl-side `resolveCloudflareBaseUrl` stays until compat dies (it is idempotent on substituted URLs), keeping extension `complete()` calls working. +- Cloudflare: provider-auth substitution (key + `CLOUDFLARE_ACCOUNT_ID`/`CLOUDFLARE_GATEWAY_ID` from credential metadata/env -> `ModelAuth.baseUrl`). Built-in compat calls route through `Models`, so they use the same provider auth path. Ordering: - [x] pi-ai rework first: `Provider.getModels()` sync + optional `refreshModels()`; `Models.getModels`/`getModel` sync, `Models.refresh(provider?)` async; `createProvider` takes `models` array + optional `refreshModels` fetcher (in-flight dedupe). Reverses Phase 1's async-listing decision — see "Provider model listing" for rationale (sync-or-async unions breed latent sync assumptions; async-only breaks sync consumer surfaces like extension `find`/`getAll`). - [ ] `FileCredentialStore` (ports the auth.json lock backend, reads legacy `type: "api_key"` tags) + `--api-key` overlay + `$ENV`/`!command` resolution; tests. -- [ ] Cloudflare provider auth in pi-ai factories; copilot `getModels` baseUrl wrap. +- [x] Cloudflare provider auth in pi-ai factories. +- [ ] Copilot `getModels` baseUrl wrap. - [ ] Extension-OAuth adapter (old `OAuthProviderInterface` config -> `OAuthAuth`). - [ ] ModelRegistry rebuild: owns `MutableModels`, async reads, models.json decoration, provider-keyed extension streams, facade auth ops; AuthStorage deleted. - [ ] Consumer rewiring: agent-session, sdk (`credentials?: CredentialStore` option replaces `authStorage`; sdk.md updated), model-resolver, interactive login/status UI on `prompt()/notify()`, cli `--api-key`. @@ -891,7 +892,7 @@ Ordering: - [ ] 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`, `api-registry.ts`, `env-api-keys.ts`, the extension-loader root-to-compat alias, the old `pi-ai/oauth` registry and `OAuthProviderInterface` (incl. `usesCallbackServer`), and the impl-side cloudflare substitution. 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, 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. ### Deferred / follow-ups diff --git a/packages/agent/test/e2e.test.ts b/packages/agent/test/e2e.test.ts index 309536ad..57d70585 100644 --- a/packages/agent/test/e2e.test.ts +++ b/packages/agent/test/e2e.test.ts @@ -9,7 +9,7 @@ import { registerFauxProvider, type ToolResultMessage, type UserMessage, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/compat"; import { afterEach, describe, expect, it } from "vitest"; import { Agent, type AgentEvent } from "../src/index.ts"; import { calculateTool } from "./utils/calculate.ts"; diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index b44637c1..28bd8d48 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -8,6 +8,33 @@ - Renamed the `Provider` type to `ProviderId`. `Provider` now names the runtime provider interface (id, name, auth, model listing, stream behavior). - API implementation modules moved from `src/providers/` to `@earendil-works/pi-ai/api/*`, renamed by API id (`anthropic` -> `api/anthropic-messages`, `google` -> `api/google-generative-ai`, `mistral` -> `api/mistral-conversations`, `amazon-bedrock` -> `api/bedrock-converse-stream`), each exporting exactly `stream` and `streamSimple`. The old per-impl export names (`streamAnthropic`, `streamSimpleAnthropic`, ...) are gone; the legacy package subpaths (`./anthropic`, `./google`, ...) keep working and point at the new modules. +Migration guide: + +- To keep the old global API temporarily, change imports from `@earendil-works/pi-ai` to `@earendil-works/pi-ai/compat`. The compat entrypoint preserves `stream`/`complete`, generated catalog reads, API registry helpers, env API-key helpers, lazy API wrappers, and image globals, but it will be removed in a future release. +- To migrate to the new runtime, create a `Models` collection and call methods on it: + + ```ts + import { builtinModels } from "@earendil-works/pi-ai/providers/all"; + + const models = builtinModels(); + const model = models.getModel("anthropic", "claude-haiku-4-5"); + if (!model) throw new Error("model not found"); + + const message = await models.complete(model, { + messages: [{ role: "user", content: "Hello", timestamp: Date.now() }], + }); + ``` + +- For an isolated provider set, register provider factories explicitly: + + ```ts + import { createModels } from "@earendil-works/pi-ai"; + import { anthropicProvider } from "@earendil-works/pi-ai/providers/anthropic"; + + const models = createModels(); + models.setProvider(anthropicProvider()); + ``` + ### Added - New `Models` runtime: `createModels()` builds an isolated provider collection with sync model reads (`getModels`/`getModel` return the last-known lists), an explicit async `refresh(provider?)` for dynamic providers, auth resolution (`getAuth`), and `stream`/`complete`/`streamSimple`/`completeSimple` that resolve auth through the owning provider. `createProvider()` builds providers from parts (single API implementation or a map dispatched on `model.api`; static `models` array plus an optional `refreshModels` fetcher with in-flight dedupe); `hasApi()` narrows dynamically listed models. @@ -22,6 +49,7 @@ ### Fixed - Fixed Amazon Bedrock endpoint resolution to honor scoped `AWS_PROFILE` values. +- Fixed Cloudflare providers to require account/gateway configuration and route built-in `/compat` requests through provider auth. - Fixed OpenAI Codex Responses WebSocket sessions to reconnect once when OpenAI's connection limit is reached before output starts ([#5973](https://github.com/earendil-works/pi/issues/5973)). - Fixed OpenCode Go GLM-5.2 metadata to expose `xhigh` reasoning and send `reasoning_effort: "max"` ([#5967](https://github.com/earendil-works/pi/issues/5967)). - Fixed Claude Fable 5 thinking-off requests to omit Anthropic's unsupported `thinking.type: "disabled"` payload ([#5567](https://github.com/earendil-works/pi/pull/5567) by [@tmustier](https://github.com/tmustier)). diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index b6bcde6e..07024585 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -277,10 +277,148 @@ function isAnthropicTemperatureUnsupportedModel(modelId: string): boolean { return id.includes("opus-4-7") || id.includes("opus-4.7") || id.includes("opus-4-8") || id.includes("opus-4.8"); } +const OPENAI_COMPLETIONS_DEFAULT_COMPAT = { + supportsStore: true, + supportsDeveloperRole: true, + supportsReasoningEffort: true, + supportsUsageInStreaming: true, + maxTokensField: "max_completion_tokens", + requiresToolResultName: false, + requiresAssistantAfterToolResult: false, + requiresThinkingAsText: false, + requiresReasoningContentOnAssistantMessages: false, + thinkingFormat: "openai", + openRouterRouting: {}, + vercelGatewayRouting: {}, + chatTemplateKwargs: {}, + zaiToolStream: false, + supportsStrictMode: true, + sendSessionAffinityHeaders: false, + supportsLongCacheRetention: true, +} satisfies Required> & { + cacheControlFormat?: OpenAICompletionsCompat["cacheControlFormat"]; +}; + +type OpenAICompletionsResolvedCompat = typeof OPENAI_COMPLETIONS_DEFAULT_COMPAT & { + cacheControlFormat?: OpenAICompletionsCompat["cacheControlFormat"]; +}; + function mergeAnthropicMessagesCompat(model: Model, compat: AnthropicMessagesCompat): void { model.compat = { ...(model.compat as AnthropicMessagesCompat | undefined), ...compat }; } +function detectOpenAICompletionsCompat(model: Model<"openai-completions">): OpenAICompletionsResolvedCompat { + const provider = model.provider; + const baseUrl = model.baseUrl; + + const isZai = + provider === "zai" || + provider === "zai-coding-cn" || + baseUrl.includes("api.z.ai") || + baseUrl.includes("open.bigmodel.cn"); + const isTogether = + provider === "together" || baseUrl.includes("api.together.ai") || baseUrl.includes("api.together.xyz"); + const isMoonshot = provider === "moonshotai" || provider === "moonshotai-cn" || baseUrl.includes("api.moonshot."); + const isOpenRouter = provider === "openrouter" || baseUrl.includes("openrouter.ai"); + const isCloudflareWorkersAI = provider === "cloudflare-workers-ai" || baseUrl.includes("api.cloudflare.com"); + const isCloudflareAiGateway = provider === "cloudflare-ai-gateway" || baseUrl.includes("gateway.ai.cloudflare.com"); + const isNvidia = provider === "nvidia" || baseUrl.includes("integrate.api.nvidia.com"); + const isAntLing = provider === "ant-ling" || baseUrl.includes("api.ant-ling.com"); + + const isNonStandard = + isNvidia || + provider === "cerebras" || + baseUrl.includes("cerebras.ai") || + provider === "xai" || + baseUrl.includes("api.x.ai") || + isTogether || + baseUrl.includes("chutes.ai") || + baseUrl.includes("deepseek.com") || + isZai || + isMoonshot || + provider === "opencode" || + baseUrl.includes("opencode.ai") || + isCloudflareWorkersAI || + isCloudflareAiGateway || + isAntLing; + + const useMaxTokens = + baseUrl.includes("chutes.ai") || isMoonshot || isCloudflareAiGateway || isTogether || isNvidia || isAntLing; + + const isGrok = provider === "xai" || baseUrl.includes("api.x.ai"); + const isDeepSeek = provider === "deepseek" || baseUrl.includes("deepseek.com"); + const isOpenRouterDeveloperRoleModel = + isOpenRouter && (model.id.startsWith("anthropic/") || model.id.startsWith("openai/")); + const cacheControlFormat = provider === "openrouter" && model.id.startsWith("anthropic/") ? "anthropic" : undefined; + + return { + supportsStore: !isNonStandard, + supportsDeveloperRole: isOpenRouterDeveloperRoleModel || (!isNonStandard && !isOpenRouter), + supportsReasoningEffort: + !isGrok && !isZai && !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia && !isAntLing, + supportsUsageInStreaming: true, + maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens", + requiresToolResultName: false, + requiresAssistantAfterToolResult: false, + requiresThinkingAsText: false, + requiresReasoningContentOnAssistantMessages: isDeepSeek, + thinkingFormat: isDeepSeek + ? "deepseek" + : isZai + ? "zai" + : isTogether + ? "together" + : isAntLing + ? "ant-ling" + : isOpenRouter + ? "openrouter" + : "openai", + openRouterRouting: {}, + vercelGatewayRouting: {}, + chatTemplateKwargs: {}, + zaiToolStream: false, + supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia, + ...(cacheControlFormat ? { cacheControlFormat } : {}), + sendSessionAffinityHeaders: false, + supportsLongCacheRetention: !( + isTogether || + isCloudflareWorkersAI || + isCloudflareAiGateway || + isNvidia || + isAntLing + ), + }; +} + +function isPlainEmptyObject(value: unknown): boolean { + return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0; +} + +function openAICompletionsCompatDelta(compat: OpenAICompletionsResolvedCompat): OpenAICompletionsCompat { + const delta: OpenAICompletionsCompat = {}; + for (const [key, value] of Object.entries(compat)) { + const defaultValue = OPENAI_COMPLETIONS_DEFAULT_COMPAT[key as keyof typeof OPENAI_COMPLETIONS_DEFAULT_COMPAT]; + if (isPlainEmptyObject(value) && isPlainEmptyObject(defaultValue)) continue; + if (value !== defaultValue) { + (delta as Record)[key] = value; + } + } + return delta; +} + +function mergeOpenAICompletionsCompat(model: Model, compat: OpenAICompletionsCompat): void { + model.compat = { ...(model.compat as OpenAICompletionsCompat | undefined), ...compat }; +} + +function applyOpenAICompletionsCompatMetadata(model: Model): void { + if (model.api !== "openai-completions") return; + const detected = openAICompletionsCompatDelta(detectOpenAICompletionsCompat(model as Model<"openai-completions">)); + model.compat = { ...detected, ...(model.compat as OpenAICompletionsCompat | undefined) }; + if (Object.keys(model.compat).length === 0) { + delete model.compat; + } +} + function isGemini3ProModel(modelId: string): boolean { return /gemini-3(?:\.\d+)?-pro/.test(modelId.toLowerCase()); } @@ -854,9 +992,10 @@ async function loadModelsDevData(): Promise[]> { continue; } - // workers-ai/* through the gateway forwards x-session-affinity to - // the underlying Workers AI runtime for prefix-cache routing. - const compat = upstream === "workers-ai" ? { sendSessionAffinityHeaders: true } : undefined; + // Gateway passthroughs forward session affinity headers to upstreams that + // use them for cache/routing affinity. + const compat = + upstream === "anthropic" || upstream === "workers-ai" ? { sendSessionAffinityHeaders: true } : undefined; models.push({ id, @@ -2096,6 +2235,7 @@ async function generateModels() { for (const model of allModels) { applyThinkingLevelMetadata(model); + applyOpenAICompletionsCompatMetadata(model); } // Group by provider and deduplicate by model ID diff --git a/packages/ai/src/api-registry.ts b/packages/ai/src/api-registry.ts deleted file mode 100644 index d86dd3e9..00000000 --- a/packages/ai/src/api-registry.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { - Api, - AssistantMessageEventStream, - Context, - Model, - SimpleStreamOptions, - StreamFunction, - StreamOptions, -} from "./types.ts"; - -export type ApiStreamFunction = ( - model: Model, - context: Context, - options?: StreamOptions, -) => AssistantMessageEventStream; - -export type ApiStreamSimpleFunction = ( - model: Model, - context: Context, - options?: SimpleStreamOptions, -) => AssistantMessageEventStream; - -export interface ApiProvider { - api: TApi; - stream: StreamFunction; - streamSimple: StreamFunction; -} - -interface ApiProviderInternal { - api: Api; - stream: ApiStreamFunction; - streamSimple: ApiStreamSimpleFunction; -} - -type RegisteredApiProvider = { - provider: ApiProviderInternal; - sourceId?: string; -}; - -const apiProviderRegistry = new Map(); - -function wrapStream( - api: TApi, - stream: StreamFunction, -): ApiStreamFunction { - return (model, context, options) => { - if (model.api !== api) { - throw new Error(`Mismatched api: ${model.api} expected ${api}`); - } - return stream(model as Model, context, options as TOptions); - }; -} - -function wrapStreamSimple( - api: TApi, - streamSimple: StreamFunction, -): ApiStreamSimpleFunction { - return (model, context, options) => { - if (model.api !== api) { - throw new Error(`Mismatched api: ${model.api} expected ${api}`); - } - return streamSimple(model as Model, context, options); - }; -} - -export function registerApiProvider( - provider: ApiProvider, - sourceId?: string, -): void { - apiProviderRegistry.set(provider.api, { - provider: { - api: provider.api, - stream: wrapStream(provider.api, provider.stream), - streamSimple: wrapStreamSimple(provider.api, provider.streamSimple), - }, - sourceId, - }); -} - -export function getApiProvider(api: Api): ApiProviderInternal | undefined { - return apiProviderRegistry.get(api)?.provider; -} - -export function getApiProviders(): ApiProviderInternal[] { - return Array.from(apiProviderRegistry.values(), (entry) => entry.provider); -} - -export function unregisterApiProviders(sourceId: string): void { - for (const [api, entry] of apiProviderRegistry.entries()) { - if (entry.sourceId === sourceId) { - apiProviderRegistry.delete(api); - } - } -} - -export function clearApiProviders(): void { - apiProviderRegistry.clear(); -} diff --git a/packages/ai/src/api/anthropic-messages.ts b/packages/ai/src/api/anthropic-messages.ts index a546db75..03bbc26c 100644 --- a/packages/ai/src/api/anthropic-messages.ts +++ b/packages/ai/src/api/anthropic-messages.ts @@ -18,6 +18,7 @@ import type { Message, Model, ProviderEnv, + ProviderHeaders, SimpleStreamOptions, StopReason, StreamFunction, @@ -34,7 +35,6 @@ import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse.ts" import { getProviderEnvValue } from "../utils/provider-env.ts"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; -import { resolveCloudflareBaseUrl } from "./cloudflare.ts"; import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.ts"; import { adjustMaxTokensForThinking, buildBaseOptions } from "./simple-options.ts"; import { transformMessages } from "./transform-messages.ts"; @@ -170,16 +170,11 @@ const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14"; function getAnthropicCompat( model: Model<"anthropic-messages">, ): Required> { - // Auto-detect session affinity and cache control support from provider - const isFireworks = model.provider === "fireworks"; - const isCloudflareAiGatewayAnthropic = - model.provider === "cloudflare-ai-gateway" && model.baseUrl.includes("anthropic"); return { - supportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? !isFireworks, - supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? !isFireworks, - sendSessionAffinityHeaders: - model.compat?.sendSessionAffinityHeaders ?? !!(isFireworks || isCloudflareAiGatewayAnthropic), - supportsCacheControlOnTools: model.compat?.supportsCacheControlOnTools ?? !isFireworks, + supportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? true, + supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? true, + sendSessionAffinityHeaders: model.compat?.sendSessionAffinityHeaders ?? false, + supportsCacheControlOnTools: model.compat?.supportsCacheControlOnTools ?? true, supportsTemperature: model.compat?.supportsTemperature ?? true, allowEmptySignature: model.compat?.allowEmptySignature ?? false, }; @@ -247,8 +242,8 @@ export interface AnthropicOptions extends StreamOptions { client?: Anthropic; } -function mergeHeaders(...headerSources: (Record | undefined)[]): Record { - const merged: Record = {}; +function mergeHeaders(...headerSources: (ProviderHeaders | undefined)[]): ProviderHeaders { + const merged: ProviderHeaders = {}; for (const headers of headerSources) { if (headers) { Object.assign(merged, headers); @@ -257,6 +252,27 @@ function mergeHeaders(...headerSources: (Record | undefin return merged; } +function hasHeader(headers: ProviderHeaders | undefined, name: string): boolean { + if (!headers) return false; + const expected = name.toLowerCase(); + for (const [key, value] of Object.entries(headers)) { + if (key.toLowerCase() === expected && value !== null && value.trim().length > 0) return true; + } + return false; +} + +function assertRequestAuth(provider: string, apiKey: string | undefined, headers: ProviderHeaders | undefined): void { + if (apiKey) return; + if ( + hasHeader(headers, "authorization") || + hasHeader(headers, "x-api-key") || + hasHeader(headers, "cf-aig-authorization") + ) { + return; + } + throw new Error(`No API key for provider: ${provider}`); +} + interface ServerSentEvent { event: string | null; data: string; @@ -484,9 +500,7 @@ export const stream: StreamFunction<"anthropic-messages", AnthropicOptions> = ( isOAuth = false; } else { const apiKey = options?.apiKey; - if (!apiKey) { - throw new Error(`No API key for provider: ${model.provider}`); - } + assertRequestAuth(model.provider, apiKey, options?.headers); let copilotDynamicHeaders: Record | undefined; if (model.provider === "github-copilot") { @@ -508,7 +522,6 @@ export const stream: StreamFunction<"anthropic-messages", AnthropicOptions> = ( options?.headers, copilotDynamicHeaders, cacheSessionId, - options?.env, ); client = created.client; isOAuth = created.isOAuthToken; @@ -748,12 +761,9 @@ export const streamSimple: StreamFunction<"anthropic-messages", SimpleStreamOpti context: Context, options?: SimpleStreamOptions, ): AssistantMessageEventStream => { - const apiKey = options?.apiKey; - if (!apiKey) { - throw new Error(`No API key for provider: ${model.provider}`); - } + assertRequestAuth(model.provider, options?.apiKey, options?.headers); - const base = buildBaseOptions(model, options, apiKey); + const base = buildBaseOptions(model, options, options?.apiKey); if (!options?.reasoning) { return stream(model, context, { ...base, thinkingEnabled: false } satisfies AnthropicOptions); } @@ -792,13 +802,12 @@ function isOAuthToken(apiKey: string): boolean { function createClient( model: Model<"anthropic-messages">, - apiKey: string, + apiKey: string | undefined, interleavedThinking: boolean, useFineGrainedToolStreamingBeta: boolean, - optionsHeaders?: Record, + optionsHeaders?: ProviderHeaders, dynamicHeaders?: Record, sessionId?: string, - env?: ProviderEnv, ): { client: Anthropic; isOAuthToken: boolean } { // Adaptive thinking models have interleaved thinking built in, so skip the beta header. const needsInterleavedBeta = interleavedThinking && model.compat?.forceAdaptiveThinking !== true; @@ -810,34 +819,11 @@ function createClient( betaFeatures.push(INTERLEAVED_THINKING_BETA); } - if (model.provider === "cloudflare-ai-gateway") { - const client = new Anthropic({ - apiKey: null, - authToken: null, - baseURL: resolveCloudflareBaseUrl(model, env), - dangerouslyAllowBrowser: true, - defaultHeaders: mergeHeaders( - { - accept: "application/json", - "anthropic-dangerous-direct-browser-access": "true", - "cf-aig-authorization": `Bearer ${apiKey}`, - "x-api-key": null, - Authorization: null, - ...(betaFeatures.length > 0 ? { "anthropic-beta": betaFeatures.join(",") } : {}), - }, - model.headers, - optionsHeaders, - ), - }); - - return { client, isOAuthToken: false }; - } - // Copilot: Bearer auth, selective betas. if (model.provider === "github-copilot") { const client = new Anthropic({ apiKey: null, - authToken: apiKey, + authToken: apiKey ?? null, baseURL: model.baseUrl, dangerouslyAllowBrowser: true, defaultHeaders: mergeHeaders( @@ -856,7 +842,7 @@ function createClient( } // OAuth: Bearer auth, Claude Code identity headers - if (isOAuthToken(apiKey)) { + if (apiKey && isOAuthToken(apiKey)) { const client = new Anthropic({ apiKey: null, authToken: apiKey, @@ -878,24 +864,25 @@ function createClient( return { client, isOAuthToken: true }; } - // API key auth - const sessionAffinityHeaders: Record = + // API key or header-owned auth. + const sessionAffinityHeaders: ProviderHeaders = sessionId && getAnthropicCompat(model).sendSessionAffinityHeaders ? { "x-session-affinity": sessionId } : {}; + const defaultHeaders = mergeHeaders( + { + accept: "application/json", + "anthropic-dangerous-direct-browser-access": "true", + ...(betaFeatures.length > 0 ? { "anthropic-beta": betaFeatures.join(",") } : {}), + }, + sessionAffinityHeaders, + model.headers, + optionsHeaders, + ); const client = new Anthropic({ - apiKey, + apiKey: apiKey ?? null, authToken: null, baseURL: model.baseUrl, dangerouslyAllowBrowser: true, - defaultHeaders: mergeHeaders( - { - accept: "application/json", - "anthropic-dangerous-direct-browser-access": "true", - ...(betaFeatures.length > 0 ? { "anthropic-beta": betaFeatures.join(",") } : {}), - }, - sessionAffinityHeaders, - model.headers, - optionsHeaders, - ), + defaultHeaders, }); return { client, isOAuthToken: false }; diff --git a/packages/ai/src/api/bedrock-converse-stream.ts b/packages/ai/src/api/bedrock-converse-stream.ts index 72c23dc6..899bacd9 100644 --- a/packages/ai/src/api/bedrock-converse-stream.ts +++ b/packages/ai/src/api/bedrock-converse-stream.ts @@ -48,6 +48,7 @@ import type { ToolResultMessage, } from "../types.ts"; import { AssistantMessageEventStream } from "../utils/event-stream.ts"; +import { providerHeadersToRecord } from "../utils/headers.ts"; import { parseStreamingJson } from "../utils/json-parse.ts"; import { resolveHttpProxyUrlForTarget } from "../utils/node-http-proxy.ts"; import { getProviderEnvValue } from "../utils/provider-env.ts"; @@ -204,8 +205,9 @@ export const stream: StreamFunction<"bedrock-converse-stream", BedrockOptions> = try { const client = new BedrockRuntimeClient(config); - if (options.headers && Object.keys(options.headers).length > 0) { - addCustomHeadersMiddleware(client, options.headers); + const customHeaders = providerHeadersToRecord(options.headers); + if (customHeaders) { + addCustomHeadersMiddleware(client, customHeaders); } const cacheRetention = resolveCacheRetention(options.cacheRetention, options.env); const inferenceMaxTokens = options.maxTokens ?? (isAnthropicClaudeModel(model) ? model.maxTokens : undefined); diff --git a/packages/ai/src/api/cloudflare.ts b/packages/ai/src/api/cloudflare.ts index 98546419..f8b1138c 100644 --- a/packages/ai/src/api/cloudflare.ts +++ b/packages/ai/src/api/cloudflare.ts @@ -1,6 +1,3 @@ -import type { Api, Model, ProviderEnv } from "../types.ts"; -import { getProviderEnvValue } from "../utils/provider-env.ts"; - /** Workers AI direct endpoint. */ export const CLOUDFLARE_WORKERS_AI_BASE_URL = "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1"; @@ -16,21 +13,3 @@ export const CLOUDFLARE_AI_GATEWAY_OPENAI_BASE_URL = /** AI Gateway → Anthropic passthrough. */ export const CLOUDFLARE_AI_GATEWAY_ANTHROPIC_BASE_URL = "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic"; - -export function isCloudflareProvider(provider: string): boolean { - return provider === "cloudflare-workers-ai" || provider === "cloudflare-ai-gateway"; -} - -/** Substitute `{VAR}` placeholders in a Cloudflare baseUrl from provider env or process.env. */ -export function resolveCloudflareBaseUrl(model: Model, env?: ProviderEnv): string { - const url = model.baseUrl; - if (!url.includes("{")) return url; - const baseUrl = url.replace(/\{([A-Z_][A-Z0-9_]*)\}/g, (_match, name: string) => { - const value = getProviderEnvValue(name, env); - if (!value) { - throw new Error(`${name} is required for provider ${model.provider} but is not set.`); - } - return value; - }); - return baseUrl; -} diff --git a/packages/ai/src/api/google-generative-ai.ts b/packages/ai/src/api/google-generative-ai.ts index d28f97c4..4e402a23 100644 --- a/packages/ai/src/api/google-generative-ai.ts +++ b/packages/ai/src/api/google-generative-ai.ts @@ -10,6 +10,7 @@ import type { AssistantMessage, Context, Model, + ProviderHeaders, SimpleStreamOptions, StreamFunction, StreamOptions, @@ -20,6 +21,7 @@ import type { ToolCall, } from "../types.ts"; import { AssistantMessageEventStream } from "../utils/event-stream.ts"; +import { providerHeadersToRecord } from "../utils/headers.ts"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; import type { GoogleThinkingLevel } from "./google-shared.ts"; import { @@ -318,15 +320,16 @@ export const streamSimple: StreamFunction<"google-generative-ai", SimpleStreamOp function createClient( model: Model<"google-generative-ai">, apiKey?: string, - optionsHeaders?: Record, + optionsHeaders?: ProviderHeaders, ): GoogleGenAI { const httpOptions: { baseUrl?: string; apiVersion?: string; headers?: Record } = {}; if (model.baseUrl) { httpOptions.baseUrl = model.baseUrl; httpOptions.apiVersion = ""; // baseUrl already includes version path, don't append } - if (model.headers || optionsHeaders) { - httpOptions.headers = { ...model.headers, ...optionsHeaders }; + const headers = providerHeadersToRecord({ ...model.headers, ...optionsHeaders }); + if (headers) { + httpOptions.headers = headers; } return new GoogleGenAI({ diff --git a/packages/ai/src/api/google-vertex.ts b/packages/ai/src/api/google-vertex.ts index 79087c61..de0ecd54 100644 --- a/packages/ai/src/api/google-vertex.ts +++ b/packages/ai/src/api/google-vertex.ts @@ -15,6 +15,7 @@ import type { Model, ThinkingLevel as PiThinkingLevel, ProviderEnv, + ProviderHeaders, SimpleStreamOptions, StreamFunction, StreamOptions, @@ -24,6 +25,7 @@ import type { ToolCall, } from "../types.ts"; import { AssistantMessageEventStream } from "../utils/event-stream.ts"; +import { providerHeadersToRecord } from "../utils/headers.ts"; import { getProviderEnvValue } from "../utils/provider-env.ts"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; import type { GoogleThinkingLevel } from "./google-shared.ts"; @@ -334,7 +336,7 @@ function createClient( model: Model<"google-vertex">, project: string, location: string, - optionsHeaders?: Record, + optionsHeaders?: ProviderHeaders, env?: ProviderEnv, ): GoogleGenAI { const googleAuthOptions = buildGoogleAuthOptions(env); @@ -351,7 +353,7 @@ function createClient( function createClientWithApiKey( model: Model<"google-vertex">, apiKey: string, - optionsHeaders?: Record, + optionsHeaders?: ProviderHeaders, ): GoogleGenAI { return new GoogleGenAI({ vertexai: true, @@ -361,10 +363,7 @@ function createClientWithApiKey( }); } -function buildHttpOptions( - model: Model<"google-vertex">, - optionsHeaders?: Record, -): HttpOptions | undefined { +function buildHttpOptions(model: Model<"google-vertex">, optionsHeaders?: ProviderHeaders): HttpOptions | undefined { const httpOptions: HttpOptions = {}; const baseUrl = resolveCustomBaseUrl(model.baseUrl); if (baseUrl) { @@ -375,8 +374,9 @@ function buildHttpOptions( } } - if (model.headers || optionsHeaders) { - httpOptions.headers = { ...model.headers, ...optionsHeaders }; + const headers = providerHeadersToRecord({ ...model.headers, ...optionsHeaders }); + if (headers) { + httpOptions.headers = headers; } return Object.keys(httpOptions).length > 0 ? httpOptions : undefined; diff --git a/packages/ai/src/api/openai-codex-responses.ts b/packages/ai/src/api/openai-codex-responses.ts index 6107bb22..5db519a7 100644 --- a/packages/ai/src/api/openai-codex-responses.ts +++ b/packages/ai/src/api/openai-codex-responses.ts @@ -28,6 +28,7 @@ import type { Context, Model, ProviderEnv, + ProviderHeaders, SimpleStreamOptions, StreamFunction, StreamOptions, @@ -1467,13 +1468,17 @@ function createCodexRequestId(): string { function buildBaseCodexHeaders( initHeaders: Record | undefined, - additionalHeaders: Record | undefined, + additionalHeaders: ProviderHeaders | undefined, accountId: string, token: string, ): Headers { const headers = new Headers(initHeaders); for (const [key, value] of Object.entries(additionalHeaders || {})) { - headers.set(key, value); + if (value === null) { + headers.delete(key); + } else { + headers.set(key, value); + } } headers.set("Authorization", `Bearer ${token}`); headers.set("chatgpt-account-id", accountId); @@ -1485,7 +1490,7 @@ function buildBaseCodexHeaders( function buildSSEHeaders( initHeaders: Record | undefined, - additionalHeaders: Record | undefined, + additionalHeaders: ProviderHeaders | undefined, accountId: string, token: string, sessionId?: string, @@ -1505,7 +1510,7 @@ function buildSSEHeaders( function buildWebSocketHeaders( initHeaders: Record | undefined, - additionalHeaders: Record | undefined, + additionalHeaders: ProviderHeaders | undefined, accountId: string, token: string, requestId: string, diff --git a/packages/ai/src/api/openai-completions.ts b/packages/ai/src/api/openai-completions.ts index e891365e..5204815f 100644 --- a/packages/ai/src/api/openai-completions.ts +++ b/packages/ai/src/api/openai-completions.ts @@ -21,6 +21,7 @@ import type { Model, OpenAICompletionsCompat, ProviderEnv, + ProviderHeaders, SimpleStreamOptions, StopReason, StreamFunction, @@ -36,7 +37,6 @@ import { headersToRecord } from "../utils/headers.ts"; import { parseStreamingJson } from "../utils/json-parse.ts"; import { getProviderEnvValue } from "../utils/provider-env.ts"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; -import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.ts"; import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.ts"; import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts"; import { buildBaseOptions } from "./simple-options.ts"; @@ -47,6 +47,21 @@ import { transformMessages } from "./transform-messages.ts"; * This is needed because Anthropic (via proxy) requires the tools param * to be present when messages include tool_calls or tool role messages. */ +function hasHeader(headers: ProviderHeaders | undefined, name: string): boolean { + if (!headers) return false; + const expected = name.toLowerCase(); + for (const [key, value] of Object.entries(headers)) { + if (key.toLowerCase() === expected && value !== null && value.trim().length > 0) return true; + } + return false; +} + +function getClientApiKey(provider: string, apiKey: string | undefined, headers: ProviderHeaders | undefined): string { + if (apiKey) return apiKey; + if (hasHeader(headers, "authorization") || hasHeader(headers, "cf-aig-authorization")) return "unused"; + throw new Error(`No API key for provider: ${provider}`); +} + function hasToolHistory(messages: Message[]): boolean { for (const msg of messages) { if (msg.role === "toolResult") { @@ -160,14 +175,11 @@ export const stream: StreamFunction<"openai-completions", OpenAICompletionsOptio }; try { - const apiKey = options?.apiKey; - if (!apiKey) { - throw new Error(`No API key for provider: ${model.provider}`); - } + const apiKey = getClientApiKey(model.provider, options?.apiKey, options?.headers); const compat = getCompat(model); const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env); const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId; - const client = createClient(model, context, apiKey, options?.headers, cacheSessionId, compat, options?.env); + const client = createClient(model, context, apiKey, options?.headers, cacheSessionId, compat); let params = buildParams(model, context, options, compat, cacheRetention); const nextParams = await options?.onPayload?.(params, model); if (nextParams !== undefined) { @@ -468,12 +480,9 @@ export const streamSimple: StreamFunction<"openai-completions", SimpleStreamOpti context: Context, options?: SimpleStreamOptions, ): AssistantMessageEventStream => { - const apiKey = options?.apiKey; - if (!apiKey) { - throw new Error(`No API key for provider: ${model.provider}`); - } + getClientApiKey(model.provider, options?.apiKey, options?.headers); - const base = buildBaseOptions(model, options, apiKey); + const base = buildBaseOptions(model, options, options?.apiKey); const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined; const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning; const toolChoice = (options as OpenAICompletionsOptions | undefined)?.toolChoice; @@ -489,12 +498,11 @@ function createClient( model: Model<"openai-completions">, context: Context, apiKey: string, - optionsHeaders?: Record, + optionsHeaders?: ProviderHeaders, sessionId?: string, compat: ResolvedOpenAICompletionsCompat = getCompat(model), - env?: ProviderEnv, ) { - const headers = { ...model.headers }; + const headers: ProviderHeaders = { ...model.headers }; if (model.provider === "github-copilot") { const hasImages = hasCopilotVisionInput(context.messages); const copilotHeaders = buildCopilotDynamicHeaders({ @@ -515,20 +523,11 @@ function createClient( Object.assign(headers, optionsHeaders); } - const defaultHeaders = - model.provider === "cloudflare-ai-gateway" - ? { - ...headers, - Authorization: headers.Authorization ?? null, - "cf-aig-authorization": `Bearer ${apiKey}`, - } - : headers; - return new OpenAI({ apiKey, - baseURL: isCloudflareProvider(model.provider) ? resolveCloudflareBaseUrl(model, env) : model.baseUrl, + baseURL: model.baseUrl, dangerouslyAllowBrowser: true, - defaultHeaders, + defaultHeaders: headers, }); } @@ -674,7 +673,7 @@ function buildParams( } // Vercel AI Gateway provider routing preferences - if (model.baseUrl.includes("ai-gateway.vercel.sh") && model.compat?.vercelGatewayRouting) { + if (model.compat?.vercelGatewayRouting) { const routing = model.compat.vercelGatewayRouting; if (routing.only || routing.order) { const gatewayOptions: Record = {}; @@ -1164,123 +1163,55 @@ function mapStopReason(reason: ChatCompletionChunk.Choice["finish_reason"] | str } } -/** - * Detect compatibility settings from provider and baseUrl for known providers. - * Provider takes precedence over URL-based detection since it's explicitly configured. - * Returns a fully resolved OpenAICompletionsCompat object with all fields set. - */ -function detectCompat(model: Model<"openai-completions">): ResolvedOpenAICompletionsCompat { - const provider = model.provider; - const baseUrl = model.baseUrl; - - const isZai = - provider === "zai" || - provider === "zai-coding-cn" || - baseUrl.includes("api.z.ai") || - baseUrl.includes("open.bigmodel.cn"); - const isTogether = - provider === "together" || baseUrl.includes("api.together.ai") || baseUrl.includes("api.together.xyz"); - const isMoonshot = provider === "moonshotai" || provider === "moonshotai-cn" || baseUrl.includes("api.moonshot."); - const isOpenRouter = provider === "openrouter" || baseUrl.includes("openrouter.ai"); - const isCloudflareWorkersAI = provider === "cloudflare-workers-ai" || baseUrl.includes("api.cloudflare.com"); - const isCloudflareAiGateway = provider === "cloudflare-ai-gateway" || baseUrl.includes("gateway.ai.cloudflare.com"); - const isNvidia = provider === "nvidia" || baseUrl.includes("integrate.api.nvidia.com"); - const isAntLing = provider === "ant-ling" || baseUrl.includes("api.ant-ling.com"); - - const isNonStandard = - isNvidia || - provider === "cerebras" || - baseUrl.includes("cerebras.ai") || - provider === "xai" || - baseUrl.includes("api.x.ai") || - isTogether || - baseUrl.includes("chutes.ai") || - baseUrl.includes("deepseek.com") || - isZai || - isMoonshot || - provider === "opencode" || - baseUrl.includes("opencode.ai") || - isCloudflareWorkersAI || - isCloudflareAiGateway || - isAntLing; - - const useMaxTokens = - baseUrl.includes("chutes.ai") || isMoonshot || isCloudflareAiGateway || isTogether || isNvidia || isAntLing; - - const isGrok = provider === "xai" || baseUrl.includes("api.x.ai"); - const isDeepSeek = provider === "deepseek" || baseUrl.includes("deepseek.com"); - const isOpenRouterDeveloperRoleModel = - isOpenRouter && (model.id.startsWith("anthropic/") || model.id.startsWith("openai/")); - const cacheControlFormat = provider === "openrouter" && model.id.startsWith("anthropic/") ? "anthropic" : undefined; - - return { - supportsStore: !isNonStandard, - supportsDeveloperRole: isOpenRouterDeveloperRoleModel || (!isNonStandard && !isOpenRouter), - supportsReasoningEffort: - !isGrok && !isZai && !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia && !isAntLing, - supportsUsageInStreaming: true, - maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens", - requiresToolResultName: false, - requiresAssistantAfterToolResult: false, - requiresThinkingAsText: false, - requiresReasoningContentOnAssistantMessages: isDeepSeek, - thinkingFormat: isDeepSeek - ? "deepseek" - : isZai - ? "zai" - : isTogether - ? "together" - : isAntLing - ? "ant-ling" - : isOpenRouter - ? "openrouter" - : "openai", - openRouterRouting: {}, - vercelGatewayRouting: {}, - chatTemplateKwargs: {}, - zaiToolStream: false, - supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia, - cacheControlFormat, - sendSessionAffinityHeaders: false, - supportsLongCacheRetention: !( - isTogether || - isCloudflareWorkersAI || - isCloudflareAiGateway || - isNvidia || - isAntLing - ), - }; -} +const DEFAULT_COMPAT: ResolvedOpenAICompletionsCompat = { + supportsStore: true, + supportsDeveloperRole: true, + supportsReasoningEffort: true, + supportsUsageInStreaming: true, + maxTokensField: "max_completion_tokens", + requiresToolResultName: false, + requiresAssistantAfterToolResult: false, + requiresThinkingAsText: false, + requiresReasoningContentOnAssistantMessages: false, + thinkingFormat: "openai", + openRouterRouting: {}, + vercelGatewayRouting: {}, + chatTemplateKwargs: {}, + zaiToolStream: false, + supportsStrictMode: true, + cacheControlFormat: undefined, + sendSessionAffinityHeaders: false, + supportsLongCacheRetention: true, +}; /** * Get resolved compatibility settings for a model. - * Uses explicit model.compat if provided, otherwise auto-detects from provider/URL. + * Uses explicit generated/custom model.compat over OpenAI-standard defaults. */ function getCompat(model: Model<"openai-completions">): ResolvedOpenAICompletionsCompat { - const detected = detectCompat(model); - if (!model.compat) return detected; + if (!model.compat) return DEFAULT_COMPAT; return { - supportsStore: model.compat.supportsStore ?? detected.supportsStore, - supportsDeveloperRole: model.compat.supportsDeveloperRole ?? detected.supportsDeveloperRole, - supportsReasoningEffort: model.compat.supportsReasoningEffort ?? detected.supportsReasoningEffort, - supportsUsageInStreaming: model.compat.supportsUsageInStreaming ?? detected.supportsUsageInStreaming, - maxTokensField: model.compat.maxTokensField ?? detected.maxTokensField, - requiresToolResultName: model.compat.requiresToolResultName ?? detected.requiresToolResultName, + supportsStore: model.compat.supportsStore ?? DEFAULT_COMPAT.supportsStore, + supportsDeveloperRole: model.compat.supportsDeveloperRole ?? DEFAULT_COMPAT.supportsDeveloperRole, + supportsReasoningEffort: model.compat.supportsReasoningEffort ?? DEFAULT_COMPAT.supportsReasoningEffort, + supportsUsageInStreaming: model.compat.supportsUsageInStreaming ?? DEFAULT_COMPAT.supportsUsageInStreaming, + maxTokensField: model.compat.maxTokensField ?? DEFAULT_COMPAT.maxTokensField, + requiresToolResultName: model.compat.requiresToolResultName ?? DEFAULT_COMPAT.requiresToolResultName, requiresAssistantAfterToolResult: - model.compat.requiresAssistantAfterToolResult ?? detected.requiresAssistantAfterToolResult, - requiresThinkingAsText: model.compat.requiresThinkingAsText ?? detected.requiresThinkingAsText, + model.compat.requiresAssistantAfterToolResult ?? DEFAULT_COMPAT.requiresAssistantAfterToolResult, + requiresThinkingAsText: model.compat.requiresThinkingAsText ?? DEFAULT_COMPAT.requiresThinkingAsText, requiresReasoningContentOnAssistantMessages: model.compat.requiresReasoningContentOnAssistantMessages ?? - detected.requiresReasoningContentOnAssistantMessages, - thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat, - openRouterRouting: model.compat.openRouterRouting ?? {}, - vercelGatewayRouting: model.compat.vercelGatewayRouting ?? detected.vercelGatewayRouting, - chatTemplateKwargs: model.compat.chatTemplateKwargs ?? detected.chatTemplateKwargs, - zaiToolStream: model.compat.zaiToolStream ?? detected.zaiToolStream, - supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode, - cacheControlFormat: model.compat.cacheControlFormat ?? detected.cacheControlFormat, - sendSessionAffinityHeaders: model.compat.sendSessionAffinityHeaders ?? detected.sendSessionAffinityHeaders, - supportsLongCacheRetention: model.compat.supportsLongCacheRetention ?? detected.supportsLongCacheRetention, + DEFAULT_COMPAT.requiresReasoningContentOnAssistantMessages, + thinkingFormat: model.compat.thinkingFormat ?? DEFAULT_COMPAT.thinkingFormat, + openRouterRouting: model.compat.openRouterRouting ?? DEFAULT_COMPAT.openRouterRouting, + vercelGatewayRouting: model.compat.vercelGatewayRouting ?? DEFAULT_COMPAT.vercelGatewayRouting, + chatTemplateKwargs: model.compat.chatTemplateKwargs ?? DEFAULT_COMPAT.chatTemplateKwargs, + zaiToolStream: model.compat.zaiToolStream ?? DEFAULT_COMPAT.zaiToolStream, + supportsStrictMode: model.compat.supportsStrictMode ?? DEFAULT_COMPAT.supportsStrictMode, + cacheControlFormat: model.compat.cacheControlFormat ?? DEFAULT_COMPAT.cacheControlFormat, + sendSessionAffinityHeaders: model.compat.sendSessionAffinityHeaders ?? DEFAULT_COMPAT.sendSessionAffinityHeaders, + supportsLongCacheRetention: model.compat.supportsLongCacheRetention ?? DEFAULT_COMPAT.supportsLongCacheRetention, }; } diff --git a/packages/ai/src/api/openai-responses.ts b/packages/ai/src/api/openai-responses.ts index 92756db3..40998eaa 100644 --- a/packages/ai/src/api/openai-responses.ts +++ b/packages/ai/src/api/openai-responses.ts @@ -9,6 +9,7 @@ import type { Model, OpenAIResponsesCompat, ProviderEnv, + ProviderHeaders, SimpleStreamOptions, StreamFunction, StreamOptions, @@ -17,7 +18,6 @@ import type { import { AssistantMessageEventStream } from "../utils/event-stream.ts"; import { headersToRecord } from "../utils/headers.ts"; import { getProviderEnvValue } from "../utils/provider-env.ts"; -import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.ts"; import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.ts"; import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts"; import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.ts"; @@ -25,6 +25,21 @@ import { buildBaseOptions } from "./simple-options.ts"; const OPENAI_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]); +function hasHeader(headers: ProviderHeaders | undefined, name: string): boolean { + if (!headers) return false; + const expected = name.toLowerCase(); + for (const [key, value] of Object.entries(headers)) { + if (key.toLowerCase() === expected && value !== null && value.trim().length > 0) return true; + } + return false; +} + +function getClientApiKey(provider: string, apiKey: string | undefined, headers: ProviderHeaders | undefined): string { + if (apiKey) return apiKey; + if (hasHeader(headers, "authorization") || hasHeader(headers, "cf-aig-authorization")) return "unused"; + throw new Error(`No API key for provider: ${provider}`); +} + /** * Resolve cache retention preference. * Defaults to "short" and uses PI_CACHE_RETENTION for backward compatibility. @@ -109,13 +124,10 @@ export const stream: StreamFunction<"openai-responses", OpenAIResponsesOptions> try { // Create OpenAI client - const apiKey = options?.apiKey; - if (!apiKey) { - throw new Error(`No API key for provider: ${model.provider}`); - } + const apiKey = getClientApiKey(model.provider, options?.apiKey, options?.headers); const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env); const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId; - const client = createClient(model, context, apiKey, options?.headers, cacheSessionId, options?.env); + const client = createClient(model, context, apiKey, options?.headers, cacheSessionId); let params = buildParams(model, context, options); const nextParams = await options?.onPayload?.(params, model); if (nextParams !== undefined) { @@ -166,12 +178,9 @@ export const streamSimple: StreamFunction<"openai-responses", SimpleStreamOption context: Context, options?: SimpleStreamOptions, ): AssistantMessageEventStream => { - const apiKey = options?.apiKey; - if (!apiKey) { - throw new Error(`No API key for provider: ${model.provider}`); - } + getClientApiKey(model.provider, options?.apiKey, options?.headers); - const base = buildBaseOptions(model, options, apiKey); + const base = buildBaseOptions(model, options, options?.apiKey); const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined; const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning; @@ -185,12 +194,11 @@ function createClient( model: Model<"openai-responses">, context: Context, apiKey: string, - optionsHeaders?: Record, + optionsHeaders?: ProviderHeaders, sessionId?: string, - env?: ProviderEnv, ) { const compat = getCompat(model); - const headers = { ...model.headers }; + const headers: ProviderHeaders = { ...model.headers }; if (model.provider === "github-copilot") { const hasImages = hasCopilotVisionInput(context.messages); const copilotHeaders = buildCopilotDynamicHeaders({ @@ -212,20 +220,11 @@ function createClient( Object.assign(headers, optionsHeaders); } - const defaultHeaders = - model.provider === "cloudflare-ai-gateway" - ? { - ...headers, - Authorization: headers.Authorization ?? null, - "cf-aig-authorization": `Bearer ${apiKey}`, - } - : headers; - return new OpenAI({ apiKey, - baseURL: isCloudflareProvider(model.provider) ? resolveCloudflareBaseUrl(model, env) : model.baseUrl, + baseURL: model.baseUrl, dangerouslyAllowBrowser: true, - defaultHeaders, + defaultHeaders: headers, }); } diff --git a/packages/ai/src/api/openrouter-images.ts b/packages/ai/src/api/openrouter-images.ts index c96be7c8..12117366 100644 --- a/packages/ai/src/api/openrouter-images.ts +++ b/packages/ai/src/api/openrouter-images.ts @@ -13,9 +13,10 @@ import type { ImagesFunction, ImagesModel, ImagesOptions, + ProviderHeaders, TextContent, } from "../types.ts"; -import { headersToRecord } from "../utils/headers.ts"; +import { headersToRecord, providerHeadersToRecord } from "../utils/headers.ts"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; interface OpenRouterGeneratedImage { @@ -106,16 +107,13 @@ export const generateImages: ImagesFunction<"openrouter-images", ImagesOptions> function createClient( model: ImagesModel<"openrouter-images">, apiKey: string, - optionsHeaders?: Record, + optionsHeaders?: ProviderHeaders, ): OpenAI { return new OpenAI({ apiKey, baseURL: model.baseUrl, dangerouslyAllowBrowser: true, - defaultHeaders: { - ...model.headers, - ...optionsHeaders, - }, + defaultHeaders: providerHeadersToRecord({ ...model.headers, ...optionsHeaders }), }); } diff --git a/packages/ai/src/auth/types.ts b/packages/ai/src/auth/types.ts index 6cb285cf..ffda2858 100644 --- a/packages/ai/src/auth/types.ts +++ b/packages/ai/src/auth/types.ts @@ -1,4 +1,4 @@ -import type { Api, ImagesApi, ImagesModel, Model, ProviderEnv } from "../types.ts"; +import type { Api, ImagesApi, ImagesModel, Model, ProviderEnv, ProviderHeaders } from "../types.ts"; import type { OAuthCredentials } from "../utils/oauth/types.ts"; /** @@ -7,7 +7,7 @@ import type { OAuthCredentials } from "../utils/oauth/types.ts"; */ export interface ModelAuth { apiKey?: string; - headers?: Record; + headers?: ProviderHeaders; baseUrl?: string; } diff --git a/packages/ai/src/compat.ts b/packages/ai/src/compat.ts index fff4746e..51b7e17f 100644 --- a/packages/ai/src/compat.ts +++ b/packages/ai/src/compat.ts @@ -19,7 +19,6 @@ export * from "./api/mistral-conversations.lazy.ts"; export * from "./api/openai-codex-responses.lazy.ts"; export * from "./api/openai-completions.lazy.ts"; export * from "./api/openai-responses.lazy.ts"; -export * from "./api-registry.ts"; export * from "./env-api-keys.ts"; export * from "./image-models.ts"; export * from "./images.ts"; @@ -36,11 +35,12 @@ import { mistralConversationsApi } from "./api/mistral-conversations.lazy.ts"; import { openAICodexResponsesApi } from "./api/openai-codex-responses.lazy.ts"; import { openAICompletionsApi } from "./api/openai-completions.lazy.ts"; import { openAIResponsesApi } from "./api/openai-responses.lazy.ts"; -import { clearApiProviders, getApiProvider, registerApiProvider } from "./api-registry.ts"; import { getEnvApiKey } from "./env-api-keys.ts"; -import { getBuiltinModel, getBuiltinModels, getBuiltinProviders } from "./providers/all.ts"; +import { builtinModels, getBuiltinModel, getBuiltinModels, getBuiltinProviders } from "./providers/all.ts"; +import { createFauxCore, type FauxProviderRegistration, type RegisterFauxProviderOptions } from "./providers/faux.ts"; import type { Api, + ApiStreamOptions, AssistantMessage, AssistantMessageEventStream, Context, @@ -48,6 +48,7 @@ import type { ProviderStreamOptions, ProviderStreams, SimpleStreamOptions, + StreamFunction, StreamOptions, } from "./types.ts"; @@ -60,6 +61,113 @@ export const getModels = getBuiltinModels; /** @deprecated Static catalog read. Use `getBuiltinProviders` from "@earendil-works/pi-ai/providers/all" or `Models.getProviders()`. */ export const getProviders = getBuiltinProviders; +export type ApiStreamFunction = ( + model: Model, + context: Context, + options?: StreamOptions, +) => AssistantMessageEventStream; + +export type ApiStreamSimpleFunction = ( + model: Model, + context: Context, + options?: SimpleStreamOptions, +) => AssistantMessageEventStream; + +export interface ApiProvider { + api: TApi; + stream: StreamFunction; + streamSimple: StreamFunction; +} + +interface ApiProviderInternal { + api: Api; + stream: ApiStreamFunction; + streamSimple: ApiStreamSimpleFunction; +} + +type RegisteredApiProvider = { + provider: ApiProviderInternal; + sourceId?: string; +}; + +const apiProviderRegistry = new Map(); + +function wrapStream( + api: TApi, + stream: StreamFunction, +): ApiStreamFunction { + return (model, context, options) => { + if (model.api !== api) { + throw new Error(`Mismatched api: ${model.api} expected ${api}`); + } + return stream(model as Model, context, options as TOptions); + }; +} + +function wrapStreamSimple( + api: TApi, + streamSimple: StreamFunction, +): ApiStreamSimpleFunction { + return (model, context, options) => { + if (model.api !== api) { + throw new Error(`Mismatched api: ${model.api} expected ${api}`); + } + return streamSimple(model as Model, context, options); + }; +} + +export function registerApiProvider( + provider: ApiProvider, + sourceId?: string, +): void { + apiProviderRegistry.set(provider.api, { + provider: { + api: provider.api, + stream: wrapStream(provider.api, provider.stream), + streamSimple: wrapStreamSimple(provider.api, provider.streamSimple), + }, + sourceId, + }); +} + +export function getApiProvider(api: Api): ApiProviderInternal | undefined { + return apiProviderRegistry.get(api)?.provider; +} + +export function getApiProviders(): ApiProviderInternal[] { + return Array.from(apiProviderRegistry.values(), (entry) => entry.provider); +} + +export function unregisterApiProviders(sourceId: string): void { + for (const [api, entry] of apiProviderRegistry.entries()) { + if (entry.sourceId === sourceId) { + apiProviderRegistry.delete(api); + } + } +} + +function clearApiProviders(): void { + apiProviderRegistry.clear(); +} + +export function registerFauxProvider(options: RegisterFauxProviderOptions = {}): FauxProviderRegistration { + const core = createFauxCore(options); + const sourceId = `faux-provider-${Math.random().toString(36).slice(2, 10)}`; + 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); + }, + }; +} + const BUILTIN_APIS: [Api, ProviderStreams][] = [ ["anthropic-messages", anthropicMessagesApi()], ["openai-completions", openAICompletionsApi()], @@ -72,6 +180,8 @@ const BUILTIN_APIS: [Api, ProviderStreams][] = [ ["bedrock-converse-stream", bedrockConverseStreamApi()], ]; +const builtinApiProviderInstances = new Map>(); + /** * Registers the builtin API implementations into the api-registry without * clobbering existing entries: compat may load after a test or extension has @@ -79,18 +189,23 @@ const BUILTIN_APIS: [Api, ProviderStreams][] = [ */ export function registerBuiltInApiProviders(): void { for (const [api, streams] of BUILTIN_APIS) { - if (getApiProvider(api)) continue; - registerApiProvider({ api, stream: streams.stream, streamSimple: streams.streamSimple }); + if (!getApiProvider(api)) { + registerApiProvider({ api, stream: streams.stream, streamSimple: streams.streamSimple }); + } + builtinApiProviderInstances.set(api, getApiProvider(api)); } } export function resetApiProviders(): void { clearApiProviders(); + builtinApiProviderInstances.clear(); registerBuiltInApiProviders(); } registerBuiltInApiProviders(); +const compatModels = builtinModels(); + function hasExplicitApiKey(apiKey: string | undefined): apiKey is string { return typeof apiKey === "string" && apiKey.trim().length > 0; } @@ -105,6 +220,11 @@ function withEnvApiKey( return { ...options, apiKey } as TOptions; } +function shouldUseBuiltinModels(model: Model): boolean { + const builtin = compatModels.getModel(model.provider, model.id); + return builtin?.api === model.api && getApiProvider(model.api) === builtinApiProviderInstances.get(model.api); +} + function resolveApiProvider(api: Api) { const provider = getApiProvider(api); if (!provider) { @@ -118,6 +238,9 @@ export function stream( context: Context, options?: ProviderStreamOptions, ): AssistantMessageEventStream { + if (shouldUseBuiltinModels(model)) { + return compatModels.stream(model, context, options as ApiStreamOptions | undefined); + } const provider = resolveApiProvider(model.api); return provider.stream(model, context, withEnvApiKey(model, options) as StreamOptions); } @@ -136,6 +259,9 @@ export function streamSimple( context: Context, options?: SimpleStreamOptions, ): AssistantMessageEventStream { + if (shouldUseBuiltinModels(model)) { + return compatModels.streamSimple(model, context, options); + } const provider = resolveApiProvider(model.api); return provider.streamSimple(model, context, withEnvApiKey(model, options)); } diff --git a/packages/ai/src/models.ts b/packages/ai/src/models.ts index de17f8ec..669b1525 100644 --- a/packages/ai/src/models.ts +++ b/packages/ai/src/models.ts @@ -11,6 +11,7 @@ import type { Context, Model, ModelThinkingLevel, + ProviderHeaders, ProviderStreams, SimpleStreamOptions, StreamOptions, @@ -33,7 +34,7 @@ export interface Provider { readonly name: string; readonly baseUrl?: string; - readonly headers?: Record; + readonly headers?: ProviderHeaders; /** * Required: at least one of `apiKey`/`oauth`. Every provider has auth @@ -287,7 +288,7 @@ export interface CreateProviderOptions { /** Display name. Default: `id`. */ name?: string; baseUrl?: string; - headers?: Record; + headers?: ProviderHeaders; /** Required — every provider has auth semantics, even ambient/keyless ones. */ auth: ProviderAuth; /** Initial model list (empty for purely dynamic providers). */ diff --git a/packages/ai/src/providers/ant-ling.models.ts b/packages/ai/src/providers/ant-ling.models.ts index 6acdebef..10c656fe 100644 --- a/packages/ai/src/providers/ant-ling.models.ts +++ b/packages/ai/src/providers/ant-ling.models.ts @@ -10,7 +10,7 @@ export const ANT_LING_MODELS = { api: "openai-completions", provider: "ant-ling", baseUrl: "https://api.ant-ling.com/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"ant-ling","supportsLongCacheRetention":false}, reasoning: false, input: ["text"], cost: { @@ -28,7 +28,7 @@ export const ANT_LING_MODELS = { api: "openai-completions", provider: "ant-ling", baseUrl: "https://api.ant-ling.com/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"ant-ling","supportsLongCacheRetention":false}, reasoning: false, input: ["text"], cost: { @@ -46,7 +46,7 @@ export const ANT_LING_MODELS = { api: "openai-completions", provider: "ant-ling", baseUrl: "https://api.ant-ling.com/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false,"thinkingFormat":"ant-ling"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"ant-ling","supportsLongCacheRetention":false}, reasoning: true, thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh"}, input: ["text"], diff --git a/packages/ai/src/providers/cerebras.models.ts b/packages/ai/src/providers/cerebras.models.ts index e93c8bc3..53c4151c 100644 --- a/packages/ai/src/providers/cerebras.models.ts +++ b/packages/ai/src/providers/cerebras.models.ts @@ -10,6 +10,7 @@ export const CEREBRAS_MODELS = { api: "openai-completions", provider: "cerebras", baseUrl: "https://api.cerebras.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false}, reasoning: true, input: ["text"], cost: { @@ -27,6 +28,7 @@ export const CEREBRAS_MODELS = { api: "openai-completions", provider: "cerebras", baseUrl: "https://api.cerebras.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false}, reasoning: true, input: ["text"], cost: { diff --git a/packages/ai/src/providers/cloudflare-ai-gateway.models.ts b/packages/ai/src/providers/cloudflare-ai-gateway.models.ts index c54752be..d8a562fe 100644 --- a/packages/ai/src/providers/cloudflare-ai-gateway.models.ts +++ b/packages/ai/src/providers/cloudflare-ai-gateway.models.ts @@ -10,6 +10,7 @@ export const CLOUDFLARE_AI_GATEWAY_MODELS = { api: "anthropic-messages", provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, reasoning: false, input: ["text", "image"], cost: { @@ -27,6 +28,7 @@ export const CLOUDFLARE_AI_GATEWAY_MODELS = { api: "anthropic-messages", provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, reasoning: false, input: ["text", "image"], cost: { @@ -44,6 +46,7 @@ export const CLOUDFLARE_AI_GATEWAY_MODELS = { api: "anthropic-messages", provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, reasoning: false, input: ["text", "image"], cost: { @@ -61,6 +64,7 @@ export const CLOUDFLARE_AI_GATEWAY_MODELS = { api: "anthropic-messages", provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, reasoning: false, input: ["text", "image"], cost: { @@ -78,6 +82,7 @@ export const CLOUDFLARE_AI_GATEWAY_MODELS = { api: "anthropic-messages", provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, reasoning: false, input: ["text", "image"], cost: { @@ -95,6 +100,7 @@ export const CLOUDFLARE_AI_GATEWAY_MODELS = { api: "anthropic-messages", provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, reasoning: false, input: ["text", "image"], cost: { @@ -112,7 +118,7 @@ export const CLOUDFLARE_AI_GATEWAY_MODELS = { api: "anthropic-messages", provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - compat: {"forceAdaptiveThinking":true}, + compat: {"sendSessionAffinityHeaders":true,"forceAdaptiveThinking":true}, reasoning: true, thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], @@ -131,6 +137,7 @@ export const CLOUDFLARE_AI_GATEWAY_MODELS = { api: "anthropic-messages", provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, reasoning: true, input: ["text", "image"], cost: { @@ -148,6 +155,7 @@ export const CLOUDFLARE_AI_GATEWAY_MODELS = { api: "anthropic-messages", provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, reasoning: true, input: ["text", "image"], cost: { @@ -165,6 +173,7 @@ export const CLOUDFLARE_AI_GATEWAY_MODELS = { api: "anthropic-messages", provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, reasoning: true, input: ["text", "image"], cost: { @@ -182,6 +191,7 @@ export const CLOUDFLARE_AI_GATEWAY_MODELS = { api: "anthropic-messages", provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, reasoning: true, input: ["text", "image"], cost: { @@ -199,7 +209,7 @@ export const CLOUDFLARE_AI_GATEWAY_MODELS = { api: "anthropic-messages", provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - compat: {"forceAdaptiveThinking":true}, + compat: {"sendSessionAffinityHeaders":true,"forceAdaptiveThinking":true}, reasoning: true, thinkingLevelMap: {"xhigh":"max"}, input: ["text", "image"], @@ -218,7 +228,7 @@ export const CLOUDFLARE_AI_GATEWAY_MODELS = { api: "anthropic-messages", provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, + compat: {"sendSessionAffinityHeaders":true,"forceAdaptiveThinking":true,"supportsTemperature":false}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -237,7 +247,7 @@ export const CLOUDFLARE_AI_GATEWAY_MODELS = { api: "anthropic-messages", provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, + compat: {"sendSessionAffinityHeaders":true,"forceAdaptiveThinking":true,"supportsTemperature":false}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -256,6 +266,7 @@ export const CLOUDFLARE_AI_GATEWAY_MODELS = { api: "anthropic-messages", provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, reasoning: true, input: ["text", "image"], cost: { @@ -273,6 +284,7 @@ export const CLOUDFLARE_AI_GATEWAY_MODELS = { api: "anthropic-messages", provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, reasoning: true, input: ["text", "image"], cost: { @@ -290,7 +302,7 @@ export const CLOUDFLARE_AI_GATEWAY_MODELS = { api: "anthropic-messages", provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - compat: {"forceAdaptiveThinking":true}, + compat: {"sendSessionAffinityHeaders":true,"forceAdaptiveThinking":true}, reasoning: true, input: ["text", "image"], cost: { @@ -587,7 +599,7 @@ export const CLOUDFLARE_AI_GATEWAY_MODELS = { api: "openai-completions", provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - compat: {"sendSessionAffinityHeaders":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, reasoning: true, input: ["text", "image"], cost: { @@ -605,7 +617,7 @@ export const CLOUDFLARE_AI_GATEWAY_MODELS = { api: "openai-completions", provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - compat: {"sendSessionAffinityHeaders":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, reasoning: true, input: ["text", "image"], cost: { @@ -623,7 +635,7 @@ export const CLOUDFLARE_AI_GATEWAY_MODELS = { api: "openai-completions", provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - compat: {"sendSessionAffinityHeaders":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, reasoning: true, input: ["text"], cost: { @@ -641,7 +653,7 @@ export const CLOUDFLARE_AI_GATEWAY_MODELS = { api: "openai-completions", provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - compat: {"sendSessionAffinityHeaders":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, reasoning: true, input: ["text"], cost: { diff --git a/packages/ai/src/providers/cloudflare-ai-gateway.ts b/packages/ai/src/providers/cloudflare-ai-gateway.ts index e8538f7c..9f6ff5f6 100644 --- a/packages/ai/src/providers/cloudflare-ai-gateway.ts +++ b/packages/ai/src/providers/cloudflare-ai-gateway.ts @@ -1,9 +1,9 @@ import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts"; import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; import { openAIResponsesApi } from "../api/openai-responses.lazy.ts"; -import { envApiKeyAuth } from "../auth/helpers.ts"; import { createProvider, type Provider } from "../models.ts"; import { CLOUDFLARE_AI_GATEWAY_MODELS } from "./cloudflare-ai-gateway.models.ts"; +import { cloudflareAIGatewayAuth } from "./cloudflare-auth.ts"; export function cloudflareAIGatewayProvider(): Provider< "anthropic-messages" | "openai-completions" | "openai-responses" @@ -11,7 +11,7 @@ export function cloudflareAIGatewayProvider(): Provider< return createProvider({ id: "cloudflare-ai-gateway", name: "Cloudflare AI Gateway", - auth: { apiKey: envApiKeyAuth("Cloudflare API key", ["CLOUDFLARE_API_KEY"]) }, + auth: { apiKey: cloudflareAIGatewayAuth() }, models: Object.values(CLOUDFLARE_AI_GATEWAY_MODELS), api: { "anthropic-messages": anthropicMessagesApi(), diff --git a/packages/ai/src/providers/cloudflare-auth.ts b/packages/ai/src/providers/cloudflare-auth.ts new file mode 100644 index 00000000..7bf909cd --- /dev/null +++ b/packages/ai/src/providers/cloudflare-auth.ts @@ -0,0 +1,108 @@ +import type { ApiKeyAuth, ApiKeyCredential, AuthContext } from "../auth/types.ts"; +import type { Api, ImagesApi, ImagesModel, Model, ProviderEnv } from "../types.ts"; + +const CLOUDFLARE_API_KEY = "CLOUDFLARE_API_KEY"; +const CLOUDFLARE_ACCOUNT_ID = "CLOUDFLARE_ACCOUNT_ID"; +const CLOUDFLARE_GATEWAY_ID = "CLOUDFLARE_GATEWAY_ID"; + +type CloudflareAuthKind = "workers-ai" | "ai-gateway"; + +async function resolveValue(input: { + name: string; + ctx: AuthContext; + credential: ApiKeyCredential | undefined; +}): Promise { + if (input.credential) { + if (input.name === CLOUDFLARE_API_KEY) return input.credential.key; + return input.credential.metadata?.[input.name]; + } + return input.ctx.env(input.name); +} + +function resolveCloudflareBaseUrl( + model: Model | ImagesModel, + accountId: string, + gatewayId: string | undefined, +): string { + return model.baseUrl + .replaceAll(`{${CLOUDFLARE_ACCOUNT_ID}}`, accountId) + .replaceAll(`{${CLOUDFLARE_GATEWAY_ID}}`, gatewayId ?? ""); +} + +async function resolveCloudflareEnv(input: { + kind: CloudflareAuthKind; + model: Model | ImagesModel; + ctx: AuthContext; + credential: ApiKeyCredential | undefined; +}): Promise<{ apiKey: string; env: ProviderEnv; baseUrl: string; source: string } | undefined> { + const apiKey = await resolveValue({ name: CLOUDFLARE_API_KEY, ctx: input.ctx, credential: input.credential }); + const accountId = await resolveValue({ name: CLOUDFLARE_ACCOUNT_ID, ctx: input.ctx, credential: input.credential }); + const gatewayId = + input.kind === "ai-gateway" + ? await resolveValue({ name: CLOUDFLARE_GATEWAY_ID, ctx: input.ctx, credential: input.credential }) + : undefined; + + if (!apiKey || !accountId || (input.kind === "ai-gateway" && !gatewayId)) return undefined; + + return { + apiKey, + env: { + CLOUDFLARE_ACCOUNT_ID: accountId, + ...(gatewayId ? { CLOUDFLARE_GATEWAY_ID: gatewayId } : {}), + }, + baseUrl: resolveCloudflareBaseUrl(input.model, accountId, gatewayId), + source: input.credential ? "stored credential" : CLOUDFLARE_API_KEY, + }; +} + +export function cloudflareWorkersAIAuth(): ApiKeyAuth { + return { + name: "Cloudflare API key", + login: async (callbacks) => { + const key = await callbacks.prompt({ type: "secret", message: "Enter Cloudflare API key" }); + const accountId = await callbacks.prompt({ type: "text", message: "Enter Cloudflare account ID" }); + return { type: "api-key", key, metadata: { CLOUDFLARE_ACCOUNT_ID: accountId } }; + }, + resolve: async ({ model, ctx, credential }) => { + const resolved = await resolveCloudflareEnv({ kind: "workers-ai", model, ctx, credential }); + if (!resolved) return undefined; + return { + auth: { apiKey: resolved.apiKey, baseUrl: resolved.baseUrl }, + env: resolved.env, + source: resolved.source, + }; + }, + }; +} + +export function cloudflareAIGatewayAuth(): ApiKeyAuth { + return { + name: "Cloudflare API key", + login: async (callbacks) => { + const key = await callbacks.prompt({ type: "secret", message: "Enter Cloudflare API key" }); + const accountId = await callbacks.prompt({ type: "text", message: "Enter Cloudflare account ID" }); + const gatewayId = await callbacks.prompt({ type: "text", message: "Enter Cloudflare AI Gateway ID" }); + return { + type: "api-key", + key, + metadata: { CLOUDFLARE_ACCOUNT_ID: accountId, CLOUDFLARE_GATEWAY_ID: gatewayId }, + }; + }, + resolve: async ({ model, ctx, credential }) => { + const resolved = await resolveCloudflareEnv({ kind: "ai-gateway", model, ctx, credential }); + if (!resolved) return undefined; + return { + auth: { + headers: { + "cf-aig-authorization": `Bearer ${resolved.apiKey}`, + Authorization: null, + "x-api-key": null, + }, + baseUrl: resolved.baseUrl, + }, + env: resolved.env, + source: resolved.source, + }; + }, + }; +} diff --git a/packages/ai/src/providers/cloudflare-workers-ai.models.ts b/packages/ai/src/providers/cloudflare-workers-ai.models.ts index e3c2ccc6..3adfee60 100644 --- a/packages/ai/src/providers/cloudflare-workers-ai.models.ts +++ b/packages/ai/src/providers/cloudflare-workers-ai.models.ts @@ -10,7 +10,7 @@ export const CLOUDFLARE_WORKERS_AI_MODELS = { api: "openai-completions", provider: "cloudflare-workers-ai", baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, reasoning: true, input: ["text", "image"], cost: { @@ -28,7 +28,7 @@ export const CLOUDFLARE_WORKERS_AI_MODELS = { api: "openai-completions", provider: "cloudflare-workers-ai", baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, reasoning: false, input: ["text"], cost: { @@ -46,7 +46,7 @@ export const CLOUDFLARE_WORKERS_AI_MODELS = { api: "openai-completions", provider: "cloudflare-workers-ai", baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, reasoning: false, input: ["text"], cost: { @@ -64,7 +64,7 @@ export const CLOUDFLARE_WORKERS_AI_MODELS = { api: "openai-completions", provider: "cloudflare-workers-ai", baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, reasoning: false, input: ["text", "image"], cost: { @@ -82,7 +82,7 @@ export const CLOUDFLARE_WORKERS_AI_MODELS = { api: "openai-completions", provider: "cloudflare-workers-ai", baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, reasoning: false, input: ["text"], cost: { @@ -100,7 +100,7 @@ export const CLOUDFLARE_WORKERS_AI_MODELS = { api: "openai-completions", provider: "cloudflare-workers-ai", baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, reasoning: true, input: ["text", "image"], cost: { @@ -118,7 +118,7 @@ export const CLOUDFLARE_WORKERS_AI_MODELS = { api: "openai-completions", provider: "cloudflare-workers-ai", baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, reasoning: true, input: ["text", "image"], cost: { @@ -136,7 +136,7 @@ export const CLOUDFLARE_WORKERS_AI_MODELS = { api: "openai-completions", provider: "cloudflare-workers-ai", baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, reasoning: true, input: ["text"], cost: { @@ -154,7 +154,7 @@ export const CLOUDFLARE_WORKERS_AI_MODELS = { api: "openai-completions", provider: "cloudflare-workers-ai", baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, reasoning: true, input: ["text"], cost: { @@ -172,7 +172,7 @@ export const CLOUDFLARE_WORKERS_AI_MODELS = { api: "openai-completions", provider: "cloudflare-workers-ai", baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, reasoning: true, input: ["text"], cost: { @@ -190,7 +190,7 @@ export const CLOUDFLARE_WORKERS_AI_MODELS = { api: "openai-completions", provider: "cloudflare-workers-ai", baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, reasoning: true, input: ["text"], cost: { @@ -208,7 +208,7 @@ export const CLOUDFLARE_WORKERS_AI_MODELS = { api: "openai-completions", provider: "cloudflare-workers-ai", baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, reasoning: true, input: ["text"], cost: { @@ -226,7 +226,7 @@ export const CLOUDFLARE_WORKERS_AI_MODELS = { api: "openai-completions", provider: "cloudflare-workers-ai", baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, reasoning: true, input: ["text"], cost: { diff --git a/packages/ai/src/providers/cloudflare-workers-ai.ts b/packages/ai/src/providers/cloudflare-workers-ai.ts index 81d18e34..9e376a5c 100644 --- a/packages/ai/src/providers/cloudflare-workers-ai.ts +++ b/packages/ai/src/providers/cloudflare-workers-ai.ts @@ -1,13 +1,13 @@ import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; -import { envApiKeyAuth } from "../auth/helpers.ts"; import { createProvider, type Provider } from "../models.ts"; +import { cloudflareWorkersAIAuth } from "./cloudflare-auth.ts"; import { CLOUDFLARE_WORKERS_AI_MODELS } from "./cloudflare-workers-ai.models.ts"; export function cloudflareWorkersAIProvider(): Provider<"openai-completions"> { return createProvider({ id: "cloudflare-workers-ai", name: "Cloudflare Workers AI", - auth: { apiKey: envApiKeyAuth("Cloudflare API key", ["CLOUDFLARE_API_KEY"]) }, + auth: { apiKey: cloudflareWorkersAIAuth() }, models: Object.values(CLOUDFLARE_WORKERS_AI_MODELS), api: openAICompletionsApi(), }); diff --git a/packages/ai/src/providers/deepseek.models.ts b/packages/ai/src/providers/deepseek.models.ts index 7d41a0a0..b9bcd95f 100644 --- a/packages/ai/src/providers/deepseek.models.ts +++ b/packages/ai/src/providers/deepseek.models.ts @@ -10,7 +10,7 @@ export const DEEPSEEK_MODELS = { api: "openai-completions", provider: "deepseek", baseUrl: "https://api.deepseek.com", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], @@ -29,7 +29,7 @@ export const DEEPSEEK_MODELS = { api: "openai-completions", provider: "deepseek", baseUrl: "https://api.deepseek.com", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], diff --git a/packages/ai/src/providers/faux.ts b/packages/ai/src/providers/faux.ts index 6fbdbc54..4a26f1ad 100644 --- a/packages/ai/src/providers/faux.ts +++ b/packages/ai/src/providers/faux.ts @@ -1,4 +1,3 @@ -import { registerApiProvider, unregisterApiProviders } from "../api-registry.ts"; import { createProvider, type Provider } from "../models.ts"; import type { AssistantMessage, @@ -401,7 +400,7 @@ async function streamWithDeltas( stream.end(message); } -function createFauxCore(options: RegisterFauxProviderOptions) { +export function createFauxCore(options: RegisterFauxProviderOptions) { const api = options.api ?? randomId(DEFAULT_API); const provider = options.provider ?? DEFAULT_PROVIDER; const minTokenSize = Math.max( @@ -508,25 +507,6 @@ function createFauxCore(options: RegisterFauxProviderOptions) { }; } -/** 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: * diff --git a/packages/ai/src/providers/huggingface.models.ts b/packages/ai/src/providers/huggingface.models.ts index 282b8d82..ffdcdaf9 100644 --- a/packages/ai/src/providers/huggingface.models.ts +++ b/packages/ai/src/providers/huggingface.models.ts @@ -4,6 +4,24 @@ import type { Model } from "../types.ts"; export const HUGGINGFACE_MODELS = { + "MiniMaxAI/MiniMax-M2": { + id: "MiniMaxAI/MiniMax-M2", + name: "MiniMax-M2", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 128000, + } satisfies Model<"openai-completions">, "MiniMaxAI/MiniMax-M2.1": { id: "MiniMaxAI/MiniMax-M2.1", name: "MiniMax-M2.1", @@ -58,6 +76,42 @@ export const HUGGINGFACE_MODELS = { contextWindow: 204800, maxTokens: 131072, } satisfies Model<"openai-completions">, + "MiniMaxAI/MiniMax-M3": { + id: "MiniMaxAI/MiniMax-M3", + name: "MiniMax-M3", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 524288, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3-235B-A22B": { + id: "Qwen/Qwen3-235B-A22B", + name: "Qwen3 235B-A22B", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.2, + output: 0.8, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 40960, + maxTokens: 16384, + } satisfies Model<"openai-completions">, "Qwen/Qwen3-235B-A22B-Thinking-2507": { id: "Qwen/Qwen3-235B-A22B-Thinking-2507", name: "Qwen3-235B-A22B-Thinking-2507", @@ -76,6 +130,42 @@ export const HUGGINGFACE_MODELS = { contextWindow: 262144, maxTokens: 131072, } satisfies Model<"openai-completions">, + "Qwen/Qwen3-32B": { + id: "Qwen/Qwen3-32B", + name: "Qwen3 32B", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.29, + output: 0.59, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3-Coder-30B-A3B-Instruct": { + id: "Qwen/Qwen3-Coder-30B-A3B-Instruct", + name: "Qwen3-Coder 30B-A3B Instruct", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0.07, + output: 0.26, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, "Qwen/Qwen3-Coder-480B-A35B-Instruct": { id: "Qwen/Qwen3-Coder-480B-A35B-Instruct", name: "Qwen3-Coder-480B-A35B-Instruct", @@ -148,6 +238,60 @@ export const HUGGINGFACE_MODELS = { contextWindow: 262144, maxTokens: 131072, } satisfies Model<"openai-completions">, + "Qwen/Qwen3.5-122B-A10B": { + id: "Qwen/Qwen3.5-122B-A10B", + name: "Qwen3.5 122B-A10B", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 3.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3.5-27B": { + id: "Qwen/Qwen3.5-27B", + name: "Qwen3.5 27B", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 2.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3.5-35B-A3B": { + id: "Qwen/Qwen3.5-35B-A3B", + name: "Qwen3.5 35B-A3B", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, "Qwen/Qwen3.5-397B-A17B": { id: "Qwen/Qwen3.5-397B-A17B", name: "Qwen3.5-397B-A17B", @@ -166,6 +310,42 @@ export const HUGGINGFACE_MODELS = { contextWindow: 262144, maxTokens: 32768, } satisfies Model<"openai-completions">, + "Qwen/Qwen3.5-9B": { + id: "Qwen/Qwen3.5-9B", + name: "Qwen3.5 9B", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.17, + output: 0.25, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3.6-35B-A3B": { + id: "Qwen/Qwen3.6-35B-A3B", + name: "Qwen3.6 35B-A3B", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.95, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, "XiaomiMiMo/MiMo-V2-Flash": { id: "XiaomiMiMo/MiMo-V2-Flash", name: "MiMo-V2-Flash", @@ -184,6 +364,24 @@ export const HUGGINGFACE_MODELS = { contextWindow: 262144, maxTokens: 4096, } satisfies Model<"openai-completions">, + "deepseek-ai/DeepSeek-R1": { + id: "deepseek-ai/DeepSeek-R1", + name: "DeepSeek-R1", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.7, + output: 2.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 64000, + maxTokens: 32768, + } satisfies Model<"openai-completions">, "deepseek-ai/DeepSeek-R1-0528": { id: "deepseek-ai/DeepSeek-R1-0528", name: "DeepSeek-R1-0528", @@ -220,6 +418,24 @@ export const HUGGINGFACE_MODELS = { contextWindow: 163840, maxTokens: 65536, } satisfies Model<"openai-completions">, + "deepseek-ai/DeepSeek-V4-Flash": { + id: "deepseek-ai/DeepSeek-V4-Flash", + name: "DeepSeek V4 Flash", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.14, + output: 0.28, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 384000, + } satisfies Model<"openai-completions">, "deepseek-ai/DeepSeek-V4-Pro": { id: "deepseek-ai/DeepSeek-V4-Pro", name: "DeepSeek V4 Pro", @@ -238,6 +454,60 @@ export const HUGGINGFACE_MODELS = { contextWindow: 1048576, maxTokens: 393216, } satisfies Model<"openai-completions">, + "google/gemma-4-26B-A4B-it": { + id: "google/gemma-4-26B-A4B-it", + name: "Gemma 4 26B A4B IT", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.13, + output: 0.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "google/gemma-4-31B-it": { + id: "google/gemma-4-31B-it", + name: "Gemma 4 31B IT", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.14, + output: 0.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "meta-llama/Llama-3.3-70B-Instruct": { + id: "meta-llama/Llama-3.3-70B-Instruct", + name: "Llama-3.3-70B-Instruct", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0.59, + output: 0.79, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 4096, + } satisfies Model<"openai-completions">, "moonshotai/Kimi-K2-Instruct": { id: "moonshotai/Kimi-K2-Instruct", name: "Kimi-K2-Instruct", @@ -328,6 +598,114 @@ export const HUGGINGFACE_MODELS = { contextWindow: 262144, maxTokens: 262144, } satisfies Model<"openai-completions">, + "moonshotai/Kimi-K2.7-Code": { + id: "moonshotai/Kimi-K2.7-Code", + name: "Kimi K2.7 Code", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "stepfun-ai/Step-3.5-Flash": { + id: "stepfun-ai/Step-3.5-Flash", + name: "Step 3.5 Flash", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.1, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 256000, + } satisfies Model<"openai-completions">, + "zai-org/GLM-4.5": { + id: "zai-org/GLM-4.5", + name: "GLM-4.5", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 98304, + } satisfies Model<"openai-completions">, + "zai-org/GLM-4.5-Air": { + id: "zai-org/GLM-4.5-Air", + name: "GLM-4.5-Air", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.13, + output: 0.85, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 98304, + } satisfies Model<"openai-completions">, + "zai-org/GLM-4.5V": { + id: "zai-org/GLM-4.5V", + name: "GLM-4.5V", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 1.8, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 65536, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "zai-org/GLM-4.6": { + id: "zai-org/GLM-4.6", + name: "GLM-4.6", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.55, + output: 2.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, "zai-org/GLM-4.7": { id: "zai-org/GLM-4.7", name: "GLM-4.7", @@ -400,4 +778,22 @@ export const HUGGINGFACE_MODELS = { contextWindow: 202752, maxTokens: 131072, } satisfies Model<"openai-completions">, + "zai-org/GLM-5.2": { + id: "zai-org/GLM-5.2", + name: "GLM-5.2", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, } as const; diff --git a/packages/ai/src/providers/opencode-go.models.ts b/packages/ai/src/providers/opencode-go.models.ts index aac18d80..6cf15918 100644 --- a/packages/ai/src/providers/opencode-go.models.ts +++ b/packages/ai/src/providers/opencode-go.models.ts @@ -10,7 +10,7 @@ export const OPENCODE_GO_MODELS = { api: "openai-completions", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], @@ -29,7 +29,7 @@ export const OPENCODE_GO_MODELS = { api: "openai-completions", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], @@ -48,7 +48,7 @@ export const OPENCODE_GO_MODELS = { api: "openai-completions", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, reasoning: true, input: ["text"], cost: { @@ -66,7 +66,7 @@ export const OPENCODE_GO_MODELS = { api: "openai-completions", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, reasoning: true, thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], @@ -85,7 +85,7 @@ export const OPENCODE_GO_MODELS = { api: "openai-completions", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, input: ["text", "image"], @@ -104,7 +104,7 @@ export const OPENCODE_GO_MODELS = { api: "openai-completions", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, reasoning: true, input: ["text", "image"], cost: { @@ -122,7 +122,7 @@ export const OPENCODE_GO_MODELS = { api: "openai-completions", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, reasoning: true, input: ["text", "image"], cost: { @@ -140,7 +140,7 @@ export const OPENCODE_GO_MODELS = { api: "openai-completions", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, reasoning: true, input: ["text"], cost: { @@ -158,7 +158,7 @@ export const OPENCODE_GO_MODELS = { api: "openai-completions", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, reasoning: true, input: ["text"], cost: { @@ -193,7 +193,7 @@ export const OPENCODE_GO_MODELS = { api: "openai-completions", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"thinkingFormat":"qwen","maxTokensField":"max_tokens"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"thinkingFormat":"qwen","maxTokensField":"max_tokens"}, reasoning: true, input: ["text", "image"], cost: { diff --git a/packages/ai/src/providers/opencode.models.ts b/packages/ai/src/providers/opencode.models.ts index 1b050cf8..3da37f41 100644 --- a/packages/ai/src/providers/opencode.models.ts +++ b/packages/ai/src/providers/opencode.models.ts @@ -10,7 +10,7 @@ export const OPENCODE_MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, reasoning: true, input: ["text"], cost: { @@ -188,7 +188,7 @@ export const OPENCODE_MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false,"requiresReasoningContentOnAssistantMessages":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false,"requiresReasoningContentOnAssistantMessages":true}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], @@ -207,7 +207,7 @@ export const OPENCODE_MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], @@ -226,7 +226,7 @@ export const OPENCODE_MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false,"requiresReasoningContentOnAssistantMessages":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false,"requiresReasoningContentOnAssistantMessages":true}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], @@ -299,7 +299,7 @@ export const OPENCODE_MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, reasoning: true, input: ["text"], cost: { @@ -317,7 +317,7 @@ export const OPENCODE_MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, reasoning: true, input: ["text"], cost: { @@ -329,6 +329,24 @@ export const OPENCODE_MODELS = { contextWindow: 204800, maxTokens: 131072, } satisfies Model<"openai-completions">, + "glm-5.2": { + id: "glm-5.2", + name: "GLM-5.2", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0.26, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, "gpt-5": { id: "gpt-5", name: "GPT-5", @@ -623,7 +641,7 @@ export const OPENCODE_MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"supportsReasoningEffort":false,"maxTokensField":"max_tokens"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens"}, reasoning: true, thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null}, input: ["text", "image"], @@ -642,7 +660,7 @@ export const OPENCODE_MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, reasoning: true, input: ["text", "image"], cost: { @@ -660,7 +678,7 @@ export const OPENCODE_MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, reasoning: true, input: ["text", "image"], cost: { @@ -678,7 +696,7 @@ export const OPENCODE_MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, reasoning: true, input: ["text", "image"], cost: { @@ -696,7 +714,7 @@ export const OPENCODE_MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, reasoning: true, input: ["text"], cost: { @@ -714,7 +732,7 @@ export const OPENCODE_MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, reasoning: true, input: ["text"], cost: { @@ -732,7 +750,7 @@ export const OPENCODE_MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, reasoning: true, input: ["text"], cost: { @@ -750,7 +768,7 @@ export const OPENCODE_MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, reasoning: true, input: ["text"], cost: { diff --git a/packages/ai/src/providers/openrouter.models.ts b/packages/ai/src/providers/openrouter.models.ts index 02a5d3b9..15727161 100644 --- a/packages/ai/src/providers/openrouter.models.ts +++ b/packages/ai/src/providers/openrouter.models.ts @@ -10,6 +10,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -27,6 +28,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -44,6 +46,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -61,6 +64,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -78,6 +82,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -95,6 +100,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -112,6 +118,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, reasoning: false, input: ["text", "image"], cost: { @@ -129,6 +136,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, reasoning: true, input: ["text", "image"], cost: { @@ -146,6 +154,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, reasoning: true, input: ["text", "image"], cost: { @@ -163,6 +172,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, reasoning: true, input: ["text", "image"], cost: { @@ -180,6 +190,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, reasoning: true, input: ["text", "image"], cost: { @@ -197,6 +208,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, reasoning: true, input: ["text", "image"], cost: { @@ -214,6 +226,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, reasoning: true, thinkingLevelMap: {"xhigh":"max"}, input: ["text", "image"], @@ -232,6 +245,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, reasoning: true, thinkingLevelMap: {"xhigh":"max"}, input: ["text", "image"], @@ -250,6 +264,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -268,6 +283,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -286,6 +302,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -304,6 +321,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -322,6 +340,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, reasoning: true, input: ["text", "image"], cost: { @@ -339,6 +358,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, reasoning: true, input: ["text", "image"], cost: { @@ -356,6 +376,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, reasoning: true, input: ["text", "image"], cost: { @@ -373,6 +394,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -390,6 +412,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -407,6 +430,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -424,6 +448,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -441,6 +466,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -458,6 +484,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -475,6 +502,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -492,6 +520,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -509,6 +538,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -526,6 +556,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -543,6 +574,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -560,6 +592,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -577,6 +610,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -594,6 +628,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -611,6 +646,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -628,6 +664,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -645,6 +682,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -662,6 +700,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -679,6 +718,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -696,7 +736,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true}, + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter","requiresReasoningContentOnAssistantMessages":true}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh"}, input: ["text"], @@ -715,7 +755,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true}, + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter","requiresReasoningContentOnAssistantMessages":true}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh"}, input: ["text"], @@ -734,6 +774,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -751,6 +792,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -768,6 +810,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -785,6 +828,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -802,6 +846,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -819,6 +864,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -836,6 +882,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -853,6 +900,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -870,6 +918,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -887,6 +936,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -904,6 +954,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -921,6 +972,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -938,6 +990,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -955,6 +1008,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -972,6 +1026,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -989,6 +1044,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -1006,6 +1062,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -1023,6 +1080,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -1040,6 +1098,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -1057,6 +1116,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -1074,6 +1134,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -1091,6 +1152,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, thinkingLevelMap: {"off":null}, input: ["text"], @@ -1109,6 +1171,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -1126,6 +1189,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -1143,6 +1207,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -1160,6 +1225,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -1177,6 +1243,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -1194,6 +1261,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -1211,6 +1279,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -1228,6 +1297,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -1245,6 +1315,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -1262,6 +1333,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -1279,6 +1351,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -1296,6 +1369,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -1313,6 +1387,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -1330,6 +1405,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -1347,6 +1423,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -1364,6 +1441,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -1381,6 +1459,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -1398,6 +1477,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -1415,6 +1495,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -1432,6 +1513,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -1449,6 +1531,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -1466,6 +1549,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -1483,6 +1567,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -1500,6 +1585,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -1517,6 +1603,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -1534,6 +1621,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -1551,6 +1639,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -1568,6 +1657,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -1585,6 +1675,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -1602,6 +1693,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -1619,6 +1711,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -1636,6 +1729,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -1653,6 +1747,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -1670,6 +1765,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -1687,6 +1783,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -1704,6 +1801,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -1721,6 +1819,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -1738,6 +1837,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -1755,7 +1855,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - compat: {"supportsDeveloperRole":false,"requiresReasoningContentOnAssistantMessages":true}, + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter","requiresReasoningContentOnAssistantMessages":true}, reasoning: true, input: ["text", "image"], cost: { @@ -1773,40 +1873,25 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { - input: 0.612, - output: 3.069, - cacheRead: 0.1296, + input: 0.68, + output: 3.41, + cacheRead: 0.144, cacheWrite: 0, }, contextWindow: 262144, maxTokens: 262144, } satisfies Model<"openai-completions">, - "nex-agi/nex-n2-pro:free": { - id: "nex-agi/nex-n2-pro:free", - name: "Nex AGI: Nex-N2-Pro (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 256000, - } satisfies Model<"openai-completions">, "nvidia/llama-3.3-nemotron-super-49b-v1.5": { id: "nvidia/llama-3.3-nemotron-super-49b-v1.5", name: "NVIDIA: Llama 3.3 Nemotron Super 49B V1.5", api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -1824,6 +1909,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -1841,6 +1927,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -1858,6 +1945,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -1875,6 +1963,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -1892,6 +1981,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -1909,6 +1999,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -1926,6 +2017,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -1943,6 +2035,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -1960,6 +2053,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -1977,6 +2071,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -1994,6 +2089,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -2011,6 +2107,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -2028,6 +2125,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -2045,6 +2143,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -2062,6 +2161,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -2079,6 +2179,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -2096,6 +2197,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -2113,6 +2215,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -2130,6 +2233,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -2147,6 +2251,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -2164,6 +2269,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -2181,6 +2287,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -2198,6 +2305,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -2215,6 +2323,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -2232,6 +2341,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -2249,6 +2359,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -2266,6 +2377,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -2283,6 +2395,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -2300,6 +2413,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -2317,6 +2431,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -2334,6 +2449,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -2351,6 +2467,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -2368,6 +2485,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -2385,6 +2503,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -2402,6 +2521,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -2420,6 +2540,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: false, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -2438,6 +2559,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -2456,6 +2578,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -2474,6 +2597,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: false, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -2492,6 +2616,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -2510,6 +2635,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -2528,6 +2654,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -2546,6 +2673,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -2564,6 +2692,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -2582,6 +2711,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -2600,6 +2730,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh","off":null,"minimal":null,"low":null}, input: ["text", "image"], @@ -2618,6 +2749,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -2635,6 +2767,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -2652,6 +2785,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -2669,6 +2803,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -2686,6 +2821,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -2703,6 +2839,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -2720,6 +2857,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -2737,6 +2875,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -2754,6 +2893,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -2771,6 +2911,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -2788,6 +2929,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -2805,6 +2947,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -2822,6 +2965,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -2839,6 +2983,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -2856,6 +3001,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -2873,6 +3019,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -2890,6 +3037,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -2907,6 +3055,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -2924,6 +3073,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -2941,6 +3091,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -2958,6 +3109,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -2975,6 +3127,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -2992,6 +3145,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -3009,6 +3163,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -3026,6 +3181,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -3037,29 +3193,13 @@ export const OPENROUTER_MODELS = { contextWindow: 262144, maxTokens: 32768, } satisfies Model<"openai-completions">, - "prime-intellect/intellect-3": { - id: "prime-intellect/intellect-3", - name: "Prime Intellect: INTELLECT-3", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.2, - output: 1.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, "qwen/qwen-2.5-72b-instruct": { id: "qwen/qwen-2.5-72b-instruct", name: "Qwen2.5 72B Instruct", api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -3077,6 +3217,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -3094,6 +3235,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -3111,6 +3253,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -3128,6 +3271,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -3145,6 +3289,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -3162,6 +3307,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -3179,6 +3325,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -3196,6 +3343,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -3213,6 +3361,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -3230,6 +3379,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -3247,6 +3397,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -3264,6 +3415,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -3281,6 +3433,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -3298,6 +3451,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -3315,6 +3469,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -3332,6 +3487,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -3349,6 +3505,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -3366,6 +3523,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -3383,6 +3541,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -3400,6 +3559,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -3417,6 +3577,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -3434,6 +3595,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -3451,6 +3613,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -3468,6 +3631,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -3485,6 +3649,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -3502,6 +3667,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -3519,6 +3685,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -3536,6 +3703,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -3553,6 +3721,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -3570,6 +3739,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -3587,6 +3757,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -3604,6 +3775,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -3621,6 +3793,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -3638,6 +3811,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -3655,6 +3829,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -3672,6 +3847,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -3689,6 +3865,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -3706,6 +3883,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -3723,6 +3901,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -3740,6 +3919,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -3757,6 +3937,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -3774,6 +3955,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -3791,6 +3973,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -3808,6 +3991,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -3825,6 +4009,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -3842,6 +4027,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -3859,6 +4045,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text", "image"], cost: { @@ -3876,6 +4063,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -3893,6 +4081,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -3910,6 +4099,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -3927,6 +4117,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -3944,6 +4135,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -3961,6 +4153,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -3978,6 +4171,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: false, input: ["text"], cost: { @@ -3995,6 +4189,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -4012,6 +4207,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -4029,6 +4225,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -4046,6 +4243,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -4063,6 +4261,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -4080,6 +4279,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -4097,6 +4297,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -4114,6 +4315,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -4131,6 +4333,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -4148,6 +4351,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -4165,6 +4369,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -4182,6 +4387,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -4199,6 +4405,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -4216,6 +4423,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -4233,6 +4441,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -4250,6 +4459,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text"], cost: { @@ -4267,17 +4477,36 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text"], cost: { - input: 1, - output: 4, - cacheRead: 0.18, + input: 0.98, + output: 3.08, + cacheRead: 0.182, cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 32768, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "z-ai/glm-5v-turbo": { + id: "z-ai/glm-5v-turbo", + name: "Z.ai: GLM 5V Turbo", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.2, + output: 4, + cacheRead: 0.24, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 131072, } satisfies Model<"openai-completions">, "~anthropic/claude-fable-latest": { id: "~anthropic/claude-fable-latest", @@ -4285,6 +4514,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -4302,6 +4532,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -4319,6 +4550,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -4336,6 +4568,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -4353,6 +4586,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -4370,6 +4604,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -4387,6 +4622,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -4404,6 +4640,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { @@ -4421,6 +4658,7 @@ export const OPENROUTER_MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, reasoning: true, input: ["text", "image"], cost: { diff --git a/packages/ai/src/providers/together.models.ts b/packages/ai/src/providers/together.models.ts index 5ba1500c..5c0104e0 100644 --- a/packages/ai/src/providers/together.models.ts +++ b/packages/ai/src/providers/together.models.ts @@ -10,7 +10,7 @@ export const TOGETHER_MODELS = { api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, reasoning: true, thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null}, input: ["text"], @@ -29,7 +29,7 @@ export const TOGETHER_MODELS = { api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, input: ["text", "image"], @@ -48,7 +48,7 @@ export const TOGETHER_MODELS = { api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, reasoning: false, input: ["text"], cost: { @@ -66,7 +66,7 @@ export const TOGETHER_MODELS = { api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, reasoning: false, input: ["text"], cost: { @@ -84,7 +84,7 @@ export const TOGETHER_MODELS = { api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, input: ["text", "image"], @@ -103,7 +103,7 @@ export const TOGETHER_MODELS = { api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, input: ["text", "image"], @@ -122,7 +122,7 @@ export const TOGETHER_MODELS = { api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, input: ["text"], @@ -141,7 +141,7 @@ export const TOGETHER_MODELS = { api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, reasoning: false, input: ["text"], cost: { @@ -159,7 +159,7 @@ export const TOGETHER_MODELS = { api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":null}, input: ["text"], @@ -178,7 +178,7 @@ export const TOGETHER_MODELS = { api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, reasoning: false, input: ["text"], cost: { @@ -196,7 +196,7 @@ export const TOGETHER_MODELS = { api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, input: ["text", "image"], @@ -215,7 +215,7 @@ export const TOGETHER_MODELS = { api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, reasoning: false, input: ["text"], cost: { @@ -233,7 +233,7 @@ export const TOGETHER_MODELS = { api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, input: ["text", "image"], @@ -252,7 +252,7 @@ export const TOGETHER_MODELS = { api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, input: ["text"], @@ -271,7 +271,7 @@ export const TOGETHER_MODELS = { api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, input: ["text"], @@ -290,7 +290,7 @@ export const TOGETHER_MODELS = { api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"openai"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"maxTokensField":"max_tokens","thinkingFormat":"openai","supportsStrictMode":false,"supportsLongCacheRetention":false}, reasoning: true, thinkingLevelMap: {"off":null,"minimal":null}, input: ["text"], @@ -309,7 +309,7 @@ export const TOGETHER_MODELS = { api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"openai"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"maxTokensField":"max_tokens","thinkingFormat":"openai","supportsStrictMode":false,"supportsLongCacheRetention":false}, reasoning: true, thinkingLevelMap: {"off":null,"minimal":null}, input: ["text"], @@ -328,7 +328,7 @@ export const TOGETHER_MODELS = { api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, input: ["text"], @@ -347,7 +347,7 @@ export const TOGETHER_MODELS = { api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, input: ["text"], diff --git a/packages/ai/src/providers/xai.models.ts b/packages/ai/src/providers/xai.models.ts index 878193aa..5ebb3e55 100644 --- a/packages/ai/src/providers/xai.models.ts +++ b/packages/ai/src/providers/xai.models.ts @@ -10,6 +10,7 @@ export const XAI_MODELS = { api: "openai-completions", provider: "xai", baseUrl: "https://api.x.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, reasoning: false, input: ["text"], cost: { @@ -27,6 +28,7 @@ export const XAI_MODELS = { api: "openai-completions", provider: "xai", baseUrl: "https://api.x.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, reasoning: false, input: ["text"], cost: { @@ -44,6 +46,7 @@ export const XAI_MODELS = { api: "openai-completions", provider: "xai", baseUrl: "https://api.x.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, reasoning: false, input: ["text", "image"], cost: { @@ -61,6 +64,7 @@ export const XAI_MODELS = { api: "openai-completions", provider: "xai", baseUrl: "https://api.x.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, reasoning: true, input: ["text", "image"], cost: { @@ -78,6 +82,7 @@ export const XAI_MODELS = { api: "openai-completions", provider: "xai", baseUrl: "https://api.x.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, reasoning: true, input: ["text", "image"], cost: { @@ -95,6 +100,7 @@ export const XAI_MODELS = { api: "openai-completions", provider: "xai", baseUrl: "https://api.x.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, reasoning: true, input: ["text", "image"], cost: { @@ -112,6 +118,7 @@ export const XAI_MODELS = { api: "openai-completions", provider: "xai", baseUrl: "https://api.x.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, reasoning: false, input: ["text"], cost: { diff --git a/packages/ai/src/providers/zai-coding-cn.models.ts b/packages/ai/src/providers/zai-coding-cn.models.ts index 90865c0b..cd13ebe3 100644 --- a/packages/ai/src/providers/zai-coding-cn.models.ts +++ b/packages/ai/src/providers/zai-coding-cn.models.ts @@ -10,7 +10,7 @@ export const ZAI_CODING_CN_MODELS = { api: "openai-completions", provider: "zai-coding-cn", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai"}, reasoning: true, input: ["text"], cost: { @@ -28,7 +28,7 @@ export const ZAI_CODING_CN_MODELS = { api: "openai-completions", provider: "zai-coding-cn", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai","zaiToolStream":true}, reasoning: true, input: ["text"], cost: { @@ -46,7 +46,7 @@ export const ZAI_CODING_CN_MODELS = { api: "openai-completions", provider: "zai-coding-cn", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai","zaiToolStream":true}, reasoning: true, input: ["text"], cost: { @@ -64,7 +64,7 @@ export const ZAI_CODING_CN_MODELS = { api: "openai-completions", provider: "zai-coding-cn", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai","zaiToolStream":true}, reasoning: true, input: ["text"], cost: { @@ -82,7 +82,7 @@ export const ZAI_CODING_CN_MODELS = { api: "openai-completions", provider: "zai-coding-cn", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","supportsReasoningEffort":true,"zaiToolStream":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"thinkingFormat":"zai","zaiToolStream":true}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":"high","medium":"high","high":"high","xhigh":"max"}, input: ["text"], @@ -101,7 +101,7 @@ export const ZAI_CODING_CN_MODELS = { api: "openai-completions", provider: "zai-coding-cn", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai","zaiToolStream":true}, reasoning: true, input: ["text", "image"], cost: { diff --git a/packages/ai/src/providers/zai.models.ts b/packages/ai/src/providers/zai.models.ts index 364a158f..ba7f55c4 100644 --- a/packages/ai/src/providers/zai.models.ts +++ b/packages/ai/src/providers/zai.models.ts @@ -10,7 +10,7 @@ export const ZAI_MODELS = { api: "openai-completions", provider: "zai", baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai"}, reasoning: true, input: ["text"], cost: { @@ -28,7 +28,7 @@ export const ZAI_MODELS = { api: "openai-completions", provider: "zai", baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai","zaiToolStream":true}, reasoning: true, input: ["text"], cost: { @@ -46,7 +46,7 @@ export const ZAI_MODELS = { api: "openai-completions", provider: "zai", baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai","zaiToolStream":true}, reasoning: true, input: ["text"], cost: { @@ -64,7 +64,7 @@ export const ZAI_MODELS = { api: "openai-completions", provider: "zai", baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai","zaiToolStream":true}, reasoning: true, input: ["text"], cost: { @@ -82,7 +82,7 @@ export const ZAI_MODELS = { api: "openai-completions", provider: "zai", baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","supportsReasoningEffort":true,"zaiToolStream":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"thinkingFormat":"zai","zaiToolStream":true}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":"high","medium":"high","high":"high","xhigh":"max"}, input: ["text"], @@ -101,7 +101,7 @@ export const ZAI_MODELS = { api: "openai-completions", provider: "zai", baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai","zaiToolStream":true}, reasoning: true, input: ["text", "image"], cost: { diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index 35b79b7c..29449813 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -99,6 +99,7 @@ export type Transport = "sse" | "websocket" | "websocket-cached" | "auto"; /** Provider-scoped environment overrides. Values take precedence over process.env. */ export type ProviderEnv = Record; +export type ProviderHeaders = Record; export interface ProviderResponse { status: number; @@ -142,8 +143,9 @@ export interface StreamOptions { * On AWS Bedrock these are injected via a Smithy `build`-step middleware so * they are covered by SigV4 signing; reserved headers (`x-amz-*`, * `authorization`, `host`) are silently ignored to preserve SigV4 / bearer auth. + * A null value suppresses a provider/API default header with the same name. */ - headers?: Record; + headers?: ProviderHeaders; /** * HTTP request timeout in milliseconds for providers/SDKs that support it. * For example, OpenAI and Anthropic SDK clients default to 10 minutes. @@ -256,8 +258,9 @@ export interface ImagesOptions { /** * Optional custom HTTP headers to include in API requests. * Merged with provider defaults; can override default headers. + * A null value suppresses a provider/API default header with the same name. */ - headers?: Record; + headers?: ProviderHeaders; /** * HTTP request timeout in milliseconds for providers/SDKs that support it. */ diff --git a/packages/ai/src/utils/headers.ts b/packages/ai/src/utils/headers.ts index fae8a036..2d923d28 100644 --- a/packages/ai/src/utils/headers.ts +++ b/packages/ai/src/utils/headers.ts @@ -1,3 +1,5 @@ +import type { ProviderHeaders } from "../types.ts"; + export function headersToRecord(headers: Headers): Record { const result: Record = {}; for (const [key, value] of headers.entries()) { @@ -5,3 +7,12 @@ export function headersToRecord(headers: Headers): Record { } return result; } + +export function providerHeadersToRecord(headers: ProviderHeaders | undefined): Record | undefined { + if (!headers) return undefined; + const result: Record = {}; + for (const [key, value] of Object.entries(headers)) { + if (value !== null) result[key] = value; + } + return Object.keys(result).length > 0 ? result : undefined; +} diff --git a/packages/ai/test/compat-env.test.ts b/packages/ai/test/compat-env.test.ts index 69a57da1..59928211 100644 --- a/packages/ai/test/compat-env.test.ts +++ b/packages/ai/test/compat-env.test.ts @@ -9,7 +9,7 @@ const model: Model<"openai-responses"> = { id: "test-model", name: "Test Model", api: "openai-responses", - provider: "openai", + provider: "custom-openai", baseUrl: "https://example.test/v1", reasoning: false, input: ["text"], @@ -38,12 +38,12 @@ function message(): AssistantMessage { }; } -describe("compat env API key injection", () => { +describe("compat legacy API fallback", () => { afterEach(() => { resetApiProviders(); }); - it("uses request-scoped env when injecting provider API keys", async () => { + it("dispatches unknown providers through the legacy API registry", async () => { let capturedApiKey: string | undefined; registerApiProvider({ api: "openai-responses", @@ -67,8 +67,8 @@ describe("compat env API key injection", () => { }, }); - await complete(model, context, { env: { OPENAI_API_KEY: "scoped-key" } }); + await complete(model, context, { apiKey: "request-key" }); - expect(capturedApiKey).toBe("scoped-key"); + expect(capturedApiKey).toBe("request-key"); }); }); diff --git a/packages/ai/test/openai-completions-empty-tools.test.ts b/packages/ai/test/openai-completions-empty-tools.test.ts index 5906ea30..e83f63ae 100644 --- a/packages/ai/test/openai-completions-empty-tools.test.ts +++ b/packages/ai/test/openai-completions-empty-tools.test.ts @@ -127,6 +127,7 @@ describe("openai-completions empty tools handling", () => { }); it("uses conservative OpenAI-compatible fields for Cloudflare AI Gateway /compat models", async () => { + process.env.CLOUDFLARE_API_KEY = "cf-token"; process.env.CLOUDFLARE_ACCOUNT_ID = "account-id"; process.env.CLOUDFLARE_GATEWAY_ID = "gateway-id"; const model = getModel("cloudflare-ai-gateway", "workers-ai/@cf/moonshotai/kimi-k2.6")!; @@ -137,7 +138,7 @@ describe("openai-completions empty tools handling", () => { systemPrompt: "You are helpful.", messages: [{ role: "user", content: "hi", timestamp: Date.now() }], }, - { apiKey: "test", maxTokens: 1234, reasoning: "high" }, + { maxTokens: 1234, reasoning: "high" }, ).result(); const params = mockState.lastParams as { @@ -159,35 +160,25 @@ describe("openai-completions empty tools handling", () => { }; expect(clientOptions.baseURL).toBe("https://gateway.ai.cloudflare.com/v1/account-id/gateway-id/compat"); expect(clientOptions.defaultHeaders?.Authorization).toBeNull(); - expect(clientOptions.defaultHeaders?.["cf-aig-authorization"]).toBe("Bearer test"); + expect(clientOptions.defaultHeaders?.["cf-aig-authorization"]).toBe("Bearer cf-token"); }); - it("uses provider env before process.env for Cloudflare AI Gateway base URL", async () => { - process.env.CLOUDFLARE_ACCOUNT_ID = "process-account"; - process.env.CLOUDFLARE_GATEWAY_ID = "process-gateway"; + it("resolves Cloudflare AI Gateway base URL through provider auth", async () => { + process.env.CLOUDFLARE_API_KEY = "cf-token"; + process.env.CLOUDFLARE_ACCOUNT_ID = "account-id"; + process.env.CLOUDFLARE_GATEWAY_ID = "gateway-id"; const model = getModel("cloudflare-ai-gateway", "workers-ai/@cf/moonshotai/kimi-k2.6")!; - await streamSimple( - model, - { - messages: [{ role: "user", content: "hi", timestamp: Date.now() }], - }, - { - apiKey: "test", - env: { - CLOUDFLARE_ACCOUNT_ID: "provider-account", - CLOUDFLARE_GATEWAY_ID: "provider-gateway", - }, - }, - ).result(); + await streamSimple(model, { + messages: [{ role: "user", content: "hi", timestamp: Date.now() }], + }).result(); const clientOptions = mockState.lastClientOptions as { baseURL?: string }; - expect(clientOptions.baseURL).toBe( - "https://gateway.ai.cloudflare.com/v1/provider-account/provider-gateway/compat", - ); + expect(clientOptions.baseURL).toBe("https://gateway.ai.cloudflare.com/v1/account-id/gateway-id/compat"); }); it("preserves inline upstream Authorization for Cloudflare AI Gateway BYOK requests", async () => { + process.env.CLOUDFLARE_API_KEY = "cf-token"; process.env.CLOUDFLARE_ACCOUNT_ID = "account-id"; process.env.CLOUDFLARE_GATEWAY_ID = "gateway-id"; const model = getModel("cloudflare-ai-gateway", "gpt-5.1")!; @@ -197,7 +188,7 @@ describe("openai-completions empty tools handling", () => { { messages: [{ role: "user", content: "hi", timestamp: Date.now() }], }, - { apiKey: "cf-token", headers: { Authorization: "Bearer upstream-token" } }, + { headers: { Authorization: "Bearer upstream-token" } }, ).result(); const clientOptions = mockState.lastClientOptions as { defaultHeaders?: Record }; @@ -206,6 +197,7 @@ describe("openai-completions empty tools handling", () => { }); it("sends session affinity headers for Workers AI through Cloudflare AI Gateway", async () => { + process.env.CLOUDFLARE_API_KEY = "cf-token"; process.env.CLOUDFLARE_ACCOUNT_ID = "account-id"; process.env.CLOUDFLARE_GATEWAY_ID = "gateway-id"; const workersModel = getModel("cloudflare-ai-gateway", "workers-ai/@cf/moonshotai/kimi-k2.6")!; @@ -215,7 +207,7 @@ describe("openai-completions empty tools handling", () => { { messages: [{ role: "user", content: "hi", timestamp: Date.now() }], }, - { apiKey: "test", sessionId: "session-1" }, + { sessionId: "session-1" }, ).result(); const clientOptions = mockState.lastClientOptions as { defaultHeaders?: Record }; diff --git a/packages/ai/test/providers.test.ts b/packages/ai/test/providers.test.ts index 56a72b68..e3e8c66d 100644 --- a/packages/ai/test/providers.test.ts +++ b/packages/ai/test/providers.test.ts @@ -5,6 +5,8 @@ import { createModels, createProvider } from "../src/models.ts"; import { builtinModels, builtinProviders } from "../src/providers/all.ts"; import { amazonBedrockProvider } from "../src/providers/amazon-bedrock.ts"; import { anthropicProvider } from "../src/providers/anthropic.ts"; +import { cloudflareAIGatewayProvider } from "../src/providers/cloudflare-ai-gateway.ts"; +import { cloudflareWorkersAIProvider } from "../src/providers/cloudflare-workers-ai.ts"; import { fauxAssistantMessage, fauxProvider } from "../src/providers/faux.ts"; import { googleVertexProvider } from "../src/providers/google-vertex.ts"; import type { Api, Context, Model, ProviderStreams } from "../src/types.ts"; @@ -66,6 +68,55 @@ describe("builtin providers", () => { expect(await unconfigured.getAuth(model)).toBeUndefined(); }); + it("requires Cloudflare Workers AI account config and returns scoped env", async () => { + const missingAccount = createModels({ authContext: fakeAuthContext({ CLOUDFLARE_API_KEY: "cf-key" }) }); + missingAccount.setProvider(cloudflareWorkersAIProvider()); + const model = missingAccount.getModels("cloudflare-workers-ai")[0]; + expect(await missingAccount.getAuth(model)).toBeUndefined(); + + const configured = createModels({ + authContext: fakeAuthContext({ CLOUDFLARE_API_KEY: "cf-key", CLOUDFLARE_ACCOUNT_ID: "account-id" }), + }); + configured.setProvider(cloudflareWorkersAIProvider()); + const result = await configured.getAuth(model); + expect(result?.auth).toEqual({ + apiKey: "cf-key", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/account-id/ai/v1", + }); + expect(result?.env).toEqual({ CLOUDFLARE_ACCOUNT_ID: "account-id" }); + }); + + it("requires Cloudflare AI Gateway account and gateway config and returns scoped env headers", async () => { + const missingGateway = createModels({ + authContext: fakeAuthContext({ CLOUDFLARE_API_KEY: "cf-key", CLOUDFLARE_ACCOUNT_ID: "account-id" }), + }); + missingGateway.setProvider(cloudflareAIGatewayProvider()); + const model = missingGateway.getModels("cloudflare-ai-gateway")[0]; + expect(await missingGateway.getAuth(model)).toBeUndefined(); + + const configured = createModels({ + authContext: fakeAuthContext({ + CLOUDFLARE_API_KEY: "cf-key", + CLOUDFLARE_ACCOUNT_ID: "account-id", + CLOUDFLARE_GATEWAY_ID: "gateway-id", + }), + }); + configured.setProvider(cloudflareAIGatewayProvider()); + const result = await configured.getAuth(model); + expect(result?.auth).toEqual({ + headers: { + "cf-aig-authorization": "Bearer cf-key", + Authorization: null, + "x-api-key": null, + }, + baseUrl: "https://gateway.ai.cloudflare.com/v1/account-id/gateway-id/anthropic", + }); + expect(result?.env).toEqual({ + CLOUDFLARE_ACCOUNT_ID: "account-id", + CLOUDFLARE_GATEWAY_ID: "gateway-id", + }); + }); + it("resolves vertex via ADC file plus project and location", async () => { const adc = "~/.config/gcloud/application_default_credentials.json"; const configured = createModels({ diff --git a/packages/coding-agent/src/core/provider-attribution.ts b/packages/coding-agent/src/core/provider-attribution.ts index 97ad1cfa..d3c6652d 100644 --- a/packages/coding-agent/src/core/provider-attribution.ts +++ b/packages/coding-agent/src/core/provider-attribution.ts @@ -1,4 +1,4 @@ -import type { Api, Model } from "@earendil-works/pi-ai"; +import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai"; import type { SettingsManager } from "./settings-manager.ts"; import { isInstallTelemetryEnabled } from "./telemetry.ts"; @@ -92,9 +92,9 @@ export function mergeProviderAttributionHeaders( model: Model, settingsManager: SettingsManager, sessionId: string | undefined, - ...headerSources: Array | undefined> -): Record | undefined { - const merged = { + ...headerSources: Array +): ProviderHeaders | undefined { + const merged: ProviderHeaders = { ...getSessionHeaders(model, sessionId), ...getDefaultAttributionHeaders(model, settingsManager), }; diff --git a/packages/coding-agent/test/agent-session-runtime-events.test.ts b/packages/coding-agent/test/agent-session-runtime-events.test.ts index 42b01fe1..348c0451 100644 --- a/packages/coding-agent/test/agent-session-runtime-events.test.ts +++ b/packages/coding-agent/test/agent-session-runtime-events.test.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai"; +import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai/compat"; import { afterEach, describe, expect, it } from "vitest"; import { type CreateAgentSessionRuntimeFactory, diff --git a/packages/coding-agent/test/sdk-openrouter-attribution.test.ts b/packages/coding-agent/test/sdk-openrouter-attribution.test.ts index b03e5bb6..bae02aa4 100644 --- a/packages/coding-agent/test/sdk-openrouter-attribution.test.ts +++ b/packages/coding-agent/test/sdk-openrouter-attribution.test.ts @@ -6,6 +6,7 @@ import { type AssistantMessage, createAssistantMessageEventStream, type Model, + type ProviderHeaders, type SimpleStreamOptions, } from "@earendil-works/pi-ai"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; @@ -88,7 +89,7 @@ describe("createAgentSession provider attribution headers", () => { requestHeaders?: Record; sessionId?: string; } = {}, - ): Promise | undefined> { + ): Promise { const settingsManager = SettingsManager.create(cwd, agentDir); if (options.telemetryEnabled === false) { settingsManager.setEnableInstallTelemetry(false); diff --git a/packages/coding-agent/test/suite/agent-session-runtime.test.ts b/packages/coding-agent/test/suite/agent-session-runtime.test.ts index 69c817ac..cf1a81c2 100644 --- a/packages/coding-agent/test/suite/agent-session-runtime.test.ts +++ b/packages/coding-agent/test/suite/agent-session-runtime.test.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, realpathSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, parse } from "node:path"; -import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai"; +import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai/compat"; import { afterEach, describe, expect, it } from "vitest"; import { type CreateAgentSessionRuntimeFactory, diff --git a/packages/coding-agent/test/suite/harness.ts b/packages/coding-agent/test/suite/harness.ts index cec82013..0fb95825 100644 --- a/packages/coding-agent/test/suite/harness.ts +++ b/packages/coding-agent/test/suite/harness.ts @@ -7,8 +7,13 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import type { AgentMessage, AgentTool } from "@earendil-works/pi-agent-core"; import { Agent } from "@earendil-works/pi-agent-core"; -import type { FauxModelDefinition, FauxProviderRegistration, FauxResponseStep, Model } from "@earendil-works/pi-ai"; -import { registerFauxProvider } from "@earendil-works/pi-ai"; +import type { + FauxModelDefinition, + FauxProviderRegistration, + FauxResponseStep, + Model, +} from "@earendil-works/pi-ai/compat"; +import { registerFauxProvider } from "@earendil-works/pi-ai/compat"; import { AgentSession, type AgentSessionEvent } from "../../src/core/agent-session.ts"; import { AuthStorage } from "../../src/core/auth-storage.ts"; import type { ExtensionRunner } from "../../src/core/extensions/index.ts"; diff --git a/packages/coding-agent/test/suite/regressions/2753-reload-stale-resource-settings.test.ts b/packages/coding-agent/test/suite/regressions/2753-reload-stale-resource-settings.test.ts index 17d9b302..81d3f275 100644 --- a/packages/coding-agent/test/suite/regressions/2753-reload-stale-resource-settings.test.ts +++ b/packages/coding-agent/test/suite/regressions/2753-reload-stale-resource-settings.test.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { registerFauxProvider } from "@earendil-works/pi-ai"; +import { registerFauxProvider } from "@earendil-works/pi-ai/compat"; import { afterEach, describe, expect, it } from "vitest"; import { type CreateAgentSessionRuntimeFactory, diff --git a/packages/coding-agent/test/suite/regressions/2860-replaced-session-context.test.ts b/packages/coding-agent/test/suite/regressions/2860-replaced-session-context.test.ts index 0e4c061e..b6f7002e 100644 --- a/packages/coding-agent/test/suite/regressions/2860-replaced-session-context.test.ts +++ b/packages/coding-agent/test/suite/regressions/2860-replaced-session-context.test.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai"; +import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai/compat"; import { afterEach, describe, expect, it } from "vitest"; import type { AgentSession } from "../../../src/core/agent-session.ts"; import { diff --git a/packages/coding-agent/test/suite/regressions/5596-missing-theme-export.test.ts b/packages/coding-agent/test/suite/regressions/5596-missing-theme-export.test.ts index 58bc17f8..296993e0 100644 --- a/packages/coding-agent/test/suite/regressions/5596-missing-theme-export.test.ts +++ b/packages/coding-agent/test/suite/regressions/5596-missing-theme-export.test.ts @@ -2,7 +2,7 @@ import { existsSync, mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Agent } from "@earendil-works/pi-agent-core"; -import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai"; +import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai/compat"; import { afterEach, describe, expect, it } from "vitest"; import { AgentSession } from "../../../src/core/agent-session.ts"; import { AuthStorage } from "../../../src/core/auth-storage.ts"; From 15f92260d7dd23ed08d18f8f0b33746ce8085e57 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 23 Jun 2026 15:32:42 +0200 Subject: [PATCH 093/122] docs(ai): expand models migration guide --- packages/ai/CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 28bd8d48..28895fa5 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -35,6 +35,24 @@ Migration guide: models.setProvider(anthropicProvider()); ``` +- To call a raw API implementation directly, import from `@earendil-works/pi-ai/api/*` and pass a compatible model plus auth/options yourself. Raw API modules export `stream` and `streamSimple`; use `.result()` on the returned stream for `complete`/`completeSimple` behavior: + + ```ts + import { streamSimple } from "@earendil-works/pi-ai/api/anthropic-messages"; + import { getBuiltinModel } from "@earendil-works/pi-ai/providers/all"; + + const model = getBuiltinModel("anthropic", "claude-haiku-4-5"); + const stream = streamSimple( + model, + { messages: [{ role: "user", content: "Hello", timestamp: Date.now() }] }, + { apiKey: process.env.ANTHROPIC_API_KEY }, + ); + + const message = await stream.result(); + ``` + + Custom raw models must set the matching `api` value (for example `"anthropic-messages"` for `api/anthropic-messages`) and any required provider compatibility metadata in `model.compat`. + ### Added - New `Models` runtime: `createModels()` builds an isolated provider collection with sync model reads (`getModels`/`getModel` return the last-known lists), an explicit async `refresh(provider?)` for dynamic providers, auth resolution (`getAuth`), and `stream`/`complete`/`streamSimple`/`completeSimple` that resolve auth through the owning provider. `createProvider()` builds providers from parts (single API implementation or a map dispatched on `model.api`; static `models` array plus an optional `refreshModels` fetcher with in-flight dedupe); `hasApi()` narrows dynamically listed models. From 12ace0ba46be93b31e751743e479af9087b1122a Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 23 Jun 2026 15:33:37 +0200 Subject: [PATCH 094/122] docs(ai): reference README in migration guide --- packages/ai/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 28895fa5..a4af5ca1 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -10,6 +10,7 @@ Migration guide: +- Read `packages/ai/README.md` in full for the new `Models` API, provider factories, auth configuration, image generation, and custom provider examples. - To keep the old global API temporarily, change imports from `@earendil-works/pi-ai` to `@earendil-works/pi-ai/compat`. The compat entrypoint preserves `stream`/`complete`, generated catalog reads, API registry helpers, env API-key helpers, lazy API wrappers, and image globals, but it will be removed in a future release. - To migrate to the new runtime, create a `Models` collection and call methods on it: From e3dcb244f81e3c699a9f5dab96dec0d792c1a51c Mon Sep 17 00:00:00 2001 From: Konstantin Gukov Date: Tue, 23 Jun 2026 15:38:43 +0200 Subject: [PATCH 095/122] Support modern Microsoft Foundry Responses API endpoints: - support *.ai.azure.com endpoints - normalize away the /responses postfix in the query path which by default is added on Microsoft Foundry UI --- packages/ai/README.md | 2 +- packages/ai/src/api/azure-openai-responses.ts | 12 ++++++++++-- packages/ai/test/azure-openai-base-url.test.ts | 10 ++++++++++ packages/coding-agent/docs/providers.md | 3 ++- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/ai/README.md b/packages/ai/README.md index f81f6190..b058bdad 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -368,7 +368,7 @@ Built-in providers resolve these env vars (Node.js; in browsers pass `apiKey` ex |----------|------------------------| | OpenAI | `OPENAI_API_KEY` | | Ant Ling | `ANT_LING_API_KEY` | -| Azure OpenAI | `AZURE_OPENAI_API_KEY` + `AZURE_OPENAI_BASE_URL` (e.g. `https://{resource}.openai.azure.com`) or `AZURE_OPENAI_RESOURCE_NAME`. Supports `*.openai.azure.com` and `*.cognitiveservices.azure.com`; root endpoints auto-normalize to `/openai/v1`. Optional: `AZURE_OPENAI_API_VERSION` (default `v1`), `AZURE_OPENAI_DEPLOYMENT_NAME_MAP`. | +| Azure OpenAI | `AZURE_OPENAI_API_KEY` + `AZURE_OPENAI_BASE_URL` (e.g. `https://{resource}.ai.azure.com`) or `AZURE_OPENAI_RESOURCE_NAME`. Supports `*.openai.azure.com`, `*.cognitiveservices.azure.com` and `*.ai.azure.com`; root endpoints auto-normalize to `/openai/v1`. Optional: `AZURE_OPENAI_API_VERSION` (default `v1`), `AZURE_OPENAI_DEPLOYMENT_NAME_MAP`. | | Anthropic | `ANTHROPIC_API_KEY` or `ANTHROPIC_OAUTH_TOKEN` | | DeepSeek | `DEEPSEEK_API_KEY` | | NVIDIA NIM | `NVIDIA_API_KEY` | diff --git a/packages/ai/src/api/azure-openai-responses.ts b/packages/ai/src/api/azure-openai-responses.ts index 8150a82b..137c43c9 100644 --- a/packages/ai/src/api/azure-openai-responses.ts +++ b/packages/ai/src/api/azure-openai-responses.ts @@ -180,12 +180,20 @@ function normalizeAzureBaseUrl(baseUrl: string): string { } const isAzureHost = - url.hostname.endsWith(".openai.azure.com") || url.hostname.endsWith(".cognitiveservices.azure.com"); + url.hostname.endsWith(".openai.azure.com") || + url.hostname.endsWith(".cognitiveservices.azure.com") || + url.hostname.endsWith(".ai.azure.com"); const normalizedPath = url.pathname.replace(/\/+$/, ""); // Ensure Azure hosts have /openai/v1 as base path so the AzureOpenAI SDK // can append /deployments//... and ?api-version=v1 correctly. - if (isAzureHost && (normalizedPath === "" || normalizedPath === "/" || normalizedPath === "/openai")) { + if ( + isAzureHost && + (normalizedPath === "" || + normalizedPath === "/" || + normalizedPath === "/openai" || + normalizedPath === "/openai/v1/responses") + ) { url.pathname = "/openai/v1"; url.search = ""; } diff --git a/packages/ai/test/azure-openai-base-url.test.ts b/packages/ai/test/azure-openai-base-url.test.ts index e372168b..5908ad38 100644 --- a/packages/ai/test/azure-openai-base-url.test.ts +++ b/packages/ai/test/azure-openai-base-url.test.ts @@ -96,6 +96,11 @@ describe("azure-openai-responses base URL normalization", () => { expect(baseURL).toBe("https://marc-quicktests-resource.cognitiveservices.azure.com/openai/v1"); }); + it("normalizes Microsoft Foundry root endpoints to /openai/v1", async () => { + const baseURL = await captureClientBaseUrl("https://marc-quicktests-resource.ai.azure.com"); + expect(baseURL).toBe("https://marc-quicktests-resource.ai.azure.com/openai/v1"); + }); + it("normalizes Azure OpenAI root endpoints to /openai/v1", async () => { const baseURL = await captureClientBaseUrl("https://my-resource.openai.azure.com"); expect(baseURL).toBe("https://my-resource.openai.azure.com/openai/v1"); @@ -111,6 +116,11 @@ describe("azure-openai-responses base URL normalization", () => { expect(baseURL).toBe("https://my-resource.cognitiveservices.azure.com/openai/v1"); }); + it("normalizes /openai/v1/responses to /openai/v1", async () => { + const baseURL = await captureClientBaseUrl("https://my-resource.services.ai.azure.com/openai/v1/responses"); + expect(baseURL).toBe("https://my-resource.services.ai.azure.com/openai/v1"); + }); + it("preserves explicit non-Azure proxy paths", async () => { const baseURL = await captureClientBaseUrl("https://my-proxy.example.com/v1"); expect(baseURL).toBe("https://my-proxy.example.com/v1"); diff --git a/packages/coding-agent/docs/providers.md b/packages/coding-agent/docs/providers.md index e97eba5d..46163abf 100644 --- a/packages/coding-agent/docs/providers.md +++ b/packages/coding-agent/docs/providers.md @@ -156,8 +156,9 @@ OAuth credentials are also stored here after `/login` and managed automatically. ```bash export AZURE_OPENAI_API_KEY=... -export AZURE_OPENAI_BASE_URL=https://your-resource.openai.azure.com +export AZURE_OPENAI_BASE_URL=https://your-resource.ai.azure.com # also supported: https://your-resource.cognitiveservices.azure.com +# also supported: https://your-resource.openai.azure.com # root endpoints are auto-normalized to /openai/v1 # or use resource name instead of base URL export AZURE_OPENAI_RESOURCE_NAME=your-resource From 2285f87964803c6b12673f1737679e12b8c3755d Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 23 Jun 2026 15:58:50 +0200 Subject: [PATCH 096/122] fix(ai): remove legacy raw API subpaths --- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 32 -------------------------------- 2 files changed, 1 insertion(+), 33 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index a4af5ca1..db6631d3 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -6,7 +6,7 @@ - The root entrypoint (`@earendil-works/pi-ai`) is now core-only and side-effect free. The old global API moved to the temporary `@earendil-works/pi-ai/compat` entrypoint, a strict superset of the root: switching a file's import path is the only migration step. Moved symbols include `stream`/`complete`/`streamSimple`/`completeSimple`, `getModel`/`getModels`/`getProviders` (now deprecated aliases of `getBuiltinModel`/`getBuiltinModels`/`getBuiltinProviders` from `@earendil-works/pi-ai/providers/all`), `registerApiProvider`/`unregisterApiProviders`/`resetApiProviders`/`getApiProvider`, `getEnvApiKey`/`findEnvKeys`, `setBedrockProviderModule`, the per-API lazy stream wrappers (`anthropicMessagesApi`, ...), and the image-generation API. - Renamed the `Provider` type to `ProviderId`. `Provider` now names the runtime provider interface (id, name, auth, model listing, stream behavior). -- API implementation modules moved from `src/providers/` to `@earendil-works/pi-ai/api/*`, renamed by API id (`anthropic` -> `api/anthropic-messages`, `google` -> `api/google-generative-ai`, `mistral` -> `api/mistral-conversations`, `amazon-bedrock` -> `api/bedrock-converse-stream`), each exporting exactly `stream` and `streamSimple`. The old per-impl export names (`streamAnthropic`, `streamSimpleAnthropic`, ...) are gone; the legacy package subpaths (`./anthropic`, `./google`, ...) keep working and point at the new modules. +- API implementation modules moved from `src/providers/` to `@earendil-works/pi-ai/api/*`, renamed by API id (`anthropic` -> `api/anthropic-messages`, `google` -> `api/google-generative-ai`, `mistral` -> `api/mistral-conversations`, `amazon-bedrock` -> `api/bedrock-converse-stream`), each exporting exactly `stream` and `streamSimple`. The old per-impl export names (`streamAnthropic`, `streamSimpleAnthropic`, ...) and legacy raw API subpaths (`./anthropic`, `./google`, `./openai-completions`, ...) are gone; import raw API implementations through `@earendil-works/pi-ai/api/*`. Migration guide: diff --git a/packages/ai/package.json b/packages/ai/package.json index 41392a2b..432219da 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -27,38 +27,6 @@ "types": "./dist/api/*.d.ts", "import": "./dist/api/*.js" }, - "./anthropic": { - "types": "./dist/api/anthropic-messages.d.ts", - "import": "./dist/api/anthropic-messages.js" - }, - "./azure-openai-responses": { - "types": "./dist/api/azure-openai-responses.d.ts", - "import": "./dist/api/azure-openai-responses.js" - }, - "./google": { - "types": "./dist/api/google-generative-ai.d.ts", - "import": "./dist/api/google-generative-ai.js" - }, - "./google-vertex": { - "types": "./dist/api/google-vertex.d.ts", - "import": "./dist/api/google-vertex.js" - }, - "./mistral": { - "types": "./dist/api/mistral-conversations.d.ts", - "import": "./dist/api/mistral-conversations.js" - }, - "./openai-codex-responses": { - "types": "./dist/api/openai-codex-responses.d.ts", - "import": "./dist/api/openai-codex-responses.js" - }, - "./openai-completions": { - "types": "./dist/api/openai-completions.d.ts", - "import": "./dist/api/openai-completions.js" - }, - "./openai-responses": { - "types": "./dist/api/openai-responses.d.ts", - "import": "./dist/api/openai-responses.js" - }, "./oauth": { "types": "./dist/oauth.d.ts", "import": "./dist/oauth.js" From cd95c2749f491ecbf59ccd80975ae1206f704805 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 23 Jun 2026 16:35:45 +0200 Subject: [PATCH 097/122] fix(ai): require OpenAI Responses terminal events --- packages/agent/CHANGELOG.md | 4 + .../src/harness/compaction/compaction.ts | 9 +- .../agent/test/harness/compaction.test.ts | 17 ++ packages/ai/CHANGELOG.md | 1 + .../ai/src/api/openai-responses-shared.ts | 68 ++--- ...nai-responses-partial-json-cleanup.test.ts | 5 + .../openai-responses-terminal-event.test.ts | 233 ++++++++++++++++++ packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/core/agent-session.ts | 12 +- .../src/core/compaction/compaction.ts | 11 +- .../test/agent-session-stats.test.ts | 24 ++ packages/coding-agent/test/compaction.test.ts | 32 +++ 12 files changed, 378 insertions(+), 39 deletions(-) create mode 100644 packages/ai/test/openai-responses-terminal-event.test.ts diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index a48ede06..edbed28a 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -8,6 +8,10 @@ - `compact()`, `generateSummary()`, and `generateBranchSummary()` take a `Models` parameter and no longer accept explicit `apiKey`/`headers`. - `StreamFn` is defined structurally (`(model, context, options?) => AssistantMessageEventStream | Promise<...>`); `Models.streamSimple` satisfies it. +### Fixed + +- Fixed harness compaction estimates to ignore malformed all-zero assistant usage after truncated responses ([#5526](https://github.com/earendil-works/pi/pull/5526) by [@dmmulroy](https://github.com/dmmulroy)). + ## [0.79.10] - 2026-06-22 ## [0.79.9] - 2026-06-20 diff --git a/packages/agent/src/harness/compaction/compaction.ts b/packages/agent/src/harness/compaction/compaction.ts index d6874c33..2d6d5583 100644 --- a/packages/agent/src/harness/compaction/compaction.ts +++ b/packages/agent/src/harness/compaction/compaction.ts @@ -121,14 +121,19 @@ export function calculateContextTokens(usage: Usage): number { function getAssistantUsage(msg: AgentMessage): Usage | undefined { if (msg.role === "assistant" && "usage" in msg) { const assistantMsg = msg as AssistantMessage; - if (assistantMsg.stopReason !== "aborted" && assistantMsg.stopReason !== "error" && assistantMsg.usage) { + if ( + assistantMsg.stopReason !== "aborted" && + assistantMsg.stopReason !== "error" && + assistantMsg.usage && + calculateContextTokens(assistantMsg.usage) > 0 + ) { return assistantMsg.usage; } } return undefined; } -/** Return usage from the last successful assistant message in session entries. */ +/** Return usage from the last valid assistant message in session entries. */ export function getLastAssistantUsage(entries: SessionTreeEntry[]): Usage | undefined { for (let i = entries.length - 1; i >= 0; i--) { const entry = entries[i]; diff --git a/packages/agent/test/harness/compaction.test.ts b/packages/agent/test/harness/compaction.test.ts index 95148c3b..8a228eeb 100644 --- a/packages/agent/test/harness/compaction.test.ts +++ b/packages/agent/test/harness/compaction.test.ts @@ -301,11 +301,28 @@ describe("harness compaction", () => { createMessageEntry({ ...assistant, stopReason: "error" }), ]), ).toBeUndefined(); + expect( + getLastAssistantUsage([ + createMessageEntry(createUserMessage("user")), + createMessageEntry(assistant), + createMessageEntry(createAssistantMessage("partial", createMockUsage(0, 0))), + ]), + ).toBe(usage); expect(estimateContextTokens([createUserMessage("no usage")]).lastUsageIndex).toBeNull(); expect(estimateContextTokens([assistant, createUserMessage("tail")])).toMatchObject({ usageTokens: 20, lastUsageIndex: 0, }); + const estimate = estimateContextTokens([ + createUserMessage("Hello"), + assistant, + createUserMessage("continue"), + createAssistantMessage("Partial thinking", createMockUsage(0, 0)), + ]); + expect(estimate.usageTokens).toBe(20); + expect(estimate.lastUsageIndex).toBe(1); + expect(estimate.trailingTokens).toBeGreaterThan(0); + expect(estimate.tokens).toBe(20 + estimate.trailingTokens); }); it("builds session context with a compaction entry", () => { diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index db6631d3..cdcae32c 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -67,6 +67,7 @@ Migration guide: ### Fixed +- Fixed OpenAI Responses streams to fail when they end before a terminal response event and to treat `response.incomplete` as a length stop ([#5526](https://github.com/earendil-works/pi/pull/5526) by [@dmmulroy](https://github.com/dmmulroy)). - Fixed Amazon Bedrock endpoint resolution to honor scoped `AWS_PROFILE` values. - Fixed Cloudflare providers to require account/gateway configuration and route built-in `/compat` requests through provider auth. - Fixed OpenAI Codex Responses WebSocket sessions to reconnect once when OpenAI's connection limit is reached before output starts ([#5973](https://github.com/earendil-works/pi/issues/5973)). diff --git a/packages/ai/src/api/openai-responses-shared.ts b/packages/ai/src/api/openai-responses-shared.ts index 6fd59a44..72b4b8ae 100644 --- a/packages/ai/src/api/openai-responses-shared.ts +++ b/packages/ai/src/api/openai-responses-shared.ts @@ -294,8 +294,41 @@ export async function processResponsesStream( ): Promise { let currentItem: ResponseReasoningItem | ResponseOutputMessage | ResponseFunctionToolCall | null = null; let currentBlock: ThinkingContent | TextContent | (ToolCall & { partialJson: string }) | null = null; + let sawTerminalResponseEvent = false; const blocks = output.content; const blockIndex = () => blocks.length - 1; + const finalizeResponse = ( + response: Extract["response"], + ): void => { + sawTerminalResponseEvent = true; + if (response?.id) { + output.responseId = response.id; + } + if (response?.usage) { + const cachedTokens = response.usage.input_tokens_details?.cached_tokens || 0; + output.usage = { + // OpenAI includes cached tokens in input_tokens, so subtract to get non-cached input + input: (response.usage.input_tokens || 0) - cachedTokens, + output: response.usage.output_tokens || 0, + cacheRead: cachedTokens, + cacheWrite: 0, + totalTokens: response.usage.total_tokens || 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }; + } + calculateCost(model, output.usage); + if (options?.applyServiceTierPricing) { + const serviceTier = options.resolveServiceTier + ? options.resolveServiceTier(response?.service_tier, options.serviceTier) + : (response?.service_tier ?? options.serviceTier); + options.applyServiceTierPricing(output.usage, serviceTier); + } + // Map status to stop reason + output.stopReason = mapStopReason(response?.status); + if (output.content.some((b) => b.type === "toolCall") && output.stopReason === "stop") { + output.stopReason = "toolUse"; + } + }; for await (const event of openaiStream) { if (event.type === "response.created") { @@ -491,38 +524,12 @@ export async function processResponsesStream( currentBlock = null; stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output }); } - } else if (event.type === "response.completed") { - const response = event.response; - if (response?.id) { - output.responseId = response.id; - } - if (response?.usage) { - const cachedTokens = response.usage.input_tokens_details?.cached_tokens || 0; - output.usage = { - // OpenAI includes cached tokens in input_tokens, so subtract to get non-cached input - input: (response.usage.input_tokens || 0) - cachedTokens, - output: response.usage.output_tokens || 0, - cacheRead: cachedTokens, - cacheWrite: 0, - totalTokens: response.usage.total_tokens || 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }; - } - calculateCost(model, output.usage); - if (options?.applyServiceTierPricing) { - const serviceTier = options.resolveServiceTier - ? options.resolveServiceTier(response?.service_tier, options.serviceTier) - : (response?.service_tier ?? options.serviceTier); - options.applyServiceTierPricing(output.usage, serviceTier); - } - // Map status to stop reason - output.stopReason = mapStopReason(response?.status); - if (output.content.some((b) => b.type === "toolCall") && output.stopReason === "stop") { - output.stopReason = "toolUse"; - } + } else if (event.type === "response.completed" || event.type === "response.incomplete") { + finalizeResponse(event.response); } else if (event.type === "error") { throw new Error(`Error Code ${event.code}: ${event.message}` || "Unknown error"); } else if (event.type === "response.failed") { + sawTerminalResponseEvent = true; const error = event.response?.error; const details = event.response?.incomplete_details; const msg = error @@ -533,6 +540,9 @@ export async function processResponsesStream( throw new Error(msg); } } + if (!sawTerminalResponseEvent) { + throw new Error("OpenAI Responses stream ended before a terminal response event"); + } } function mapStopReason(status: OpenAI.Responses.ResponseStatus | undefined): StopReason { diff --git a/packages/ai/test/openai-responses-partial-json-cleanup.test.ts b/packages/ai/test/openai-responses-partial-json-cleanup.test.ts index 26a6704a..e4f4de4d 100644 --- a/packages/ai/test/openai-responses-partial-json-cleanup.test.ts +++ b/packages/ai/test/openai-responses-partial-json-cleanup.test.ts @@ -57,6 +57,11 @@ async function* createFunctionCallEvents(argumentsJson: string): AsyncIterable { diff --git a/packages/ai/test/openai-responses-terminal-event.test.ts b/packages/ai/test/openai-responses-terminal-event.test.ts new file mode 100644 index 00000000..c37896a3 --- /dev/null +++ b/packages/ai/test/openai-responses-terminal-event.test.ts @@ -0,0 +1,233 @@ +import type { ResponseStreamEvent } from "openai/resources/responses/responses.js"; +import { describe, expect, it, vi } from "vitest"; +import { stream as streamOpenAIResponses } from "../src/api/openai-responses.ts"; +import { processResponsesStream } from "../src/api/openai-responses-shared.ts"; +import type { AssistantMessage, AssistantMessageEvent, Context, Model } from "../src/types.ts"; +import { AssistantMessageEventStream } from "../src/utils/event-stream.ts"; + +vi.mock("openai", () => { + async function* createMockResponsesStream(): AsyncIterable { + yield { + type: "response.created", + sequence_number: 0, + response: { id: "resp_wrapper_early_eof" }, + } as ResponseStreamEvent; + yield { + type: "response.output_item.added", + sequence_number: 1, + output_index: 0, + item: { type: "reasoning", id: "rs_wrapper_early_eof", summary: [] }, + } as ResponseStreamEvent; + yield { + type: "response.reasoning_text.delta", + sequence_number: 2, + output_index: 0, + content_index: 0, + item_id: "rs_wrapper_early_eof", + delta: "partial reasoning before the wrapper stream ends", + } as ResponseStreamEvent; + } + + class FakeOpenAI { + responses = { + create: () => { + const responseStream = createMockResponsesStream(); + const promise = Promise.resolve(responseStream) as Promise> & { + withResponse: () => Promise<{ + data: AsyncIterable; + response: { status: number; headers: Headers }; + }>; + }; + promise.withResponse = async () => ({ + data: responseStream, + response: { status: 200, headers: new Headers() }, + }); + return promise; + }, + }; + } + + return { default: FakeOpenAI }; +}); + +function createModel(): Model<"openai-responses"> { + return { + id: "gpt-5-mini", + name: "GPT-5 Mini", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 400000, + maxTokens: 128000, + }; +} + +function createOutput(model: Model<"openai-responses">): AssistantMessage { + return { + role: "assistant", + content: [], + api: model.api, + provider: model.provider, + model: model.id, + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "stop", + timestamp: Date.now(), + }; +} + +async function* createEarlyEofEvents(): AsyncIterable { + yield { + type: "response.created", + sequence_number: 0, + response: { id: "resp_early_eof" }, + } as ResponseStreamEvent; + yield { + type: "response.output_item.added", + sequence_number: 1, + output_index: 0, + item: { type: "reasoning", id: "rs_early_eof", summary: [] }, + } as ResponseStreamEvent; + yield { + type: "response.reasoning_text.delta", + sequence_number: 2, + output_index: 0, + content_index: 0, + item_id: "rs_early_eof", + delta: "partial reasoning before the stream ends", + } as ResponseStreamEvent; +} + +async function* createCompletedEvents(): AsyncIterable { + yield { + type: "response.completed", + sequence_number: 0, + response: { + id: "resp_completed", + status: "completed", + usage: { + input_tokens: 20, + output_tokens: 7, + total_tokens: 27, + input_tokens_details: { cached_tokens: 2 }, + }, + }, + } as ResponseStreamEvent; +} + +async function* createIncompleteEvents(): AsyncIterable { + yield { + type: "response.incomplete", + sequence_number: 0, + response: { + id: "resp_incomplete", + status: "incomplete", + usage: { + input_tokens: 30, + output_tokens: 12, + total_tokens: 42, + input_tokens_details: { cached_tokens: 5 }, + }, + }, + } as ResponseStreamEvent; +} + +async function* createFailedEvents(): AsyncIterable { + yield { + type: "response.failed", + sequence_number: 0, + response: { + id: "resp_failed", + status: "failed", + error: { code: "server_error", message: "boom" }, + }, + } as ResponseStreamEvent; +} + +describe("OpenAI Responses terminal event handling", () => { + it("rejects streams that end before a terminal response event", async () => { + const model = createModel(); + const output = createOutput(model); + const stream = new AssistantMessageEventStream(); + + await expect(processResponsesStream(createEarlyEofEvents(), output, stream, model)).rejects.toThrow( + "OpenAI Responses stream ended before a terminal response event", + ); + }); + + it("emits an error final result when the wrapper stream ends before a terminal response event", async () => { + const model = createModel(); + const context: Context = { + systemPrompt: "", + messages: [{ role: "user", content: [{ type: "text", text: "hi" }], timestamp: 0 }], + tools: [], + }; + const stream = streamOpenAIResponses(model, context, { apiKey: "test" }); + const events: AssistantMessageEvent[] = []; + + for await (const event of stream) { + events.push(event); + } + + const result = await stream.result(); + const lastEvent = events.at(-1); + expect(lastEvent?.type).toBe("error"); + expect(result.stopReason).toBe("error"); + expect(result.errorMessage).toBe("OpenAI Responses stream ended before a terminal response event"); + }); + + it("finalizes completed terminal events as stop", async () => { + const model = createModel(); + const output = createOutput(model); + const stream = new AssistantMessageEventStream(); + + await processResponsesStream(createCompletedEvents(), output, stream, model); + + expect(output.responseId).toBe("resp_completed"); + expect(output.stopReason).toBe("stop"); + expect(output.usage).toMatchObject({ + input: 18, + output: 7, + cacheRead: 2, + cacheWrite: 0, + totalTokens: 27, + }); + }); + + it("finalizes incomplete terminal events as length stops", async () => { + const model = createModel(); + const output = createOutput(model); + const stream = new AssistantMessageEventStream(); + + await processResponsesStream(createIncompleteEvents(), output, stream, model); + + expect(output.responseId).toBe("resp_incomplete"); + expect(output.stopReason).toBe("length"); + expect(output.usage).toMatchObject({ + input: 25, + output: 12, + cacheRead: 5, + cacheWrite: 0, + totalTokens: 42, + }); + }); + + it("rejects failed terminal events with the provider error", async () => { + const model = createModel(); + const output = createOutput(model); + const stream = new AssistantMessageEventStream(); + + await expect(processResponsesStream(createFailedEvents(), output, stream, model)).rejects.toThrow( + "server_error: boom", + ); + }); +}); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index b71110ca..324822ad 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -14,6 +14,7 @@ ### Fixed +- Fixed context usage and compaction estimates to ignore malformed all-zero assistant usage after truncated responses ([#5526](https://github.com/earendil-works/pi/pull/5526) by [@dmmulroy](https://github.com/dmmulroy)). - Fixed `pi --resume` to load user package themes and resolve automatic light/dark theme settings. - Fixed `models.json` custom providers so stored credentials can satisfy auth without a redundant provider-level `apiKey` ([#5953](https://github.com/earendil-works/pi/issues/5953)). diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 26022f63..65a6ebd8 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -1874,10 +1874,12 @@ export class AgentSession { } // Case 2: Threshold - context is getting large - // For error messages (no usage data), estimate from last successful response. - // This ensures sessions that hit persistent API errors (e.g. 529) can still compact. + // For error messages or all-zero usage messages, estimate from the last valid response. + // This ensures sessions that hit persistent API errors (e.g. 529) or malformed zero-usage + // responses can still compact and do not reset context accounting. let contextTokens: number; - if (assistantMessage.stopReason === "error") { + const directContextTokens = assistantMessage.usage ? calculateContextTokens(assistantMessage.usage) : 0; + if (assistantMessage.stopReason === "error" || directContextTokens === 0) { const messages = this.agent.state.messages; const estimate = estimateContextTokens(messages); if (estimate.lastUsageIndex === null) return false; // No usage data at all @@ -1894,7 +1896,7 @@ export class AgentSession { } contextTokens = estimate.tokens; } else { - contextTokens = calculateContextTokens(assistantMessage.usage); + contextTokens = directContextTokens; } if (shouldCompact(contextTokens, contextWindow, settings)) { return await this._runAutoCompaction("threshold", false); @@ -3011,8 +3013,8 @@ export class AgentSession { const contextTokens = calculateContextTokens(assistant.usage); if (contextTokens > 0) { hasPostCompactionUsage = true; + break; } - break; } } } diff --git a/packages/coding-agent/src/core/compaction/compaction.ts b/packages/coding-agent/src/core/compaction/compaction.ts index 83b0db57..f369601c 100644 --- a/packages/coding-agent/src/core/compaction/compaction.ts +++ b/packages/coding-agent/src/core/compaction/compaction.ts @@ -139,12 +139,17 @@ export function calculateContextTokens(usage: Usage): number { /** * Get usage from an assistant message if available. - * Skips aborted and error messages as they don't have valid usage data. + * Skips aborted, error, and all-zero usage messages as they don't have valid usage data. */ function getAssistantUsage(msg: AgentMessage): Usage | undefined { if (msg.role === "assistant" && "usage" in msg) { const assistantMsg = msg as AssistantMessage; - if (assistantMsg.stopReason !== "aborted" && assistantMsg.stopReason !== "error" && assistantMsg.usage) { + if ( + assistantMsg.stopReason !== "aborted" && + assistantMsg.stopReason !== "error" && + assistantMsg.usage && + calculateContextTokens(assistantMsg.usage) > 0 + ) { return assistantMsg.usage; } } @@ -152,7 +157,7 @@ function getAssistantUsage(msg: AgentMessage): Usage | undefined { } /** - * Find the last non-aborted assistant message usage from session entries. + * Find the last valid assistant message usage from session entries. */ export function getLastAssistantUsage(entries: SessionEntry[]): Usage | undefined { for (let i = entries.length - 1; i >= 0; i--) { diff --git a/packages/coding-agent/test/agent-session-stats.test.ts b/packages/coding-agent/test/agent-session-stats.test.ts index 9ba30a94..7e2a5f3f 100644 --- a/packages/coding-agent/test/agent-session-stats.test.ts +++ b/packages/coding-agent/test/agent-session-stats.test.ts @@ -140,4 +140,28 @@ describe("AgentSession.getSessionStats", () => { session.dispose(); } }); + + it("ignores zero-usage messages when checking for post-compaction context usage", () => { + const { session, sessionManager } = createSession(); + + try { + sessionManager.appendMessage(createUserMessage("first", 1)); + sessionManager.appendMessage(createAssistantMessage("response1", 180_000, 2)); + const keptUserId = sessionManager.appendMessage(createUserMessage("second", 3)); + sessionManager.appendMessage(createAssistantMessage("response2", 195_000, 4)); + sessionManager.appendCompaction("summary", keptUserId, 195_000); + sessionManager.appendMessage(createUserMessage("third", 5)); + sessionManager.appendMessage(createAssistantMessage("response3", 25_000, 6)); + sessionManager.appendMessage(createUserMessage("continue", 7)); + sessionManager.appendMessage(createAssistantMessage("partial", 0, 8)); + syncAgentMessages(session, sessionManager); + + const stats = session.getSessionStats(); + expect(stats.contextUsage).toBeDefined(); + expect(stats.contextUsage?.tokens).not.toBeNull(); + expect(stats.contextUsage?.tokens ?? 0).toBeGreaterThan(25_000); + } finally { + session.dispose(); + } + }); }); diff --git a/packages/coding-agent/test/compaction.test.ts b/packages/coding-agent/test/compaction.test.ts index a245a3fc..ab09c3bf 100644 --- a/packages/coding-agent/test/compaction.test.ts +++ b/packages/coding-agent/test/compaction.test.ts @@ -9,6 +9,7 @@ import { calculateContextTokens, compact, DEFAULT_COMPACTION_SETTINGS, + estimateContextTokens, findCutPoint, getLastAssistantUsage, prepareCompaction, @@ -217,12 +218,43 @@ describe("getLastAssistantUsage", () => { expect(usage!.input).toBe(100); }); + it("should skip all-zero assistant usage", () => { + const entries: SessionEntry[] = [ + createMessageEntry(createUserMessage("Hello")), + createMessageEntry(createAssistantMessage("Hi", createMockUsage(100, 50))), + createMessageEntry(createUserMessage("continue")), + createMessageEntry(createAssistantMessage("Partial", createMockUsage(0, 0))), + ]; + + const usage = getLastAssistantUsage(entries); + expect(usage).not.toBeNull(); + expect(usage!.input).toBe(100); + }); + it("should return undefined if no assistant messages", () => { const entries: SessionEntry[] = [createMessageEntry(createUserMessage("Hello"))]; expect(getLastAssistantUsage(entries)).toBeUndefined(); }); }); +describe("estimateContextTokens", () => { + it("uses the last non-zero assistant usage as the context anchor", () => { + const messages: AgentMessage[] = [ + createUserMessage("Hello"), + createAssistantMessage("Hi", createMockUsage(100, 50)), + createUserMessage("continue"), + createAssistantMessage("Partial thinking", createMockUsage(0, 0)), + ]; + + const estimate = estimateContextTokens(messages); + + expect(estimate.usageTokens).toBe(150); + expect(estimate.lastUsageIndex).toBe(1); + expect(estimate.trailingTokens).toBeGreaterThan(0); + expect(estimate.tokens).toBe(150 + estimate.trailingTokens); + }); +}); + describe("shouldCompact", () => { it("should return true when context exceeds threshold", () => { const settings: CompactionSettings = { From 2be6e6702854d0c30976085dd4ef538f987c89ae Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 23 Jun 2026 16:40:46 +0200 Subject: [PATCH 098/122] docs(ai): document bundling behavior --- packages/ai/README.md | 65 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/packages/ai/README.md b/packages/ai/README.md index f81f6190..d600f648 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -44,6 +44,7 @@ Unified LLM API with provider collections, automatic auth resolution, token and - [Cross-Provider Handoffs](#cross-provider-handoffs) - [Context Serialization](#context-serialization) - [Browser Usage](#browser-usage) +- [Bundling and Tree Shaking](#bundling-and-tree-shaking) - [OAuth Providers](#oauth-providers) - [Vertex AI](#vertex-ai) - [CLI Login](#cli-login) @@ -95,7 +96,7 @@ TypeBox exports are re-exported from `@earendil-works/pi-ai`: `Type`, `Static`, ## Quick Start -You build a `Models` collection of providers and stream through it. The quickest start registers every built-in provider; apps that care about bundle size register individual providers instead (see [Provider Factories](#provider-factories)). Either way, provider SDKs load lazily on first request. +You build a `Models` collection of providers and stream through it. The quickest start registers every built-in provider; apps that care about bundle size register individual providers instead (see [Provider Factories](#provider-factories) and [Bundling and Tree Shaking](#bundling-and-tree-shaking)). ```typescript import { Type, type Context, type Tool } from '@earendil-works/pi-ai'; @@ -245,7 +246,7 @@ models.setProvider(anthropicProvider()); models.setProvider(openrouterProvider()); ``` -Provider SDKs (`@anthropic-ai/sdk`, `openai`, `@google/genai`, AWS) are **not** imported by registering a provider — they load lazily on the first request to a model of that API. +Provider factories import their model catalog and a lazy API wrapper. They do not import other providers. With bundler code splitting, SDK implementations (`@anthropic-ai/sdk`, `openai`, `@google/genai`, etc.) stay in lazy chunks loaded on the first request to a model of that API. ### All Built-in Providers @@ -257,7 +258,7 @@ import { builtinModels } from '@earendil-works/pi-ai/providers/all'; const models = builtinModels(); // a Models collection with every built-in provider registered ``` -This imports all catalogs (it is the heavy, explicit entrypoint) but still no SDKs. `builtinModels()` accepts the same options as `createModels()` (`credentials`, `authContext`); `builtinProviders()` returns the provider array if you want to register them on your own collection. +This imports all catalogs and every built-in provider factory. It is the heavy, explicit entrypoint. `builtinModels()` accepts the same options as `createModels()` (`credentials`, `authContext`); `builtinProviders()` returns the provider array if you want to register them on your own collection. ### Querying Models @@ -1041,7 +1042,7 @@ Built-in API implementations live under `./api/`: | `mistral-conversations` | `MistralOptions` | | `bedrock-converse-stream` | `BedrockOptions` | -Importing an implementation module loads its SDK. The `./api/.lazy` wrappers (used by the provider factories) defer that load to the first request. Legacy subpaths from older releases (`./anthropic`, `./google`, `./mistral`, `./openai-completions`, ...) still resolve to the corresponding API implementation modules. +Importing an implementation module loads its SDK. The `./api/.lazy` wrappers (used by the provider factories) defer that load to the first request when the runtime or bundler supports dynamic import chunking. Legacy raw API subpaths from older releases (`./anthropic`, `./google`, `./mistral`, `./openai-completions`, ...) were removed; use `@earendil-works/pi-ai/api/`. ### OpenAI Compatibility Settings @@ -1271,6 +1272,60 @@ Browser compatibility notes: - OAuth login flows are Node-only. They are lazy-loaded behind bundler-opaque imports, so registering an OAuth-capable provider does not pull Node-only code into a browser bundle — only actually logging in would. - Use a server-side proxy or backend service if you need Bedrock or OAuth-based auth from a web app. +## Bundling and Tree Shaking + +For small bundles, import only the providers you need: + +```typescript +import { createModels } from '@earendil-works/pi-ai'; +import { openaiProvider } from '@earendil-works/pi-ai/providers/openai'; + +const models = createModels(); +models.setProvider(openaiProvider()); +``` + +Rules: + +- `@earendil-works/pi-ai` is the core entrypoint and does not import built-in catalogs, provider factories, or SDK implementations. +- `@earendil-works/pi-ai/providers/` imports that provider's catalog and lazy API wrapper only. +- `@earendil-works/pi-ai/providers/all` imports every built-in provider factory and all catalogs. Use it only when you want the full built-in set. +- With code splitting, provider SDKs stay in lazy chunks and load on first request. +- Without code splitting, bundlers fold reachable lazy API implementations into the single bundle. A single-provider bundle then includes that provider's SDK; `providers/all` includes all statically visible SDKs. Bedrock is the exception: its AWS SDK implementation is loaded through a bundler-opaque Node-only import. +- Importing `@earendil-works/pi-ai/api/` directly loads that API implementation and its SDK immediately. + +Avoid `@earendil-works/pi-ai/compat` in new bundled apps; it preserves the old global API and imports the full built-in catalog surface. + +For single-file Node ESM bundles, some SDK dependencies may still use dynamic CommonJS `require()` internally. If you see errors such as `Dynamic require of "child_process" is not supported`, add a Node `require` shim to the bundle. With esbuild: + +```bash +esbuild app.js --bundle --platform=node --format=esm \ + --banner:js='import { createRequire } from "module";const require = createRequire(import.meta.url);' \ + --outfile=app.bundle.js +``` + +This is only for Node bundles; it is not a browser or Cloudflare Workers workaround. + +Bedrock is Node-only. Add it like any other provider: + +```typescript +import { createModels } from '@earendil-works/pi-ai'; +import { amazonBedrockProvider } from '@earendil-works/pi-ai/providers/amazon-bedrock'; + +const models = createModels(); +models.setProvider(amazonBedrockProvider()); +``` + +In normal Node package usage and code-split bundles, Bedrock loads its AWS SDK implementation lazily. For a standalone single-file bundle that must include Bedrock support, register the implementation module explicitly: + +```typescript +import { setBedrockProviderModule } from '@earendil-works/pi-ai/api/bedrock-converse-stream.lazy'; +import { bedrockProviderModule } from '@earendil-works/pi-ai/bedrock-provider'; + +setBedrockProviderModule(bedrockProviderModule); +``` + +That explicit override bundles the AWS SDK. Without it, Bedrock's opaque runtime import expects the package's Bedrock implementation file to be available at runtime. + ### Provider-Scoped Environment Overrides Pass `env` in stream options to scope provider configuration to a request. Values in `env` are used before process environment variables for provider auth and configuration such as Cloudflare account IDs, Azure OpenAI settings, Vertex project/location, Bedrock settings, `PI_CACHE_RETENTION`, and `HTTP_PROXY`/`HTTPS_PROXY`. @@ -1399,7 +1454,7 @@ Compat is a strict superset of the root entrypoint, so a file can switch its imp | `stream(model, ctx, opts)` (env-key injection) | `models.stream(model, ctx, opts)` (provider auth resolution) | | `registerApiProvider({ api, stream, streamSimple })` | `createProvider({ id, auth, models, api })` + `models.setProvider()` | | `getEnvApiKey('openai')` | `await models.getAuth(model)` | -| `streamAnthropic(model, ctx, opts)` | `stream` from `@earendil-works/pi-ai/anthropic`, or a provider in a collection | +| `streamAnthropic(model, ctx, opts)` | `stream` from `@earendil-works/pi-ai/api/anthropic-messages`, or a provider in a collection | | `registerFauxProvider()` | `fauxProvider()` + `models.setProvider()` | ## Development From 192fcccd2d897ad069a7e069c59e8ba626987dbf Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 23 Jun 2026 18:20:15 +0200 Subject: [PATCH 099/122] fix(coding-agent): hint when extensions fail to load --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/main.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 324822ad..2add6cb3 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -14,6 +14,7 @@ ### Fixed +- Fixed extension-related crash and startup-failure reporting to suggest restarting with `pi -ne`. - Fixed context usage and compaction estimates to ignore malformed all-zero assistant usage after truncated responses ([#5526](https://github.com/earendil-works/pi/pull/5526) by [@dmmulroy](https://github.com/dmmulroy)). - Fixed `pi --resume` to load user package themes and resolve automatic light/dark theme settings. - Fixed `models.json` custom providers so stored credentials can satisfy auth without a redundant provider-level `apiKey` ([#5953](https://github.com/earendil-works/pi/issues/5953)). diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 568d96d1..50a19bba 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -49,6 +49,8 @@ import { handleConfigCommand, handlePackageCommand } from "./package-manager-cli import { isLocalPath, normalizePath, resolvePath } from "./utils/paths.ts"; import { cleanupWindowsSelfUpdateQuarantine } from "./utils/windows-self-update.ts"; +const EXTENSION_LOAD_FAILURE_HINT = 'Hint: Start without extensions using "pi -ne".'; + /** * Read all content from piped stdin. * Returns undefined if stdin is a TTY (interactive terminal). @@ -774,6 +776,9 @@ export async function main(args: string[], options?: MainOptions) { time("resolveModelScope"); reportDiagnostics(runtime.diagnostics); if (runtime.diagnostics.some((diagnostic) => diagnostic.type === "error")) { + if (runtime.diagnostics.some((diagnostic) => diagnostic.message.includes("Failed to load extension"))) { + console.error(chalk.yellow(EXTENSION_LOAD_FAILURE_HINT)); + } process.exit(1); } time("createAgentSession"); From 526351d99a9014dd4347d1a4dadf112d39ef06c1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 23 Jun 2026 19:25:26 +0200 Subject: [PATCH 100/122] docs: audit unreleased changelogs --- packages/agent/CHANGELOG.md | 2 ++ packages/ai/CHANGELOG.md | 4 ++-- packages/coding-agent/CHANGELOG.md | 17 ++++++++++++----- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index edbed28a..e0e4e604 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -7,9 +7,11 @@ - `AgentHarnessOptions.models` is required and is the only auth path: the harness streams turns, compaction, and branch summarization through the provided `Models` instance (`models.streamSimple()`/`completeSimple()`), resolving auth through the providers. `AgentHarnessOptions.getApiKeyAndHeaders` is removed — apps that resolved keys per request now express that as provider auth (`ApiKeyAuth`/`OAuthAuth`) on the providers in the `Models` collection. Build one with `createModels()` + provider factories (or `builtinModels()` from `@earendil-works/pi-ai/providers/all`); tests use `fauxProvider()`. - `compact()`, `generateSummary()`, and `generateBranchSummary()` take a `Models` parameter and no longer accept explicit `apiKey`/`headers`. - `StreamFn` is defined structurally (`(model, context, options?) => AssistantMessageEventStream | Promise<...>`); `Models.streamSimple` satisfies it. +- Removed the `@earendil-works/pi-agent-core/base` selective-provider entrypoint; use the root package with an explicit `Models` instance instead. ### Fixed +- Fixed harness session names to normalize newline characters before storing labels ([#5999](https://github.com/earendil-works/pi/pull/5999) by [@haoqixu](https://github.com/haoqixu)). - Fixed harness compaction estimates to ignore malformed all-zero assistant usage after truncated responses ([#5526](https://github.com/earendil-works/pi/pull/5526) by [@dmmulroy](https://github.com/dmmulroy)). ## [0.79.10] - 2026-06-22 diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index cdcae32c..6d36bcc3 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -7,6 +7,7 @@ - The root entrypoint (`@earendil-works/pi-ai`) is now core-only and side-effect free. The old global API moved to the temporary `@earendil-works/pi-ai/compat` entrypoint, a strict superset of the root: switching a file's import path is the only migration step. Moved symbols include `stream`/`complete`/`streamSimple`/`completeSimple`, `getModel`/`getModels`/`getProviders` (now deprecated aliases of `getBuiltinModel`/`getBuiltinModels`/`getBuiltinProviders` from `@earendil-works/pi-ai/providers/all`), `registerApiProvider`/`unregisterApiProviders`/`resetApiProviders`/`getApiProvider`, `getEnvApiKey`/`findEnvKeys`, `setBedrockProviderModule`, the per-API lazy stream wrappers (`anthropicMessagesApi`, ...), and the image-generation API. - Renamed the `Provider` type to `ProviderId`. `Provider` now names the runtime provider interface (id, name, auth, model listing, stream behavior). - API implementation modules moved from `src/providers/` to `@earendil-works/pi-ai/api/*`, renamed by API id (`anthropic` -> `api/anthropic-messages`, `google` -> `api/google-generative-ai`, `mistral` -> `api/mistral-conversations`, `amazon-bedrock` -> `api/bedrock-converse-stream`), each exporting exactly `stream` and `streamSimple`. The old per-impl export names (`streamAnthropic`, `streamSimpleAnthropic`, ...) and legacy raw API subpaths (`./anthropic`, `./google`, `./openai-completions`, ...) are gone; import raw API implementations through `@earendil-works/pi-ai/api/*`. +- Removed the `@earendil-works/pi-ai/base` selective-provider entrypoint; use the root/core APIs with explicit `createModels()` collections and provider factories for isolated bundles. Migration guide: @@ -62,7 +63,6 @@ Migration guide: - OAuth flows (Anthropic, OpenAI Codex, GitHub Copilot) gained `OAuthAuth` adapters (`login`/`refresh`/`toAuth`) on unified `prompt()`/`notify()` login callbacks; Copilot's per-credential base URL is derived in `toAuth()`. - `fauxProvider()` returns a faux `Provider` for tests built on explicit `Models` collections. - Image generation mirrors the chat-side design: `createImagesModels()`/`ImagesProvider`/`createImagesProvider()` with sync model reads, explicit `refresh()`, provider-resolved auth, and never-rejecting `generateImages()`; `openrouterImagesProvider()` factory plus `builtinImagesProviders()`/`builtinImagesModels()` in `providers/all`. The `ImagesProvider` id type alias is renamed to `ImagesProviderId`; the old global image API stays on `/compat`. -- When Amazon Bedrock rejects an unsupported data retention mode, the error now links the AWS data retention documentation ([#5561](https://github.com/earendil-works/pi/pull/5561) by [@unexge](https://github.com/unexge)). - Provider auth results can carry provider-scoped environment values that `Models` and `ImagesModels` merge into API implementation options. ### Fixed @@ -70,9 +70,9 @@ Migration guide: - Fixed OpenAI Responses streams to fail when they end before a terminal response event and to treat `response.incomplete` as a length stop ([#5526](https://github.com/earendil-works/pi/pull/5526) by [@dmmulroy](https://github.com/dmmulroy)). - Fixed Amazon Bedrock endpoint resolution to honor scoped `AWS_PROFILE` values. - Fixed Cloudflare providers to require account/gateway configuration and route built-in `/compat` requests through provider auth. +- Fixed `/compat` API-key injection to honor request-scoped `env` values. - Fixed OpenAI Codex Responses WebSocket sessions to reconnect once when OpenAI's connection limit is reached before output starts ([#5973](https://github.com/earendil-works/pi/issues/5973)). - Fixed OpenCode Go GLM-5.2 metadata to expose `xhigh` reasoning and send `reasoning_effort: "max"` ([#5967](https://github.com/earendil-works/pi/issues/5967)). -- Fixed Claude Fable 5 thinking-off requests to omit Anthropic's unsupported `thinking.type: "disabled"` payload ([#5567](https://github.com/earendil-works/pi/pull/5567) by [@tmustier](https://github.com/tmustier)). ## [0.79.10] - 2026-06-22 diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 2add6cb3..d532cb19 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,17 +8,24 @@ - Renamed the displayed `zai` provider label to ZAI Coding Plan (Global) for clarity ([#5965](https://github.com/earendil-works/pi/issues/5965)). - pi-ai's old global API (`stream`/`complete`/`completeSimple`, `getModel`/`getModels`/`getProviders`, `registerApiProvider`, `getEnvApiKey`, ...) moved off the `@earendil-works/pi-ai` root entrypoint to `@earendil-works/pi-ai/compat`. Extensions are not affected at runtime: the extension loader resolves the pi-ai root to the compat entrypoint (a strict superset), so existing extensions keep working unchanged. Extension sources that typecheck against pi-ai's published types should switch those imports to `@earendil-works/pi-ai/compat` (or migrate to the new `createModels()`/provider-factory API). The compat entrypoint and the loader alias will be removed in a future release with a migration guide. -### Added - -- Added an experimental first-time setup flow behind `PI_EXPERIMENTAL=1` that asks for a dark/light theme choice (preselecting the detected appearance) and opt-in analytics data sharing on first launch with the default agent directory; opting in stores a `trackingId` in `settings.json`. - ### Fixed +- Fixed session names to normalize newline characters before storing or displaying labels ([#5999](https://github.com/earendil-works/pi/pull/5999) by [@haoqixu](https://github.com/haoqixu)). +- Fixed the session selector to order threaded session trees by the latest activity anywhere in each subtree ([#5784](https://github.com/earendil-works/pi/pull/5784) by [@Perlence](https://github.com/Perlence)). - Fixed extension-related crash and startup-failure reporting to suggest restarting with `pi -ne`. -- Fixed context usage and compaction estimates to ignore malformed all-zero assistant usage after truncated responses ([#5526](https://github.com/earendil-works/pi/pull/5526) by [@dmmulroy](https://github.com/dmmulroy)). +- Fixed inherited OpenAI Responses streams to fail before missing terminal events and fixed context usage and compaction estimates to ignore malformed all-zero assistant usage after truncated responses ([#5526](https://github.com/earendil-works/pi/pull/5526) by [@dmmulroy](https://github.com/dmmulroy)). +- Fixed inherited OpenAI Codex Responses WebSocket sessions to reconnect once when OpenAI's connection limit is reached before output starts ([#5973](https://github.com/earendil-works/pi/issues/5973)). +- Fixed inherited Amazon Bedrock endpoint resolution to honor scoped `AWS_PROFILE` values. +- Fixed inherited Cloudflare providers to require account/gateway configuration and route built-in compat calls through provider auth. +- Fixed provider-scoped auth environment values to reach inherited `Models`/`ImagesModels` API calls and compat API-key injection. +- Fixed inherited OpenCode Go GLM-5.2 metadata to expose `xhigh` reasoning and send the provider's max reasoning effort ([#5967](https://github.com/earendil-works/pi/issues/5967)). - Fixed `pi --resume` to load user package themes and resolve automatic light/dark theme settings. - Fixed `models.json` custom providers so stored credentials can satisfy auth without a redundant provider-level `apiKey` ([#5953](https://github.com/earendil-works/pi/issues/5953)). +### Removed + +- Removed inherited selective-provider `@earendil-works/pi-ai/base` and `@earendil-works/pi-agent-core/base` entrypoints; use the root packages with explicit `Models` provider factories instead. + ## [0.79.10] - 2026-06-22 ### New Features From f08e968c83d92bce5f5fd2f7f20ef37f8cf04a39 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 23 Jun 2026 19:30:32 +0200 Subject: [PATCH 101/122] Release v0.80.0 --- package-lock.json | 51 ++++++++++++++----- packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 +- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- .../ai/src/providers/openrouter.models.ts | 44 ++-------------- packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/gondolin/package-lock.json | 4 +- .../examples/extensions/gondolin/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/npm-shrinkwrap.json | 24 ++++----- packages/coding-agent/package.json | 8 +-- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- 20 files changed, 79 insertions(+), 90 deletions(-) diff --git a/package-lock.json b/package-lock.json index ba0eac39..46e18dd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5111,10 +5111,10 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.79.10", + "version": "0.80.0", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.10", + "@earendil-works/pi-ai": "^0.80.0", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -5129,6 +5129,31 @@ "node": ">=22.19.0" } }, + "packages/agent/node_modules/@earendil-works/pi-ai": { + "version": "0.79.10", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.10.tgz", + "integrity": "sha512-9jR23tOl0BIUdQMn70Gr72xYBpM7Xgl9Lyv7gAnU1USfkNRuYG/f/edLl+n/Dp/RafDW3JI4DF7y/GhgkORuew==", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.91.1", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@google/genai": "1.52.0", + "@mistralai/mistralai": "2.2.6", + "@opentelemetry/api": "1.9.0", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", + "openai": "6.26.0", + "partial-json": "0.1.7", + "typebox": "1.1.38" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + } + }, "packages/agent/node_modules/@types/node": { "version": "24.12.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", @@ -5463,7 +5488,7 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.79.10", + "version": "0.80.0", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -5769,12 +5794,12 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.10", + "version": "0.80.0", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.10", - "@earendil-works/pi-ai": "^0.79.10", - "@earendil-works/pi-tui": "^0.79.10", + "@earendil-works/pi-agent-core": "^0.80.0", + "@earendil-works/pi-ai": "^0.80.0", + "@earendil-works/pi-tui": "^0.80.0", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -5815,32 +5840,32 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.79.10", + "version": "0.80.0", "dependencies": { "@anthropic-ai/sdk": "0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.79.10" + "version": "0.80.0" }, "packages/coding-agent/examples/extensions/gondolin": { "name": "pi-extension-gondolin", - "version": "0.79.10", + "version": "0.80.0", "dependencies": { "@earendil-works/gondolin": "0.12.0" } }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.9.10", + "version": "1.10.0", "dependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.79.10", + "version": "0.80.0", "dependencies": { "ms": "2.1.3" }, @@ -6136,7 +6161,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.79.10", + "version": "0.80.0", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index e0e4e604..64f4f551 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.80.0] - 2026-06-23 ### Breaking Changes diff --git a/packages/agent/package.json b/packages/agent/package.json index 9322d7c6..96cffda9 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.79.10", + "version": "0.80.0", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -29,7 +29,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.79.10", + "@earendil-works/pi-ai": "^0.80.0", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 6d36bcc3..851071f0 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.80.0] - 2026-06-23 ### Breaking Changes diff --git a/packages/ai/package.json b/packages/ai/package.json index 432219da..15dcbbd7 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.79.10", + "version": "0.80.0", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/ai/src/providers/openrouter.models.ts b/packages/ai/src/providers/openrouter.models.ts index 15727161..53326347 100644 --- a/packages/ai/src/providers/openrouter.models.ts +++ b/packages/ai/src/providers/openrouter.models.ts @@ -768,24 +768,6 @@ export const OPENROUTER_MODELS = { contextWindow: 1048576, maxTokens: 384000, } satisfies Model<"openai-completions">, - "essentialai/rnj-1-instruct": { - id: "essentialai/rnj-1-instruct", - name: "EssentialAI: Rnj 1 Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 4096, - } satisfies Model<"openai-completions">, "google/gemini-2.5-flash": { id: "google/gemini-2.5-flash", name: "Google: Gemini 2.5 Flash", @@ -1445,13 +1427,13 @@ export const OPENROUTER_MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.25, - output: 1, - cacheRead: 0.05, + input: 0.24, + output: 0.96, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 204800, - maxTokens: 131072, + maxTokens: 196608, } satisfies Model<"openai-completions">, "minimax/minimax-m3": { id: "minimax/minimax-m3", @@ -4147,24 +4129,6 @@ export const OPENROUTER_MODELS = { contextWindow: 262144, maxTokens: 4096, } satisfies Model<"openai-completions">, - "thedrummer/rocinante-12b": { - id: "thedrummer/rocinante-12b", - name: "TheDrummer: Rocinante 12B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, - reasoning: false, - input: ["text"], - cost: { - input: 0.17, - output: 0.43, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 32768, - } satisfies Model<"openai-completions">, "thedrummer/unslopnemo-12b": { id: "thedrummer/unslopnemo-12b", name: "TheDrummer: UnslopNemo 12B", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d532cb19..31931d02 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.80.0] - 2026-06-23 ### Changed diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index c138594f..aecea6b5 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.79.10", + "version": "0.80.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.79.10", + "version": "0.80.0", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index 11dfb54a..fdb619f0 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.79.10", + "version": "0.80.0", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index a1d1808b..bdbab093 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.79.10", + "version": "0.80.0", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/gondolin/package-lock.json b/packages/coding-agent/examples/extensions/gondolin/package-lock.json index 7d7f5932..bc923056 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package-lock.json +++ b/packages/coding-agent/examples/extensions/gondolin/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-gondolin", - "version": "0.79.10", + "version": "0.80.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-gondolin", - "version": "0.79.10", + "version": "0.80.0", "dependencies": { "@earendil-works/gondolin": "0.12.0" } diff --git a/packages/coding-agent/examples/extensions/gondolin/package.json b/packages/coding-agent/examples/extensions/gondolin/package.json index e473cb1a..be9bc879 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package.json +++ b/packages/coding-agent/examples/extensions/gondolin/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-gondolin", "private": true, - "version": "0.79.10", + "version": "0.80.0", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 0558de01..32773aec 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.9.10", + "version": "1.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.9.10", + "version": "1.10.0", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index 5957ad4b..56acaaf2 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.9.10", + "version": "1.10.0", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index dac510c0..6c5ecda9 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.79.10", + "version": "0.80.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.79.10", + "version": "0.80.0", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index e23e480f..bb11dbbe 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.79.10", + "version": "0.80.0", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index 1e9fa157..2055ea91 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -1,17 +1,17 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.10", + "version": "0.80.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.10", + "version": "0.80.0", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.10", - "@earendil-works/pi-ai": "^0.79.10", - "@earendil-works/pi-tui": "^0.79.10", + "@earendil-works/pi-agent-core": "^0.80.0", + "@earendil-works/pi-ai": "^0.80.0", + "@earendil-works/pi-tui": "^0.80.0", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -474,11 +474,11 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.79.10", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.10.tgz", + "version": "0.80.0", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.80.0.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.10", + "@earendil-works/pi-ai": "^0.80.0", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -488,8 +488,8 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.79.10", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.10.tgz", + "version": "0.80.0", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.80.0.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -512,8 +512,8 @@ } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.79.10", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.10.tgz", + "version": "0.80.0", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.80.0.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 81489b28..e9aadca2 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.10", + "version": "0.80.0", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -36,9 +36,9 @@ "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.10", - "@earendil-works/pi-ai": "^0.79.10", - "@earendil-works/pi-tui": "^0.79.10", + "@earendil-works/pi-agent-core": "^0.80.0", + "@earendil-works/pi-ai": "^0.80.0", + "@earendil-works/pi-tui": "^0.80.0", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index a3cf70ef..bffb667f 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.80.0] - 2026-06-23 ### Changed diff --git a/packages/tui/package.json b/packages/tui/package.json index c5d0e480..91ae4fc1 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.79.10", + "version": "0.80.0", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", From 86528dd9ee8ada3a3e0e9d104f70e3bd6f74c184 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 23 Jun 2026 19:30:35 +0200 Subject: [PATCH 102/122] Add [Unreleased] section for next cycle --- packages/agent/CHANGELOG.md | 2 ++ packages/ai/CHANGELOG.md | 2 ++ packages/coding-agent/CHANGELOG.md | 2 ++ packages/tui/CHANGELOG.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 64f4f551..2cc8aa86 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.80.0] - 2026-06-23 ### Breaking Changes diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 851071f0..ab53da79 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.80.0] - 2026-06-23 ### Breaking Changes diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 31931d02..70cf4859 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.80.0] - 2026-06-23 ### Changed diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index bffb667f..ec278ccd 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.80.0] - 2026-06-23 ### Changed From 828493b37c2dd14456356650eb382da67556ce14 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 23 Jun 2026 19:57:29 +0200 Subject: [PATCH 103/122] fix(ai): unblock release provider tests --- package-lock.json | 25 ------------------- packages/ai/CHANGELOG.md | 6 +++++ packages/ai/scripts/generate-models.ts | 3 ++- packages/ai/src/api/anthropic-messages.ts | 13 +++++++--- .../ai/src/api/bedrock-converse-stream.ts | 2 +- packages/ai/src/providers/together.models.ts | 2 +- packages/coding-agent/CHANGELOG.md | 6 +++++ 7 files changed, 25 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 46e18dd1..42b42081 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5129,31 +5129,6 @@ "node": ">=22.19.0" } }, - "packages/agent/node_modules/@earendil-works/pi-ai": { - "version": "0.79.10", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.10.tgz", - "integrity": "sha512-9jR23tOl0BIUdQMn70Gr72xYBpM7Xgl9Lyv7gAnU1USfkNRuYG/f/edLl+n/Dp/RafDW3JI4DF7y/GhgkORuew==", - "license": "MIT", - "dependencies": { - "@anthropic-ai/sdk": "0.91.1", - "@aws-sdk/client-bedrock-runtime": "3.1048.0", - "@google/genai": "1.52.0", - "@mistralai/mistralai": "2.2.6", - "@opentelemetry/api": "1.9.0", - "@smithy/node-http-handler": "4.7.3", - "http-proxy-agent": "7.0.2", - "https-proxy-agent": "7.0.6", - "openai": "6.26.0", - "partial-json": "0.1.7", - "typebox": "1.1.38" - }, - "bin": { - "pi-ai": "dist/cli.js" - }, - "engines": { - "node": ">=22.19.0" - } - }, "packages/agent/node_modules/@types/node": { "version": "24.12.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index ab53da79..04e1623a 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,12 @@ ## [Unreleased] +### Fixed + +- Fixed a regression in Amazon Bedrock scoped `AWS_PROFILE` endpoint resolution for built-in inference profile endpoints. +- Fixed Fireworks Anthropic-compatible requests to apply session-affinity and unsupported tool-field defaults for custom Fireworks models. +- Fixed Together MiniMax M2.7 metadata to avoid unsupported Together reasoning toggles. + ## [0.80.0] - 2026-06-23 ### Breaking Changes diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 07024585..3b0677b6 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -324,6 +324,7 @@ function detectOpenAICompletionsCompat(model: Model<"openai-completions">): Open const isCloudflareAiGateway = provider === "cloudflare-ai-gateway" || baseUrl.includes("gateway.ai.cloudflare.com"); const isNvidia = provider === "nvidia" || baseUrl.includes("integrate.api.nvidia.com"); const isAntLing = provider === "ant-ling" || baseUrl.includes("api.ant-ling.com"); + const isTogetherReasoningOnly = isTogether && TOGETHER_REASONING_ONLY_MODELS.has(model.id); const isNonStandard = isNvidia || @@ -366,7 +367,7 @@ function detectOpenAICompletionsCompat(model: Model<"openai-completions">): Open ? "deepseek" : isZai ? "zai" - : isTogether + : isTogether && !isTogetherReasoningOnly ? "together" : isAntLing ? "ant-ling" diff --git a/packages/ai/src/api/anthropic-messages.ts b/packages/ai/src/api/anthropic-messages.ts index 03bbc26c..3b94d296 100644 --- a/packages/ai/src/api/anthropic-messages.ts +++ b/packages/ai/src/api/anthropic-messages.ts @@ -170,11 +170,16 @@ const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14"; function getAnthropicCompat( model: Model<"anthropic-messages">, ): Required> { + const isFireworks = model.provider === "fireworks"; + const isCloudflareAiGatewayAnthropic = + model.provider === "cloudflare-ai-gateway" && model.baseUrl.includes("anthropic"); + return { - supportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? true, - supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? true, - sendSessionAffinityHeaders: model.compat?.sendSessionAffinityHeaders ?? false, - supportsCacheControlOnTools: model.compat?.supportsCacheControlOnTools ?? true, + supportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? !isFireworks, + supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? !isFireworks, + sendSessionAffinityHeaders: + model.compat?.sendSessionAffinityHeaders ?? (isFireworks || isCloudflareAiGatewayAnthropic), + supportsCacheControlOnTools: model.compat?.supportsCacheControlOnTools ?? !isFireworks, supportsTemperature: model.compat?.supportsTemperature ?? true, allowEmptySignature: model.compat?.allowEmptySignature ?? false, }; diff --git a/packages/ai/src/api/bedrock-converse-stream.ts b/packages/ai/src/api/bedrock-converse-stream.ts index 899bacd9..68af206d 100644 --- a/packages/ai/src/api/bedrock-converse-stream.ts +++ b/packages/ai/src/api/bedrock-converse-stream.ts @@ -128,7 +128,7 @@ export const stream: StreamFunction<"bedrock-converse-stream", BedrockOptions> = profile: options.profile || getProviderEnvValue("AWS_PROFILE", options.env), }; const configuredRegion = getConfiguredBedrockRegion(options); - const hasAmbientConfiguredProfile = Boolean(getProviderEnvValue("AWS_PROFILE", options.env)); + const hasAmbientConfiguredProfile = Boolean(getProviderEnvValue("AWS_PROFILE")); const endpointRegion = getStandardBedrockEndpointRegion(model.baseUrl); const useExplicitEndpoint = shouldUseExplicitBedrockEndpoint( model.baseUrl, diff --git a/packages/ai/src/providers/together.models.ts b/packages/ai/src/providers/together.models.ts index 5c0104e0..6a261484 100644 --- a/packages/ai/src/providers/together.models.ts +++ b/packages/ai/src/providers/together.models.ts @@ -10,7 +10,7 @@ export const TOGETHER_MODELS = { api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, reasoning: true, thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null}, input: ["text"], diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 70cf4859..1bbfbc0e 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,12 @@ ## [Unreleased] +### Fixed + +- Fixed inherited Amazon Bedrock scoped `AWS_PROFILE` endpoint resolution for built-in inference profile endpoints. +- Fixed inherited Fireworks Anthropic-compatible requests to apply session-affinity and unsupported tool-field defaults for custom Fireworks models. +- Fixed inherited Together MiniMax M2.7 metadata to avoid unsupported Together reasoning toggles. + ## [0.80.0] - 2026-06-23 ### Changed From 1c4a9ba7c80c2b0d7f3ab57d38ed4ba1f671cb90 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 23 Jun 2026 20:04:09 +0200 Subject: [PATCH 104/122] Release v0.80.1 --- package-lock.json | 26 ++++++------- packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 +- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- .../ai/src/providers/openrouter.models.ts | 38 +++++++++---------- packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/gondolin/package-lock.json | 4 +- .../examples/extensions/gondolin/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/npm-shrinkwrap.json | 24 ++++++------ packages/coding-agent/package.json | 8 ++-- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- 20 files changed, 69 insertions(+), 69 deletions(-) diff --git a/package-lock.json b/package-lock.json index 42b42081..9fb93a6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5111,10 +5111,10 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.80.0", + "version": "0.80.1", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.80.0", + "@earendil-works/pi-ai": "^0.80.1", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -5463,7 +5463,7 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.80.0", + "version": "0.80.1", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -5769,12 +5769,12 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.80.0", + "version": "0.80.1", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.80.0", - "@earendil-works/pi-ai": "^0.80.0", - "@earendil-works/pi-tui": "^0.80.0", + "@earendil-works/pi-agent-core": "^0.80.1", + "@earendil-works/pi-ai": "^0.80.1", + "@earendil-works/pi-tui": "^0.80.1", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -5815,32 +5815,32 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.80.0", + "version": "0.80.1", "dependencies": { "@anthropic-ai/sdk": "0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.80.0" + "version": "0.80.1" }, "packages/coding-agent/examples/extensions/gondolin": { "name": "pi-extension-gondolin", - "version": "0.80.0", + "version": "0.80.1", "dependencies": { "@earendil-works/gondolin": "0.12.0" } }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.10.0", + "version": "1.10.1", "dependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.80.0", + "version": "0.80.1", "dependencies": { "ms": "2.1.3" }, @@ -6136,7 +6136,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.80.0", + "version": "0.80.1", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 2cc8aa86..f7e308c0 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.80.1] - 2026-06-23 ## [0.80.0] - 2026-06-23 diff --git a/packages/agent/package.json b/packages/agent/package.json index 96cffda9..b58662d1 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.80.0", + "version": "0.80.1", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -29,7 +29,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.80.0", + "@earendil-works/pi-ai": "^0.80.1", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 04e1623a..649d7351 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.80.1] - 2026-06-23 ### Fixed diff --git a/packages/ai/package.json b/packages/ai/package.json index 15dcbbd7..778f8fe5 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.80.0", + "version": "0.80.1", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/ai/src/providers/openrouter.models.ts b/packages/ai/src/providers/openrouter.models.ts index 53326347..434f5696 100644 --- a/packages/ai/src/providers/openrouter.models.ts +++ b/packages/ai/src/providers/openrouter.models.ts @@ -909,7 +909,7 @@ export const OPENROUTER_MODELS = { cacheRead: 0.2, cacheWrite: 0.375, }, - contextWindow: 65536, + contextWindow: 131072, maxTokens: 32768, } satisfies Model<"openai-completions">, "google/gemini-3.1-flash-lite": { @@ -1108,7 +1108,7 @@ export const OPENROUTER_MODELS = { cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 8192, + maxTokens: 32768, } satisfies Model<"openai-completions">, "ibm-granite/granite-4.1-8b": { id: "ibm-granite/granite-4.1-8b", @@ -1216,8 +1216,8 @@ export const OPENROUTER_MODELS = { cacheRead: 0.06, cacheWrite: 0, }, - contextWindow: 256000, - maxTokens: 80000, + contextWindow: 262144, + maxTokens: 144000, } satisfies Model<"openai-completions">, "liquid/lfm-2.5-1.2b-thinking:free": { id: "liquid/lfm-2.5-1.2b-thinking:free", @@ -1451,7 +1451,7 @@ export const OPENROUTER_MODELS = { cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 512000, + maxTokens: 4096, } satisfies Model<"openai-completions">, "mistralai/codestral-2508": { id: "mistralai/codestral-2508", @@ -1807,7 +1807,7 @@ export const OPENROUTER_MODELS = { cost: { input: 0.6, output: 2.5, - cacheRead: 0, + cacheRead: 0.6, cacheWrite: 0, }, contextWindow: 262144, @@ -2189,7 +2189,7 @@ export const OPENROUTER_MODELS = { cacheWrite: 0, }, contextWindow: 1047576, - maxTokens: 32768, + maxTokens: 4096, } satisfies Model<"openai-completions">, "openai/gpt-4.1-nano": { id: "openai/gpt-4.1-nano", @@ -2221,7 +2221,7 @@ export const OPENROUTER_MODELS = { cost: { input: 2.5, output: 10, - cacheRead: 0, + cacheRead: 1.25, cacheWrite: 0, }, contextWindow: 128000, @@ -2365,11 +2365,11 @@ export const OPENROUTER_MODELS = { cost: { input: 0.25, output: 2, - cacheRead: 0.025, + cacheRead: 0.03, cacheWrite: 0, }, contextWindow: 400000, - maxTokens: 128000, + maxTokens: 4096, } satisfies Model<"openai-completions">, "openai/gpt-5-nano": { id: "openai/gpt-5-nano", @@ -2437,11 +2437,11 @@ export const OPENROUTER_MODELS = { cost: { input: 1.25, output: 10, - cacheRead: 0.13, + cacheRead: 0.125, cacheWrite: 0, }, contextWindow: 128000, - maxTokens: 32000, + maxTokens: 16384, } satisfies Model<"openai-completions">, "openai/gpt-5.1-codex": { id: "openai/gpt-5.1-codex", @@ -2533,7 +2533,7 @@ export const OPENROUTER_MODELS = { cacheWrite: 0, }, contextWindow: 128000, - maxTokens: 16384, + maxTokens: 32000, } satisfies Model<"openai-completions">, "openai/gpt-5.2-codex": { id: "openai/gpt-5.2-codex", @@ -3799,11 +3799,11 @@ export const OPENROUTER_MODELS = { cost: { input: 0.14, output: 1, - cacheRead: 0, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262144, + maxTokens: 81920, } satisfies Model<"openai-completions">, "qwen/qwen3.5-397b-a17b": { id: "qwen/qwen3.5-397b-a17b", @@ -3838,8 +3838,8 @@ export const OPENROUTER_MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 262144, - maxTokens: 262144, + contextWindow: 256000, + maxTokens: 32768, } satisfies Model<"openai-completions">, "qwen/qwen3.5-flash-02-23": { id: "qwen/qwen3.5-flash-02-23", @@ -4429,11 +4429,11 @@ export const OPENROUTER_MODELS = { cost: { input: 0.98, output: 3.08, - cacheRead: 0.49, + cacheRead: 0.182, cacheWrite: 0, }, contextWindow: 202752, - maxTokens: 65535, + maxTokens: 4096, } satisfies Model<"openai-completions">, "z-ai/glm-5.2": { id: "z-ai/glm-5.2", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 1bbfbc0e..16cba20d 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.80.1] - 2026-06-23 ### Fixed diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index aecea6b5..2b78cfbd 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.80.0", + "version": "0.80.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.80.0", + "version": "0.80.1", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index fdb619f0..c0faf5ff 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.80.0", + "version": "0.80.1", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index bdbab093..d5b5cca8 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.80.0", + "version": "0.80.1", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/gondolin/package-lock.json b/packages/coding-agent/examples/extensions/gondolin/package-lock.json index bc923056..2edc9b61 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package-lock.json +++ b/packages/coding-agent/examples/extensions/gondolin/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-gondolin", - "version": "0.80.0", + "version": "0.80.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-gondolin", - "version": "0.80.0", + "version": "0.80.1", "dependencies": { "@earendil-works/gondolin": "0.12.0" } diff --git a/packages/coding-agent/examples/extensions/gondolin/package.json b/packages/coding-agent/examples/extensions/gondolin/package.json index be9bc879..041ba0c6 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package.json +++ b/packages/coding-agent/examples/extensions/gondolin/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-gondolin", "private": true, - "version": "0.80.0", + "version": "0.80.1", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 32773aec..5c79587e 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.10.0", + "version": "1.10.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.10.0", + "version": "1.10.1", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index 56acaaf2..c090d1a0 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.10.0", + "version": "1.10.1", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index 6c5ecda9..8bf67b2d 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.80.0", + "version": "0.80.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.80.0", + "version": "0.80.1", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index bb11dbbe..9a2fbb46 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.80.0", + "version": "0.80.1", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index 2055ea91..19df1779 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -1,17 +1,17 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.80.0", + "version": "0.80.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent", - "version": "0.80.0", + "version": "0.80.1", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.80.0", - "@earendil-works/pi-ai": "^0.80.0", - "@earendil-works/pi-tui": "^0.80.0", + "@earendil-works/pi-agent-core": "^0.80.1", + "@earendil-works/pi-ai": "^0.80.1", + "@earendil-works/pi-tui": "^0.80.1", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -474,11 +474,11 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.80.0.tgz", + "version": "0.80.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.80.1.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.80.0", + "@earendil-works/pi-ai": "^0.80.1", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -488,8 +488,8 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.80.0.tgz", + "version": "0.80.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.80.1.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -512,8 +512,8 @@ } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.80.0.tgz", + "version": "0.80.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.80.1.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index e9aadca2..5404261c 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.80.0", + "version": "0.80.1", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -36,9 +36,9 @@ "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.80.0", - "@earendil-works/pi-ai": "^0.80.0", - "@earendil-works/pi-tui": "^0.80.0", + "@earendil-works/pi-agent-core": "^0.80.1", + "@earendil-works/pi-ai": "^0.80.1", + "@earendil-works/pi-tui": "^0.80.1", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index ec278ccd..02ce139b 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.80.1] - 2026-06-23 ## [0.80.0] - 2026-06-23 diff --git a/packages/tui/package.json b/packages/tui/package.json index 91ae4fc1..20dcb8e4 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.80.0", + "version": "0.80.1", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", From e00074358dd7626a9526f0240998f659d856ee39 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 23 Jun 2026 20:04:12 +0200 Subject: [PATCH 105/122] Add [Unreleased] section for next cycle --- packages/agent/CHANGELOG.md | 2 ++ packages/ai/CHANGELOG.md | 2 ++ packages/coding-agent/CHANGELOG.md | 2 ++ packages/tui/CHANGELOG.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index f7e308c0..88ccede7 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.80.1] - 2026-06-23 ## [0.80.0] - 2026-06-23 diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 649d7351..6a8d1ba5 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.80.1] - 2026-06-23 ### Fixed diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 16cba20d..257a54e7 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.80.1] - 2026-06-23 ### Fixed diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 02ce139b..1b9fba50 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.80.1] - 2026-06-23 ## [0.80.0] - 2026-06-23 From 6184307c394c6a622ccdf47cf5b0f9a860cbc15e Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 23 Jun 2026 21:04:59 +0200 Subject: [PATCH 106/122] fix(ai): require explicit anthropic compat metadata --- packages/ai/src/api/anthropic-messages.ts | 13 ++----- .../ai/src/providers/openrouter.models.ts | 38 +++++++++---------- packages/ai/test/fireworks-models.test.ts | 11 +++++- 3 files changed, 33 insertions(+), 29 deletions(-) diff --git a/packages/ai/src/api/anthropic-messages.ts b/packages/ai/src/api/anthropic-messages.ts index 3b94d296..03bbc26c 100644 --- a/packages/ai/src/api/anthropic-messages.ts +++ b/packages/ai/src/api/anthropic-messages.ts @@ -170,16 +170,11 @@ const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14"; function getAnthropicCompat( model: Model<"anthropic-messages">, ): Required> { - const isFireworks = model.provider === "fireworks"; - const isCloudflareAiGatewayAnthropic = - model.provider === "cloudflare-ai-gateway" && model.baseUrl.includes("anthropic"); - return { - supportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? !isFireworks, - supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? !isFireworks, - sendSessionAffinityHeaders: - model.compat?.sendSessionAffinityHeaders ?? (isFireworks || isCloudflareAiGatewayAnthropic), - supportsCacheControlOnTools: model.compat?.supportsCacheControlOnTools ?? !isFireworks, + supportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? true, + supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? true, + sendSessionAffinityHeaders: model.compat?.sendSessionAffinityHeaders ?? false, + supportsCacheControlOnTools: model.compat?.supportsCacheControlOnTools ?? true, supportsTemperature: model.compat?.supportsTemperature ?? true, allowEmptySignature: model.compat?.allowEmptySignature ?? false, }; diff --git a/packages/ai/src/providers/openrouter.models.ts b/packages/ai/src/providers/openrouter.models.ts index 434f5696..53326347 100644 --- a/packages/ai/src/providers/openrouter.models.ts +++ b/packages/ai/src/providers/openrouter.models.ts @@ -909,7 +909,7 @@ export const OPENROUTER_MODELS = { cacheRead: 0.2, cacheWrite: 0.375, }, - contextWindow: 131072, + contextWindow: 65536, maxTokens: 32768, } satisfies Model<"openai-completions">, "google/gemini-3.1-flash-lite": { @@ -1108,7 +1108,7 @@ export const OPENROUTER_MODELS = { cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 32768, + maxTokens: 8192, } satisfies Model<"openai-completions">, "ibm-granite/granite-4.1-8b": { id: "ibm-granite/granite-4.1-8b", @@ -1216,8 +1216,8 @@ export const OPENROUTER_MODELS = { cacheRead: 0.06, cacheWrite: 0, }, - contextWindow: 262144, - maxTokens: 144000, + contextWindow: 256000, + maxTokens: 80000, } satisfies Model<"openai-completions">, "liquid/lfm-2.5-1.2b-thinking:free": { id: "liquid/lfm-2.5-1.2b-thinking:free", @@ -1451,7 +1451,7 @@ export const OPENROUTER_MODELS = { cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 4096, + maxTokens: 512000, } satisfies Model<"openai-completions">, "mistralai/codestral-2508": { id: "mistralai/codestral-2508", @@ -1807,7 +1807,7 @@ export const OPENROUTER_MODELS = { cost: { input: 0.6, output: 2.5, - cacheRead: 0.6, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, @@ -2189,7 +2189,7 @@ export const OPENROUTER_MODELS = { cacheWrite: 0, }, contextWindow: 1047576, - maxTokens: 4096, + maxTokens: 32768, } satisfies Model<"openai-completions">, "openai/gpt-4.1-nano": { id: "openai/gpt-4.1-nano", @@ -2221,7 +2221,7 @@ export const OPENROUTER_MODELS = { cost: { input: 2.5, output: 10, - cacheRead: 1.25, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 128000, @@ -2365,11 +2365,11 @@ export const OPENROUTER_MODELS = { cost: { input: 0.25, output: 2, - cacheRead: 0.03, + cacheRead: 0.025, cacheWrite: 0, }, contextWindow: 400000, - maxTokens: 4096, + maxTokens: 128000, } satisfies Model<"openai-completions">, "openai/gpt-5-nano": { id: "openai/gpt-5-nano", @@ -2437,11 +2437,11 @@ export const OPENROUTER_MODELS = { cost: { input: 1.25, output: 10, - cacheRead: 0.125, + cacheRead: 0.13, cacheWrite: 0, }, contextWindow: 128000, - maxTokens: 16384, + maxTokens: 32000, } satisfies Model<"openai-completions">, "openai/gpt-5.1-codex": { id: "openai/gpt-5.1-codex", @@ -2533,7 +2533,7 @@ export const OPENROUTER_MODELS = { cacheWrite: 0, }, contextWindow: 128000, - maxTokens: 32000, + maxTokens: 16384, } satisfies Model<"openai-completions">, "openai/gpt-5.2-codex": { id: "openai/gpt-5.2-codex", @@ -3799,11 +3799,11 @@ export const OPENROUTER_MODELS = { cost: { input: 0.14, output: 1, - cacheRead: 0.05, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 81920, + maxTokens: 262144, } satisfies Model<"openai-completions">, "qwen/qwen3.5-397b-a17b": { id: "qwen/qwen3.5-397b-a17b", @@ -3838,8 +3838,8 @@ export const OPENROUTER_MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 256000, - maxTokens: 32768, + contextWindow: 262144, + maxTokens: 262144, } satisfies Model<"openai-completions">, "qwen/qwen3.5-flash-02-23": { id: "qwen/qwen3.5-flash-02-23", @@ -4429,11 +4429,11 @@ export const OPENROUTER_MODELS = { cost: { input: 0.98, output: 3.08, - cacheRead: 0.182, + cacheRead: 0.49, cacheWrite: 0, }, contextWindow: 202752, - maxTokens: 4096, + maxTokens: 65535, } satisfies Model<"openai-completions">, "z-ai/glm-5.2": { id: "z-ai/glm-5.2", diff --git a/packages/ai/test/fireworks-models.test.ts b/packages/ai/test/fireworks-models.test.ts index 6980f318..a0c9c915 100644 --- a/packages/ai/test/fireworks-models.test.ts +++ b/packages/ai/test/fireworks-models.test.ts @@ -79,7 +79,16 @@ const tool: Tool = { parameters: Type.Object({ value: Type.String() }), }; -function createFireworksModel(compat?: Model<"anthropic-messages">["compat"]): Model<"anthropic-messages"> { +const FIREWORKS_ANTHROPIC_COMPAT = { + sendSessionAffinityHeaders: true, + supportsEagerToolInputStreaming: false, + supportsCacheControlOnTools: false, + supportsLongCacheRetention: false, +} satisfies NonNullable["compat"]>; + +function createFireworksModel( + compat: Model<"anthropic-messages">["compat"] = FIREWORKS_ANTHROPIC_COMPAT, +): Model<"anthropic-messages"> { return { id: "accounts/fireworks/models/kimi-k2p6", name: "Kimi K2.6", From c3cfeac0413ab95880008da9c20e8aafca305aea Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 23 Jun 2026 20:55:15 +0200 Subject: [PATCH 107/122] fix(coding-agent): make release publication transactional --- .github/workflows/build-binaries.yml | 184 ++++++++++++++++++++++++--- scripts/publish.mjs | 38 ++++-- 2 files changed, 187 insertions(+), 35 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 355ad1cd..9fcb4278 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -17,11 +17,17 @@ on: permissions: {} +concurrency: + group: build-binaries-${{ github.event.inputs.tag || github.ref_name }} + cancel-in-progress: false + jobs: + # Keep the public GitHub Release publication last. Binary assets are staged in + # a draft release first; cleanup removes the draft if later publishing fails. build: runs-on: ubuntu-latest permissions: - contents: write + contents: read env: RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }} SOURCE_REF: ${{ github.event.inputs.source_ref || github.event.inputs.tag || github.ref_name }} @@ -46,17 +52,16 @@ jobs: - name: Build binaries run: ./scripts/build-binaries.sh - - name: Extract changelog for this version - id: changelog + - name: Prepare GitHub release payload run: | + set -euo pipefail + + mkdir -p .release-assets + VERSION="${RELEASE_TAG}" VERSION="${VERSION#v}" # Remove 'v' prefix - node scripts/release-notes.mjs extract --version "${VERSION}" --tag "${RELEASE_TAG}" --out /tmp/release-notes.md + node scripts/release-notes.mjs extract --version "${VERSION}" --tag "${RELEASE_TAG}" --out .release-assets/RELEASE_NOTES.md - - name: Create GitHub Release and upload binaries - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | cd packages/coding-agent/binaries release_assets=( @@ -67,24 +72,107 @@ jobs: pi-windows-x64.zip pi-windows-arm64.zip ) - sha256sum "${release_assets[@]}" > SHA256SUMS - release_assets+=(SHA256SUMS) - if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then - gh release edit "${RELEASE_TAG}" \ - --title "${RELEASE_TAG}" \ - --notes-file /tmp/release-notes.md - gh release upload "${RELEASE_TAG}" "${release_assets[@]}" --clobber - else - gh release create "${RELEASE_TAG}" \ - --title "${RELEASE_TAG}" \ - --notes-file /tmp/release-notes.md \ - "${release_assets[@]}" + for asset in "${release_assets[@]}"; do + test -f "${asset}" + done + + sha256sum "${release_assets[@]}" > "${GITHUB_WORKSPACE}/.release-assets/SHA256SUMS" + cp "${release_assets[@]}" "${GITHUB_WORKSPACE}/.release-assets/" + + - name: Upload GitHub release payload + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: release-assets-${{ env.RELEASE_TAG }} + path: .release-assets/* + if-no-files-found: error + retention-days: 14 + + stage-github-release: + runs-on: ubuntu-latest + needs: build + permissions: + actions: read + contents: write + env: + GH_REPO: ${{ github.repository }} + RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }} + steps: + - name: Download GitHub release payload + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: release-assets-${{ env.RELEASE_TAG }} + path: release-assets + + - name: Validate GitHub release payload + run: | + set -euo pipefail + + cd release-assets + + expected_assets=( + pi-darwin-arm64.tar.gz + pi-darwin-x64.tar.gz + pi-linux-x64.tar.gz + pi-linux-arm64.tar.gz + pi-windows-x64.zip + pi-windows-arm64.zip + SHA256SUMS + RELEASE_NOTES.md + ) + + for asset in "${expected_assets[@]}"; do + test -f "${asset}" + done + + sha256sum -c SHA256SUMS + + - name: Create draft GitHub Release and upload binaries + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + cd release-assets + + release_assets=( + pi-darwin-arm64.tar.gz + pi-darwin-x64.tar.gz + pi-linux-x64.tar.gz + pi-linux-arm64.tar.gz + pi-windows-x64.zip + pi-windows-arm64.zip + SHA256SUMS + ) + + existing_release="$(gh release view "${RELEASE_TAG}" --json isDraft --jq .isDraft 2>/dev/null || true)" + if [[ "${existing_release}" == "false" ]]; then + echo "::error::GitHub Release ${RELEASE_TAG} is already published. Refusing to mutate a public release." + exit 1 + fi + if [[ "${existing_release}" == "true" ]]; then + gh release delete "${RELEASE_TAG}" --yes + fi + + gh release create "${RELEASE_TAG}" \ + --verify-tag \ + --draft \ + --title "${RELEASE_TAG}" \ + --notes-file RELEASE_NOTES.md \ + "${release_assets[@]}" + + expected_asset_names="$(printf '%s\n' "${release_assets[@]}" | sort)" + actual_asset_names="$(gh release view "${RELEASE_TAG}" --json assets --jq '.assets[].name' | sort)" + + if [[ "${actual_asset_names}" != "${expected_asset_names}" ]]; then + echo "::error::Draft GitHub Release asset set does not match expected files." + diff -u <(printf '%s\n' "${expected_asset_names}") <(printf '%s\n' "${actual_asset_names}") || true + exit 1 fi publish-npm: runs-on: ubuntu-latest - needs: build + needs: stage-github-release environment: npm-publish permissions: contents: read @@ -134,3 +222,57 @@ jobs: - name: Publish npm packages run: node scripts/publish.mjs + + publish-github-release: + runs-on: ubuntu-latest + needs: + - stage-github-release + - publish-npm + permissions: + contents: write + env: + GH_REPO: ${{ github.repository }} + RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }} + steps: + - name: Publish staged GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + existing_release="$(gh release view "${RELEASE_TAG}" --json isDraft --jq .isDraft 2>/dev/null || true)" + if [[ "${existing_release}" == "" ]]; then + echo "::error::Draft GitHub Release ${RELEASE_TAG} does not exist." + exit 1 + fi + if [[ "${existing_release}" == "false" ]]; then + echo "::error::GitHub Release ${RELEASE_TAG} is already published." + exit 1 + fi + + gh release edit "${RELEASE_TAG}" --draft=false + + cleanup-draft-github-release: + runs-on: ubuntu-latest + needs: + - build + - stage-github-release + - publish-npm + - publish-github-release + if: ${{ always() && needs.stage-github-release.result != 'skipped' && (needs.stage-github-release.result != 'success' || needs.publish-npm.result != 'success' || needs.publish-github-release.result != 'success') }} + permissions: + contents: write + env: + GH_REPO: ${{ github.repository }} + RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }} + steps: + - name: Delete draft GitHub Release after failure + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + existing_release="$(gh release view "${RELEASE_TAG}" --json isDraft --jq .isDraft 2>/dev/null || true)" + if [[ "${existing_release}" == "true" ]]; then + gh release delete "${RELEASE_TAG}" --yes + fi diff --git a/scripts/publish.mjs b/scripts/publish.mjs index 513ab36f..920954f8 100644 --- a/scripts/publish.mjs +++ b/scripts/publish.mjs @@ -89,24 +89,34 @@ if (versions.length !== 1) { console.log(`Publishing pi packages at ${versions[0]}${dryRun ? " (dry run)" : ""}\n`); -for (const pkg of packages) { - const version = packageVersions.get(pkg.name); +const packageStates = packages.map((pkg) => ({ + ...pkg, + published: false, + version: packageVersions.get(pkg.name), +})); + +for (const pkg of packageStates) { assertBuildOutputExists(pkg.directory); - const published = isPublished(pkg.name, version); + pkg.published = isPublished(pkg.name, pkg.version); - if (dryRun) { - if (published) { - console.log(`${pkg.name}@${version} is already published; validating package contents only.`); - } else { - console.log(`${pkg.name}@${version} is not published; validating package contents before publish.`); - } - validatePack(pkg.directory); - console.log(); - continue; + if (pkg.published) { + console.log(`${pkg.name}@${pkg.version} is already published; validating package contents only.`); + } else { + console.log(`${pkg.name}@${pkg.version} is not published; validating package contents before publish.`); } + validatePack(pkg.directory); + console.log(); +} - if (published) { - console.log(`Skipping ${pkg.name}@${version}: already published\n`); +if (dryRun) { + process.exit(0); +} + +console.log("All packages validated; starting publication.\n"); + +for (const pkg of packageStates) { + if (pkg.published) { + console.log(`Skipping ${pkg.name}@${pkg.version}: already published\n`); continue; } From b377623435e0ea01fca95517d0b0ae2797ac5b41 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 23 Jun 2026 21:59:54 +0200 Subject: [PATCH 108/122] Type name change --- packages/agent/docs/models.md | 75 +++++++++++++++---- packages/agent/src/harness/types.ts | 34 ++++----- .../agent/src/harness/utils/shell-output.ts | 12 +-- packages/agent/test/scratch/simple.ts | 11 ++- 4 files changed, 88 insertions(+), 44 deletions(-) diff --git a/packages/agent/docs/models.md b/packages/agent/docs/models.md index 0be60235..6a5cd8f7 100644 --- a/packages/agent/docs/models.md +++ b/packages/agent/docs/models.md @@ -737,9 +737,10 @@ Rules: 3. Lazy API wrappers dynamically import real API implementations. 4. Real API implementations import SDK dependencies. 5. OAuth implementations are always attached via `lazyOAuth()` and lazy-loaded behind a bundler-opaque dynamic import; provider metadata never eagerly imports Node-only OAuth code. -6. `providers/all` may import all provider metadata, but no eager SDK imports. +6. `providers/all` imports every built-in provider factory and all catalogs. It is the explicit heavy entrypoint. 7. Provider modules are side-effect-free; importing a provider does not register anything globally. -8. `package.json` sets `sideEffects: false`. +8. `package.json` lists only effectful compat/image registration files in `sideEffects`; root and provider modules stay tree-shakeable. +9. With code splitting, provider SDKs stay in lazy chunks. Without code splitting, bundlers fold statically reachable lazy API implementations into the single bundle; `providers/all` then pulls all statically visible SDKs. Bedrock is the exception because its AWS SDK implementation is behind a bundler-opaque Node-only import and needs `setBedrockProviderModule()` for standalone single-file bundles. Exports map sketch: @@ -786,11 +787,12 @@ for (const provider of layeredProviders) sessionModels.setProvider(provider); coding-agent owns: `FileCredentialStore` + decorators replacing AuthStorage (see "Replacing AuthStorage"), models.json auth sidecar (`$ENV`, `!command`), command execution policy, provider status labels (from `AuthResult.source`), login/logout UI (driving `auth.{apiKey,oauth}.login()` with `prompt()/notify()`), extension lifecycle, provider-management slash commands. -Until then, the only coding-agent changes in this pass are: +Current interim state: -- construct a `Models` instance for `AgentHarness` (builtins + legacy api-dispatch fallback bridging `ModelRegistry` custom providers) -- switch old-global imports to `@earendil-works/pi-ai/compat` -- adapt the login dialog to `prompt()/notify()` callbacks (thin UI adapter; replaces the `usesCallbackServer` special-casing) +- `AgentHarness` already accepts a `Models` instance and uses it for turn streaming, compaction, and branch summaries. +- coding-agent does not use `AgentHarness` yet; `AgentSession` still drives the low-level `Agent` with a `streamFn`. +- coding-agent still uses legacy `AuthStorage` + `ModelRegistry` and imports old global pi-ai APIs through `@earendil-works/pi-ai/compat`. +- The extension loader still aliases the pi-ai root to `/compat` as the runtime grace period for old extensions. ## Implementation TODOs @@ -877,16 +879,59 @@ Decisions: - 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 metadata/env -> `ModelAuth.baseUrl`). Built-in compat calls route through `Models`, so they use the same provider auth path. -Ordering: +Ordering for new sessions: -- [x] pi-ai rework first: `Provider.getModels()` sync + optional `refreshModels()`; `Models.getModels`/`getModel` sync, `Models.refresh(provider?)` async; `createProvider` takes `models` array + optional `refreshModels` fetcher (in-flight dedupe). Reverses Phase 1's async-listing decision — see "Provider model listing" for rationale (sync-or-async unions breed latent sync assumptions; async-only breaks sync consumer surfaces like extension `find`/`getAll`). -- [ ] `FileCredentialStore` (ports the auth.json lock backend, reads legacy `type: "api_key"` tags) + `--api-key` overlay + `$ENV`/`!command` resolution; tests. -- [x] Cloudflare provider auth in pi-ai factories. -- [ ] Copilot `getModels` baseUrl wrap. -- [ ] Extension-OAuth adapter (old `OAuthProviderInterface` config -> `OAuthAuth`). -- [ ] ModelRegistry rebuild: owns `MutableModels`, async reads, models.json decoration, provider-keyed extension streams, facade auth ops; AuthStorage deleted. -- [ ] Consumer rewiring: agent-session, sdk (`credentials?: CredentialStore` option replaces `authStorage`; sdk.md updated), model-resolver, interactive login/status UI on `prompt()/notify()`, cli `--api-key`. -- [ ] Test migration; tmux validation of login flows against real providers. +1. [x] pi-ai rework first: `Provider.getModels()` sync + optional `refreshModels()`; `Models.getModels`/`getModel` sync, `Models.refresh(provider?)` async; `createProvider` takes `models` array + optional `refreshModels` fetcher (in-flight dedupe). Reverses Phase 1's async-listing decision — see "Provider model listing" for rationale (sync-or-async unions breed latent sync assumptions; async-only breaks sync consumer surfaces like extension `find`/`getAll`). +2. [x] Cloudflare provider auth in pi-ai factories: Workers AI and AI Gateway validate their required account/gateway metadata and return resolved `baseUrl`, provider-scoped env, and header suppression/override metadata from provider auth. +3. [ ] Add `FileCredentialStore` in coding-agent. + - Implement the pi-ai `CredentialStore` interface over the existing `auth.json` lock backend (`FileAuthStorageBackend` / `InMemoryAuthStorageBackend` can be reused or renamed). + - Preserve the existing file format where possible, but normalize legacy `{ type: "api_key", key, env? }` to pi-ai `{ type: "api-key", key, metadata? }` on read. `env` becomes provider metadata/env sidecar data; do not lose unknown keys. + - `read(provider)` returns the current credential snapshot and records parse/storage errors for status UI parity. + - `modify(provider, fn)` must lock, re-read, run `fn`, merge-write the provider entry, chmod `0600`, and return the post-write credential. + - `delete(provider)` must lock and remove only that provider's entry. + - Add file-backed and in-memory tests covering lock/RMW behavior, legacy `api_key` reads, OAuth reads, metadata/env preservation, delete, parse errors, and concurrent refresh-style modifications. +4. [ ] Add store decorators for coding-agent policy. + - `withConfigValues(store, policy)` resolves stored API-key credentials whose `key` or metadata values use `$ENV` or `!command`, using existing `resolve-config-value.ts` semantics. Command execution stays in coding-agent, not pi-ai. + - `withRuntimeOverrides(store, overrides)` implements CLI `--api-key`: read returns an ephemeral `{ type: "api-key", key }` for each overridden provider, masking stored OAuth/API credentials without persisting. + - Runtime overrides must apply even to OAuth-capable providers; every provider registered in coding-agent must retain or gain an `apiKey` auth slot so the overlay is meaningful. + - Tests cover precedence: runtime override > stored credential > models.json config auth > ambient provider env, with stored credential blocking ambient fallback. +5. [ ] Build provider decoration helpers for `models.json`. + - Start from built-in provider factories, not generated model arrays. + - Wrap provider `getModels()` so provider-level `baseUrl`/`headers`/`compat`, per-model `modelOverrides`, and custom model merges apply on every sync read. + - Preserve `refreshModels()` passthrough so dynamic providers compose with decorations. + - Convert provider `apiKey`/`headers`/`authHeader` models.json config into a wrapped `ApiKeyAuth` that resolves config values first and falls back to the base provider auth. + - Custom providers with `models` use `createProvider()` with the appropriate lazy API wrapper or extension-provided stream implementation. + - Parse errors must keep current `ModelRegistry.getError()` behavior: built-ins remain available, and the error is visible. +6. [ ] Copilot `getModels()` baseUrl wrap. + - GitHub Copilot OAuth `toAuth()` already returns per-credential request `baseUrl` for streaming. + - 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. +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()` become async and delegate to the collection/status checks. Update extension-facing types and changelog this breaking change. + - `refresh()` rebuilds provider layers and calls `models.refresh()` where needed; no global api-registry reset should be part of the new path except compat-only grace behavior. + - `registerProvider()`/`unregisterProvider()` mutate provider layers and rebuild the collection. + - Facade auth ops (`login`, `logout`, provider status, available OAuth providers) drive `provider.auth.{apiKey,oauth}` and the `CredentialStore`; no `AuthStorage` type remains. + - Legacy `registerApiProvider` writes stay only for `/compat` callers and are removed in Phase 10. +9. [ ] Rewire consumers. + - `AgentSession` stream function resolves through `ModelRegistry`/`Models`, not `getApiKeyAndHeaders()` + compat globals. + - SDK options replace `authStorage` with `credentials?: CredentialStore` or an agent-dir-backed default; update `sdk.md` and examples. + - `model-resolver`, `--list-models`, model selector, login/logout/status UI, and provider attribution await async model/auth APIs. + - CLI `--api-key` populates the runtime override decorator instead of mutating `AuthStorage`. + - Keep extension loader root-to-compat alias until Phase 10, but expose the new collection/facade as the forward API. +10. [ ] Test migration and real-provider validation. + - Unit tests for `FileCredentialStore`, config-value decorators, provider decoration, extension OAuth adapter, ModelRegistry async facade, and consumer rewiring. + - Regression tests for Cloudflare account/gateway metadata, Copilot OAuth baseUrl wrapping, runtime `--api-key` precedence, `$ENV`/`!command` resolution, and stored credential blocking ambient fallback. + - Update existing tests that assume sync `ModelRegistry.getAll/find/getAvailable`. + - Run targeted non-e2e suites plus tmux validation of login flows against real providers (Anthropic OAuth/API key, OpenAI Codex OAuth, GitHub Copilot OAuth, Cloudflare AI Gateway, Bedrock if credentials are available). ### Phase 10 — compat deletion (pi 2.0 era, separate) diff --git a/packages/agent/src/harness/types.ts b/packages/agent/src/harness/types.ts index f7bdf6dd..29048e61 100644 --- a/packages/agent/src/harness/types.ts +++ b/packages/agent/src/harness/types.ts @@ -240,22 +240,6 @@ export interface FileInfo { mtimeMs: number; } -/** Options for {@link Shell.exec}. */ -export interface ExecutionEnvExecOptions { - /** Working directory for the command. Relative paths are resolved against {@link ExecutionEnv.cwd}. Defaults to {@link ExecutionEnv.cwd}. */ - cwd?: string; - /** Additional environment variables for the command. Values override the environment defaults. Defaults to no overrides. */ - env?: Record; - /** Timeout in seconds. Implementations should return a timeout error when the command exceeds this duration. Defaults to no timeout. */ - timeout?: number; - /** Abort signal used to terminate the command. Defaults to no abort signal. */ - abortSignal?: AbortSignal; - /** Called with stdout chunks as they are produced. */ - onStdout?: (chunk: string) => void; - /** Called with stderr chunks as they are produced. */ - onStderr?: (chunk: string) => void; -} - /** * Filesystem capability used by the harness. * @@ -317,12 +301,28 @@ export interface FileSystem { cleanup(): Promise; } +/** Options for {@link Shell.exec}. */ +export interface ShellExecOptions { + /** Working directory for the command. Relative paths are resolved against {@link ExecutionEnv.cwd}. Defaults to {@link ExecutionEnv.cwd}. */ + cwd?: string; + /** Additional environment variables for the command. Values override the environment defaults. Defaults to no overrides. */ + env?: Record; + /** Timeout in seconds. Implementations should return a timeout error when the command exceeds this duration. Defaults to no timeout. */ + timeout?: number; + /** Abort signal used to terminate the command. Defaults to no abort signal. */ + abortSignal?: AbortSignal; + /** Called with stdout chunks as they are produced. */ + onStdout?: (chunk: string) => void; + /** Called with stderr chunks as they are produced. */ + onStderr?: (chunk: string) => void; +} + /** Shell execution capability used by the harness. */ export interface Shell { /** Execute a shell command in {@link FileSystem.cwd} unless `options.cwd` is provided. */ exec( command: string, - options?: ExecutionEnvExecOptions, + options?: ShellExecOptions, ): Promise>; /** Release shell resources. Must be best-effort and must not throw or reject. */ cleanup(): Promise; diff --git a/packages/agent/src/harness/utils/shell-output.ts b/packages/agent/src/harness/utils/shell-output.ts index fea46d95..a98b51a5 100644 --- a/packages/agent/src/harness/utils/shell-output.ts +++ b/packages/agent/src/harness/utils/shell-output.ts @@ -1,15 +1,7 @@ -import { - type ExecutionEnv, - type ExecutionEnvExecOptions, - ExecutionError, - err, - ok, - type Result, - toError, -} from "../types.ts"; +import { type ExecutionEnv, ExecutionError, err, ok, type Result, type ShellExecOptions, toError } from "../types.ts"; import { DEFAULT_MAX_BYTES, truncateTail } from "./truncate.ts"; -export interface ShellCaptureOptions extends Omit { +export interface ShellCaptureOptions extends Omit { onChunk?: (chunk: string) => void; } diff --git a/packages/agent/test/scratch/simple.ts b/packages/agent/test/scratch/simple.ts index 86eae721..b4feb665 100644 --- a/packages/agent/test/scratch/simple.ts +++ b/packages/agent/test/scratch/simple.ts @@ -1,7 +1,7 @@ import { homedir } from "node:os"; import { join } from "node:path"; import { createModels } from "@earendil-works/pi-ai"; -import { getBuiltinModel } from "@earendil-works/pi-ai/providers/all"; +import { cloudflareAIGatewayProvider } from "@earendil-works/pi-ai/providers/cloudflare-ai-gateway"; import { openaiProvider } from "@earendil-works/pi-ai/providers/openai"; import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts"; import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.ts"; @@ -39,13 +39,20 @@ const { promptTemplates: sourcedPromptTemplates } = await loadSourcedPromptTempl const models = createModels(); models.setProvider(openaiProvider()); +models.setProvider(cloudflareAIGatewayProvider()); +const model = models.getModel("openai", "gpt-5.5"); +// const model = models.getModel("cloudflare-ai-gateway", "claude-haiku-4-5"); +if (!model) { + console.log("Model not found"); + process.exit(-1); +} const session = new Session(new InMemorySessionStorage()); const agent = new AgentHarness({ env, session, models, - model: getBuiltinModel("openai", "gpt-5.5"), + model, thinkingLevel: "low", systemPrompt: ({ env, resources }) => [ From 386d079afa11816fc9b6d0f19343c07699c5303f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 23 Jun 2026 22:19:58 +0200 Subject: [PATCH 109/122] fix(ai): restore legacy compat stream aliases closes #6016 --- packages/ai/CHANGELOG.md | 4 + packages/ai/src/compat.ts | 1 + packages/ai/src/legacy-api-aliases.ts | 108 ++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 packages/ai/src/legacy-api-aliases.ts diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 6a8d1ba5..2472866b 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Restored temporary legacy per-API stream aliases such as `streamSimpleOpenAICompletions` on the compat entrypoint ([#6016](https://github.com/earendil-works/pi/issues/6016), [#6017](https://github.com/earendil-works/pi/issues/6017)). + ## [0.80.1] - 2026-06-23 ### Fixed diff --git a/packages/ai/src/compat.ts b/packages/ai/src/compat.ts index 51b7e17f..91ecda4f 100644 --- a/packages/ai/src/compat.ts +++ b/packages/ai/src/compat.ts @@ -24,6 +24,7 @@ export * from "./image-models.ts"; export * from "./images.ts"; export * from "./images-api-registry.ts"; export * from "./index.ts"; +export * from "./legacy-api-aliases.ts"; export * from "./providers/images/register-builtins.ts"; import { anthropicMessagesApi } from "./api/anthropic-messages.lazy.ts"; diff --git a/packages/ai/src/legacy-api-aliases.ts b/packages/ai/src/legacy-api-aliases.ts new file mode 100644 index 00000000..b49c199c --- /dev/null +++ b/packages/ai/src/legacy-api-aliases.ts @@ -0,0 +1,108 @@ +import { anthropicMessagesApi } from "./api/anthropic-messages.lazy.ts"; +import type { AnthropicOptions } from "./api/anthropic-messages.ts"; +import { azureOpenAIResponsesApi } from "./api/azure-openai-responses.lazy.ts"; +import type { AzureOpenAIResponsesOptions } from "./api/azure-openai-responses.ts"; +import { googleGenerativeAIApi } from "./api/google-generative-ai.lazy.ts"; +import type { GoogleOptions } from "./api/google-generative-ai.ts"; +import { googleVertexApi } from "./api/google-vertex.lazy.ts"; +import type { GoogleVertexOptions } from "./api/google-vertex.ts"; +import { mistralConversationsApi } from "./api/mistral-conversations.lazy.ts"; +import type { MistralOptions } from "./api/mistral-conversations.ts"; +import { openAICodexResponsesApi } from "./api/openai-codex-responses.lazy.ts"; +import type { OpenAICodexResponsesOptions } from "./api/openai-codex-responses.ts"; +import { openAICompletionsApi } from "./api/openai-completions.lazy.ts"; +import type { OpenAICompletionsOptions } from "./api/openai-completions.ts"; +import { openAIResponsesApi } from "./api/openai-responses.lazy.ts"; +import type { OpenAIResponsesOptions } from "./api/openai-responses.ts"; +import type { SimpleStreamOptions, StreamFunction } from "./types.ts"; + +const anthropicMessagesStreams = anthropicMessagesApi(); +const azureOpenAIResponsesStreams = azureOpenAIResponsesApi(); +const googleGenerativeAIStreams = googleGenerativeAIApi(); +const googleVertexStreams = googleVertexApi(); +const mistralConversationsStreams = mistralConversationsApi(); +const openAICodexResponsesStreams = openAICodexResponsesApi(); +const openAICompletionsStreams = openAICompletionsApi(); +const openAIResponsesStreams = openAIResponsesApi(); + +/** @deprecated Use `stream` from `@earendil-works/pi-ai/api/anthropic-messages` or `anthropicMessagesApi().stream`. */ +export const streamAnthropic = anthropicMessagesStreams.stream as StreamFunction< + "anthropic-messages", + AnthropicOptions +>; +/** @deprecated Use `streamSimple` from `@earendil-works/pi-ai/api/anthropic-messages` or `anthropicMessagesApi().streamSimple`. */ +export const streamSimpleAnthropic = anthropicMessagesStreams.streamSimple as StreamFunction< + "anthropic-messages", + SimpleStreamOptions +>; + +/** @deprecated Use `stream` from `@earendil-works/pi-ai/api/azure-openai-responses` or `azureOpenAIResponsesApi().stream`. */ +export const streamAzureOpenAIResponses = azureOpenAIResponsesStreams.stream as StreamFunction< + "azure-openai-responses", + AzureOpenAIResponsesOptions +>; +/** @deprecated Use `streamSimple` from `@earendil-works/pi-ai/api/azure-openai-responses` or `azureOpenAIResponsesApi().streamSimple`. */ +export const streamSimpleAzureOpenAIResponses = azureOpenAIResponsesStreams.streamSimple as StreamFunction< + "azure-openai-responses", + SimpleStreamOptions +>; + +/** @deprecated Use `stream` from `@earendil-works/pi-ai/api/google-generative-ai` or `googleGenerativeAIApi().stream`. */ +export const streamGoogle = googleGenerativeAIStreams.stream as StreamFunction<"google-generative-ai", GoogleOptions>; +/** @deprecated Use `streamSimple` from `@earendil-works/pi-ai/api/google-generative-ai` or `googleGenerativeAIApi().streamSimple`. */ +export const streamSimpleGoogle = googleGenerativeAIStreams.streamSimple as StreamFunction< + "google-generative-ai", + SimpleStreamOptions +>; + +/** @deprecated Use `stream` from `@earendil-works/pi-ai/api/google-vertex` or `googleVertexApi().stream`. */ +export const streamGoogleVertex = googleVertexStreams.stream as StreamFunction<"google-vertex", GoogleVertexOptions>; +/** @deprecated Use `streamSimple` from `@earendil-works/pi-ai/api/google-vertex` or `googleVertexApi().streamSimple`. */ +export const streamSimpleGoogleVertex = googleVertexStreams.streamSimple as StreamFunction< + "google-vertex", + SimpleStreamOptions +>; + +/** @deprecated Use `stream` from `@earendil-works/pi-ai/api/mistral-conversations` or `mistralConversationsApi().stream`. */ +export const streamMistral = mistralConversationsStreams.stream as StreamFunction< + "mistral-conversations", + MistralOptions +>; +/** @deprecated Use `streamSimple` from `@earendil-works/pi-ai/api/mistral-conversations` or `mistralConversationsApi().streamSimple`. */ +export const streamSimpleMistral = mistralConversationsStreams.streamSimple as StreamFunction< + "mistral-conversations", + SimpleStreamOptions +>; + +/** @deprecated Use `stream` from `@earendil-works/pi-ai/api/openai-codex-responses` or `openAICodexResponsesApi().stream`. */ +export const streamOpenAICodexResponses = openAICodexResponsesStreams.stream as StreamFunction< + "openai-codex-responses", + OpenAICodexResponsesOptions +>; +/** @deprecated Use `streamSimple` from `@earendil-works/pi-ai/api/openai-codex-responses` or `openAICodexResponsesApi().streamSimple`. */ +export const streamSimpleOpenAICodexResponses = openAICodexResponsesStreams.streamSimple as StreamFunction< + "openai-codex-responses", + SimpleStreamOptions +>; + +/** @deprecated Use `stream` from `@earendil-works/pi-ai/api/openai-completions` or `openAICompletionsApi().stream`. */ +export const streamOpenAICompletions = openAICompletionsStreams.stream as StreamFunction< + "openai-completions", + OpenAICompletionsOptions +>; +/** @deprecated Use `streamSimple` from `@earendil-works/pi-ai/api/openai-completions` or `openAICompletionsApi().streamSimple`. */ +export const streamSimpleOpenAICompletions = openAICompletionsStreams.streamSimple as StreamFunction< + "openai-completions", + SimpleStreamOptions +>; + +/** @deprecated Use `stream` from `@earendil-works/pi-ai/api/openai-responses` or `openAIResponsesApi().stream`. */ +export const streamOpenAIResponses = openAIResponsesStreams.stream as StreamFunction< + "openai-responses", + OpenAIResponsesOptions +>; +/** @deprecated Use `streamSimple` from `@earendil-works/pi-ai/api/openai-responses` or `openAIResponsesApi().streamSimple`. */ +export const streamSimpleOpenAIResponses = openAIResponsesStreams.streamSimple as StreamFunction< + "openai-responses", + SimpleStreamOptions +>; From 49fbe6834fffe7a26d7c1dcc5a20eb489ce1f057 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 23 Jun 2026 22:29:46 +0200 Subject: [PATCH 110/122] fix(ai): align api key credentials with auth json --- packages/agent/docs/models.md | 28 +++++----- packages/ai/CHANGELOG.md | 4 ++ packages/ai/README.md | 13 +++++ packages/ai/src/auth/helpers.ts | 4 +- packages/ai/src/auth/resolve.ts | 2 +- packages/ai/src/auth/types.ts | 14 ++--- packages/ai/src/providers/cloudflare-auth.ts | 55 +++++++++----------- packages/ai/test/models-runtime.test.ts | 2 +- packages/ai/test/providers.test.ts | 4 +- 9 files changed, 70 insertions(+), 56 deletions(-) diff --git a/packages/agent/docs/models.md b/packages/agent/docs/models.md index 6a5cd8f7..c634a267 100644 --- a/packages/agent/docs/models.md +++ b/packages/agent/docs/models.md @@ -399,19 +399,19 @@ If a value cannot be expressed as `apiKey`, `headers`, or `baseUrl`, it is provi ```ts export interface ProviderAuth { - apiKey?: ApiKeyAuth; // stored key/metadata + ambient env/files/ADC/IAM + apiKey?: ApiKeyAuth; // stored key/provider env + ambient env/files/ADC/IAM oauth?: OAuthAuth; // login flow + refresh } export interface ApiKeyAuth { name: string; // "Anthropic API key" - /** Interactive setup (prompt for key/metadata). Absent = ambient-only (env, ADC, IAM). */ + /** Interactive setup (prompt for key/provider env). Absent = ambient-only (env, ADC, IAM). */ login?(callbacks: AuthLoginCallbacks): Promise; /** * Resolve auth from the stored credential and/or ambient sources, merging - * per field (credential.key ?? env("..."), metadata.accountId ?? env("...")). + * per field (credential.key ?? env("..."), credential.env?.NAME ?? env("...")). * undefined = not configured. */ resolve(input: { @@ -455,9 +455,9 @@ One credential per provider, type-tagged — exactly the shape of today's auth.j ```ts export interface ApiKeyCredential { - type: "api-key"; + type: "api_key"; key?: string; - metadata?: Record; // e.g. Cloudflare accountId/gatewayId + env?: ProviderEnv; // e.g. Cloudflare account/gateway ids, Azure/Vertex/Bedrock scoped config } export interface OAuthCredential extends OAuthCredentials { @@ -467,7 +467,7 @@ export interface OAuthCredential extends OAuthCredentials { export type Credential = ApiKeyCredential | OAuthCredential; ``` -`ApiKeyCredential.metadata` exists for providers like Cloudflare that store non-key values (account id, gateway id) alongside or instead of a key. `ApiKeyAuth.resolve()` merges per field: `credential.key ?? env("CLOUDFLARE_API_TOKEN")`, `credential.metadata?.accountId ?? env("CLOUDFLARE_ACCOUNT_ID")`, etc. +`ApiKeyCredential.env` stores provider-scoped environment/config values alongside or instead of a key. `ApiKeyAuth.resolve()` merges per field: `credential.key ?? env("CLOUDFLARE_API_KEY")`, `credential.env?.CLOUDFLARE_ACCOUNT_ID ?? env("CLOUDFLARE_ACCOUNT_ID")`, etc. The credential discriminator intentionally matches today's `auth.json` (`api_key`) so the file-backed store does not need lossy type translation. ### Credential store @@ -528,7 +528,7 @@ if (stored) { } return { auth: await oauth.toAuth(credential), source: "OAuth" }; } - if (stored.type === "api-key" && provider.auth.apiKey) { + if (stored.type === "api_key" && provider.auth.apiKey) { return provider.auth.apiKey.resolve({ model, ctx, credential: stored }); } return undefined; // stored credential without matching handler blocks ambient @@ -877,22 +877,22 @@ Decisions: - 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. - 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 metadata/env -> `ModelAuth.baseUrl`). Built-in compat calls route through `Models`, so they use the same provider auth path. +- 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. Ordering for new sessions: 1. [x] pi-ai rework first: `Provider.getModels()` sync + optional `refreshModels()`; `Models.getModels`/`getModel` sync, `Models.refresh(provider?)` async; `createProvider` takes `models` array + optional `refreshModels` fetcher (in-flight dedupe). Reverses Phase 1's async-listing decision — see "Provider model listing" for rationale (sync-or-async unions breed latent sync assumptions; async-only breaks sync consumer surfaces like extension `find`/`getAll`). -2. [x] Cloudflare provider auth in pi-ai factories: Workers AI and AI Gateway validate their required account/gateway metadata and return resolved `baseUrl`, provider-scoped env, and header suppression/override metadata from provider auth. +2. [x] Cloudflare provider auth in pi-ai factories: Workers AI and AI Gateway validate their required account/gateway env/config and return resolved `baseUrl`, provider-scoped env, and header suppression/override metadata from provider auth. 3. [ ] Add `FileCredentialStore` in coding-agent. - Implement the pi-ai `CredentialStore` interface over the existing `auth.json` lock backend (`FileAuthStorageBackend` / `InMemoryAuthStorageBackend` can be reused or renamed). - - Preserve the existing file format where possible, but normalize legacy `{ type: "api_key", key, env? }` to pi-ai `{ type: "api-key", key, metadata? }` on read. `env` becomes provider metadata/env sidecar data; do not lose unknown keys. + - Preserve the existing file format. `ApiKeyCredential` uses `{ type: "api_key", key?, env? }`, matching today's `auth.json`; do not translate `env` into metadata or rewrite discriminators. - `read(provider)` returns the current credential snapshot and records parse/storage errors for status UI parity. - `modify(provider, fn)` must lock, re-read, run `fn`, merge-write the provider entry, chmod `0600`, and return the post-write credential. - `delete(provider)` must lock and remove only that provider's entry. - - Add file-backed and in-memory tests covering lock/RMW behavior, legacy `api_key` reads, OAuth reads, metadata/env preservation, delete, parse errors, and concurrent refresh-style modifications. + - Add file-backed and in-memory tests covering lock/RMW behavior, `api_key` reads, OAuth reads, provider `env` preservation, delete, parse errors, and concurrent refresh-style modifications. 4. [ ] Add store decorators for coding-agent policy. - - `withConfigValues(store, policy)` resolves stored API-key credentials whose `key` or metadata values use `$ENV` or `!command`, using existing `resolve-config-value.ts` semantics. Command execution stays in coding-agent, not pi-ai. - - `withRuntimeOverrides(store, overrides)` implements CLI `--api-key`: read returns an ephemeral `{ type: "api-key", key }` for each overridden provider, masking stored OAuth/API credentials without persisting. + - `withConfigValues(store, policy)` resolves stored API-key credentials whose `key` or `env` values use `$ENV` or `!command`, using existing `resolve-config-value.ts` semantics. Command execution stays in coding-agent, not pi-ai. + - `withRuntimeOverrides(store, overrides)` implements CLI `--api-key`: read returns an ephemeral `{ type: "api_key", key }` for each overridden provider, masking stored OAuth/API credentials without persisting. - Runtime overrides must apply even to OAuth-capable providers; every provider registered in coding-agent must retain or gain an `apiKey` auth slot so the overlay is meaningful. - Tests cover precedence: runtime override > stored credential > models.json config auth > ambient provider env, with stored credential blocking ambient fallback. 5. [ ] Build provider decoration helpers for `models.json`. @@ -929,7 +929,7 @@ Ordering for new sessions: - Keep extension loader root-to-compat alias until Phase 10, but expose the new collection/facade as the forward API. 10. [ ] Test migration and real-provider validation. - Unit tests for `FileCredentialStore`, config-value decorators, provider decoration, extension OAuth adapter, ModelRegistry async facade, and consumer rewiring. - - Regression tests for Cloudflare account/gateway metadata, Copilot OAuth baseUrl wrapping, runtime `--api-key` precedence, `$ENV`/`!command` resolution, and stored credential blocking ambient fallback. + - Regression tests for Cloudflare account/gateway env, Copilot OAuth baseUrl wrapping, runtime `--api-key` precedence, `$ENV`/`!command` resolution, and stored credential blocking ambient fallback. - Update existing tests that assume sync `ModelRegistry.getAll/find/getAvailable`. - Run targeted non-e2e suites plus tmux validation of login flows against real providers (Anthropic OAuth/API key, OpenAI Codex OAuth, GitHub Copilot OAuth, Cloudflare AI Gateway, Bedrock if credentials are available). diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 6a8d1ba5..69d3990d 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Breaking Changes + +- Changed `ApiKeyCredential` to use the `auth.json`-compatible discriminator `type: "api_key"` and provider-scoped `env` values instead of `type: "api-key"` and metadata. + ## [0.80.1] - 2026-06-23 ### Fixed diff --git a/packages/ai/README.md b/packages/ai/README.md index d600f648..00e01b34 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -361,6 +361,19 @@ const models = createModels({ credentials: myFileBackedStore }); The contract is small: `read(providerId)`, `modify(providerId, fn)` (the only write path — a serialized read-modify-write), and `delete(providerId)`. OAuth token refresh runs inside `modify`, so concurrent requests and processes cannot double-refresh a rotated token. A stored credential *owns* its provider: environment variables are only consulted when nothing is stored, and a failed refresh never silently falls back to an env key. +API-key credentials use the same discriminator as pi's `auth.json` and can carry provider-scoped env/config values: + +```typescript +const credential = { + type: 'api_key', + key: '...', + env: { + CLOUDFLARE_ACCOUNT_ID: 'account-id', + CLOUDFLARE_GATEWAY_ID: 'gateway-id' + } +} as const; +``` + ### Environment Variables Built-in providers resolve these env vars (Node.js; in browsers pass `apiKey` explicitly): diff --git a/packages/ai/src/auth/helpers.ts b/packages/ai/src/auth/helpers.ts index 0362c364..d9a34ad2 100644 --- a/packages/ai/src/auth/helpers.ts +++ b/packages/ai/src/auth/helpers.ts @@ -3,7 +3,7 @@ import type { ApiKeyAuth, OAuthAuth } from "./types.ts"; /** * Standard api-key auth: a stored credential key wins, otherwise the first * set env var resolves. Includes a `login` that prompts for the key. - * Providers with non-standard resolution (metadata, ambient files, IAM) + * Providers with non-standard resolution (provider env, ambient files, IAM) * write their own `ApiKeyAuth`. */ export function envApiKeyAuth(name: string, envVars: readonly string[]): ApiKeyAuth { @@ -11,7 +11,7 @@ export function envApiKeyAuth(name: string, envVars: readonly string[]): ApiKeyA name, login: async (callbacks) => { const key = await callbacks.prompt({ type: "secret", message: `Enter ${name}` }); - return { type: "api-key", key }; + return { type: "api_key", key }; }, resolve: async ({ ctx, credential }) => { if (credential?.key) return { auth: { apiKey: credential.key }, source: "stored credential" }; diff --git a/packages/ai/src/auth/resolve.ts b/packages/ai/src/auth/resolve.ts index 3adec6b4..8ed866fd 100644 --- a/packages/ai/src/auth/resolve.ts +++ b/packages/ai/src/auth/resolve.ts @@ -43,7 +43,7 @@ export async function resolveProviderAuth( if (stored.type === "oauth" && provider.auth.oauth) { return resolveStoredOAuth(credentials, provider.id, provider.auth.oauth, stored); } - if (stored.type === "api-key" && provider.auth.apiKey) { + if (stored.type === "api_key" && provider.auth.apiKey) { return resolveApiKey(authContext, provider.auth.apiKey, model, stored); } return undefined; diff --git a/packages/ai/src/auth/types.ts b/packages/ai/src/auth/types.ts index ffda2858..9710dbcb 100644 --- a/packages/ai/src/auth/types.ts +++ b/packages/ai/src/auth/types.ts @@ -12,13 +12,13 @@ export interface ModelAuth { } /** - * Stored api-key credential. `metadata` holds non-key values such as - * Cloudflare account/gateway ids. + * Stored api-key credential. `env` holds provider-scoped environment/config + * values such as Cloudflare account/gateway ids. */ export interface ApiKeyCredential { - type: "api-key"; + type: "api_key"; key?: string; - metadata?: Record; + env?: ProviderEnv; } /** Stored OAuth credential (`access`, `refresh`, `expires` from OAuthCredentials). */ @@ -123,19 +123,19 @@ export interface AuthLoginCallbacks { } /** - * Api-key auth: stored key/metadata plus ambient sources (env vars, AWS + * Api-key auth: stored key/provider env plus ambient sources (env vars, AWS * profiles, ADC files). Ambient-only providers omit `login`. */ export interface ApiKeyAuth { /** Display name, e.g. "Anthropic API key". */ name: string; - /** Interactive setup (prompt for key/metadata). Absent = ambient-only. */ + /** Interactive setup (prompt for key/provider env). Absent = ambient-only. */ login?(callbacks: AuthLoginCallbacks): Promise; /** * Resolve auth from the stored credential and/or ambient sources, merging - * per field (`credential.key ?? env("...")`, `metadata.accountId ?? env("...")`). + * per field (`credential.key ?? env("...")`, `credential.env?.NAME ?? env("...")`). * undefined = not configured. Receives the chat or image-generation model * the request is for (both carry `provider` and `baseUrl`). */ diff --git a/packages/ai/src/providers/cloudflare-auth.ts b/packages/ai/src/providers/cloudflare-auth.ts index 7bf909cd..511e8d73 100644 --- a/packages/ai/src/providers/cloudflare-auth.ts +++ b/packages/ai/src/providers/cloudflare-auth.ts @@ -7,16 +7,16 @@ const CLOUDFLARE_GATEWAY_ID = "CLOUDFLARE_GATEWAY_ID"; type CloudflareAuthKind = "workers-ai" | "ai-gateway"; -async function resolveValue(input: { - name: string; - ctx: AuthContext; - credential: ApiKeyCredential | undefined; -}): Promise { - if (input.credential) { - if (input.name === CLOUDFLARE_API_KEY) return input.credential.key; - return input.credential.metadata?.[input.name]; +async function resolveValue( + name: string, + ctx: AuthContext, + credential: ApiKeyCredential | undefined, +): Promise { + if (credential) { + if (name === CLOUDFLARE_API_KEY) return credential.key; + return credential.env?.[name]; } - return input.ctx.env(input.name); + return ctx.env(name); } function resolveCloudflareBaseUrl( @@ -29,20 +29,17 @@ function resolveCloudflareBaseUrl( .replaceAll(`{${CLOUDFLARE_GATEWAY_ID}}`, gatewayId ?? ""); } -async function resolveCloudflareEnv(input: { - kind: CloudflareAuthKind; - model: Model | ImagesModel; - ctx: AuthContext; - credential: ApiKeyCredential | undefined; -}): Promise<{ apiKey: string; env: ProviderEnv; baseUrl: string; source: string } | undefined> { - const apiKey = await resolveValue({ name: CLOUDFLARE_API_KEY, ctx: input.ctx, credential: input.credential }); - const accountId = await resolveValue({ name: CLOUDFLARE_ACCOUNT_ID, ctx: input.ctx, credential: input.credential }); - const gatewayId = - input.kind === "ai-gateway" - ? await resolveValue({ name: CLOUDFLARE_GATEWAY_ID, ctx: input.ctx, credential: input.credential }) - : undefined; +async function resolveCloudflareEnv( + kind: CloudflareAuthKind, + model: Model | ImagesModel, + ctx: AuthContext, + credential: ApiKeyCredential | undefined, +): Promise<{ apiKey: string; env: ProviderEnv; baseUrl: string; source: string } | undefined> { + const apiKey = await resolveValue(CLOUDFLARE_API_KEY, ctx, credential); + const accountId = await resolveValue(CLOUDFLARE_ACCOUNT_ID, ctx, credential); + const gatewayId = kind === "ai-gateway" ? await resolveValue(CLOUDFLARE_GATEWAY_ID, ctx, credential) : undefined; - if (!apiKey || !accountId || (input.kind === "ai-gateway" && !gatewayId)) return undefined; + if (!apiKey || !accountId || (kind === "ai-gateway" && !gatewayId)) return undefined; return { apiKey, @@ -50,8 +47,8 @@ async function resolveCloudflareEnv(input: { CLOUDFLARE_ACCOUNT_ID: accountId, ...(gatewayId ? { CLOUDFLARE_GATEWAY_ID: gatewayId } : {}), }, - baseUrl: resolveCloudflareBaseUrl(input.model, accountId, gatewayId), - source: input.credential ? "stored credential" : CLOUDFLARE_API_KEY, + baseUrl: resolveCloudflareBaseUrl(model, accountId, gatewayId), + source: credential ? "stored credential" : CLOUDFLARE_API_KEY, }; } @@ -61,10 +58,10 @@ export function cloudflareWorkersAIAuth(): ApiKeyAuth { login: async (callbacks) => { const key = await callbacks.prompt({ type: "secret", message: "Enter Cloudflare API key" }); const accountId = await callbacks.prompt({ type: "text", message: "Enter Cloudflare account ID" }); - return { type: "api-key", key, metadata: { CLOUDFLARE_ACCOUNT_ID: accountId } }; + return { type: "api_key", key, env: { CLOUDFLARE_ACCOUNT_ID: accountId } }; }, resolve: async ({ model, ctx, credential }) => { - const resolved = await resolveCloudflareEnv({ kind: "workers-ai", model, ctx, credential }); + const resolved = await resolveCloudflareEnv("workers-ai", model, ctx, credential); if (!resolved) return undefined; return { auth: { apiKey: resolved.apiKey, baseUrl: resolved.baseUrl }, @@ -83,13 +80,13 @@ export function cloudflareAIGatewayAuth(): ApiKeyAuth { const accountId = await callbacks.prompt({ type: "text", message: "Enter Cloudflare account ID" }); const gatewayId = await callbacks.prompt({ type: "text", message: "Enter Cloudflare AI Gateway ID" }); return { - type: "api-key", + type: "api_key", key, - metadata: { CLOUDFLARE_ACCOUNT_ID: accountId, CLOUDFLARE_GATEWAY_ID: gatewayId }, + env: { CLOUDFLARE_ACCOUNT_ID: accountId, CLOUDFLARE_GATEWAY_ID: gatewayId }, }; }, resolve: async ({ model, ctx, credential }) => { - const resolved = await resolveCloudflareEnv({ kind: "ai-gateway", model, ctx, credential }); + const resolved = await resolveCloudflareEnv("ai-gateway", model, ctx, credential); if (!resolved) return undefined; return { auth: { diff --git a/packages/ai/test/models-runtime.test.ts b/packages/ai/test/models-runtime.test.ts index cafcb17f..f55e035e 100644 --- a/packages/ai/test/models-runtime.test.ts +++ b/packages/ai/test/models-runtime.test.ts @@ -225,7 +225,7 @@ describe("Models runtime", () => { expect(resolution?.source).toBe("OAuth"); // stored api-key credential resolves through apiKey auth, beats env - await credentials.modify("p1", async () => ({ type: "api-key", key: "stored-key" })); + await credentials.modify("p1", async () => ({ type: "api_key", key: "stored-key" })); const apiKeyResolution = await models.getAuth(model); expect(apiKeyResolution?.auth.apiKey).toBe("stored-key"); expect(apiKeyResolution?.source).toBe("stored"); diff --git a/packages/ai/test/providers.test.ts b/packages/ai/test/providers.test.ts index e3e8c66d..0f6e25bd 100644 --- a/packages/ai/test/providers.test.ts +++ b/packages/ai/test/providers.test.ts @@ -149,7 +149,7 @@ describe("envApiKeyAuth", () => { const stored = await auth.resolve({ model, ctx: fakeAuthContext({ FIRST_KEY: "env" }), - credential: { type: "api-key", key: "stored" }, + credential: { type: "api_key", key: "stored" }, }); expect(stored?.auth.apiKey).toBe("stored"); expect(stored?.source).toBe("stored credential"); @@ -170,7 +170,7 @@ describe("envApiKeyAuth", () => { }, notify: () => {}, }); - expect(credential).toEqual({ type: "api-key", key: "entered-key" }); + expect(credential).toEqual({ type: "api_key", key: "entered-key" }); }); }); From ef231c49108a3d0dc9d1b6c852e5468377966f24 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 23 Jun 2026 23:27:00 +0200 Subject: [PATCH 111/122] fix(ai): resolve request-scoped auth before provider calls closes #6021 --- packages/ai/CHANGELOG.md | 1 + packages/ai/src/auth/resolve.ts | 30 ++++++++++++++++++++++--- packages/ai/src/images-models.ts | 5 ++++- packages/ai/src/models.ts | 11 ++++++++- packages/ai/test/models-runtime.test.ts | 24 ++++++++++++++++++++ 5 files changed, 66 insertions(+), 5 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 2b9f13f2..10e3cce7 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed request-scoped `apiKey` and `env` values to participate in provider auth resolution, so providers such as Cloudflare can derive request-specific base URLs from explicit call options ([#6021](https://github.com/earendil-works/pi/issues/6021)). - Restored temporary legacy per-API stream aliases such as `streamSimpleOpenAICompletions` on the compat entrypoint ([#6016](https://github.com/earendil-works/pi/issues/6016), [#6017](https://github.com/earendil-works/pi/issues/6017)). ## [0.80.1] - 2026-06-23 diff --git a/packages/ai/src/auth/resolve.ts b/packages/ai/src/auth/resolve.ts index 8ed866fd..81d7a270 100644 --- a/packages/ai/src/auth/resolve.ts +++ b/packages/ai/src/auth/resolve.ts @@ -1,4 +1,4 @@ -import type { Api, ImagesApi, ImagesModel, Model } from "../types.ts"; +import type { Api, ImagesApi, ImagesModel, Model, ProviderEnv } from "../types.ts"; import type { ApiKeyAuth, ApiKeyCredential, @@ -13,6 +13,11 @@ import type { export type ModelsErrorCode = "model_source" | "model_validation" | "provider" | "stream" | "auth" | "oauth"; +export interface AuthResolutionOverrides { + apiKey?: string; + env?: ProviderEnv; +} + export class ModelsError extends Error { readonly code: ModelsErrorCode; @@ -37,20 +42,39 @@ export async function resolveProviderAuth( model: AuthModel, credentials: CredentialStore, authContext: AuthContext, + overrides?: AuthResolutionOverrides, ): Promise { + const requestAuthContext = overrides?.env ? overlayEnvAuthContext(authContext, overrides.env) : authContext; + + if (overrides?.apiKey !== undefined && provider.auth.apiKey) { + return resolveApiKey(requestAuthContext, provider.auth.apiKey, model, { + type: "api_key", + key: overrides.apiKey, + env: overrides.env, + }); + } + const stored = await readCredential(credentials, provider.id); if (stored) { if (stored.type === "oauth" && provider.auth.oauth) { return resolveStoredOAuth(credentials, provider.id, provider.auth.oauth, stored); } if (stored.type === "api_key" && provider.auth.apiKey) { - return resolveApiKey(authContext, provider.auth.apiKey, model, stored); + const credential = overrides?.env ? { ...stored, env: { ...stored.env, ...overrides.env } } : stored; + return resolveApiKey(requestAuthContext, provider.auth.apiKey, model, credential); } return undefined; } // Ambient (env vars, AWS profiles, ADC files). - return provider.auth.apiKey ? resolveApiKey(authContext, provider.auth.apiKey, model, undefined) : undefined; + return provider.auth.apiKey ? resolveApiKey(requestAuthContext, provider.auth.apiKey, model, undefined) : undefined; +} + +function overlayEnvAuthContext(base: AuthContext, env: ProviderEnv): AuthContext { + return { + env: async (name) => env[name] || (await base.env(name)), + fileExists: (path) => base.fileExists(path), + }; } /** diff --git a/packages/ai/src/images-models.ts b/packages/ai/src/images-models.ts index 8dd655d4..0ca5f2da 100644 --- a/packages/ai/src/images-models.ts +++ b/packages/ai/src/images-models.ts @@ -184,7 +184,10 @@ class ImagesModelsImpl implements MutableImagesModels { throw new ModelsError("provider", `Unknown provider: ${model.provider}`); } - const resolution = await this.getAuth(model); + const resolution = await resolveProviderAuth(provider, model, this.credentials, this.authContext, { + apiKey: options?.apiKey, + env: options?.env, + }); const auth = resolution?.auth; if (!auth) { return provider.generateImages(model, context, options); diff --git a/packages/ai/src/models.ts b/packages/ai/src/models.ts index 669b1525..f9cf27d3 100644 --- a/packages/ai/src/models.ts +++ b/packages/ai/src/models.ts @@ -231,7 +231,16 @@ class ModelsImpl implements MutableModels { model: Model, options: TOptions | undefined, ): Promise<{ requestModel: Model; requestOptions: TOptions | undefined }> { - const resolution = await this.getAuth(model); + const resolution = await resolveProviderAuth( + this.requireProvider(model), + model, + this.credentials, + this.authContext, + { + apiKey: options?.apiKey, + env: options?.env, + }, + ); const auth = resolution?.auth; if (!auth) return { requestModel: model, requestOptions: options }; diff --git a/packages/ai/test/models-runtime.test.ts b/packages/ai/test/models-runtime.test.ts index f55e035e..7f805e26 100644 --- a/packages/ai/test/models-runtime.test.ts +++ b/packages/ai/test/models-runtime.test.ts @@ -360,6 +360,30 @@ describe("Models runtime", () => { await expect(models.getAuth(testModel("p1", "model-a"))).rejects.toMatchObject({ code: "auth" }); }); + it("uses explicit request api key and env during provider auth resolution", async () => { + const calls: ProviderCall[] = []; + const apiKey: ApiKeyAuth = { + name: "Scoped", + resolve: async ({ credential, ctx }) => { + const account = credential?.env?.ACCOUNT_ID ?? (await ctx.env("ACCOUNT_ID")); + if (!credential?.key || !account) return undefined; + return { + auth: { apiKey: credential.key, baseUrl: `https://example.test/${account}` }, + env: { ACCOUNT_ID: account }, + }; + }, + }; + const models = createModels(); + models.setProvider(testProvider({ id: "p1", auth: { apiKey }, calls })); + const model = testModel("p1", "model-a"); + + await models.completeSimple(model, context, { apiKey: "explicit-key", env: { ACCOUNT_ID: "acct" } }); + + expect(calls[0].model.baseUrl).toBe("https://example.test/acct"); + expect(calls[0].options?.apiKey).toBe("explicit-key"); + expect(calls[0].options?.env).toEqual({ ACCOUNT_ID: "acct" }); + }); + it("merges resolved auth into stream options; explicit options win per field", async () => { const calls: ProviderCall[] = []; const apiKey: ApiKeyAuth = { From e1a2dc04f422d14cb297d6347f083a55a049c022 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 23 Jun 2026 23:29:44 +0200 Subject: [PATCH 112/122] fix(ai): restore detectCompat runtime fallback in openai-completions refs #6020 --- packages/ai/CHANGELOG.md | 1 + packages/ai/src/api/openai-completions.ts | 148 ++++++++++++++++------ 2 files changed, 109 insertions(+), 40 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 10e3cce7..be14fead 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -10,6 +10,7 @@ - Fixed request-scoped `apiKey` and `env` values to participate in provider auth resolution, so providers such as Cloudflare can derive request-specific base URLs from explicit call options ([#6021](https://github.com/earendil-works/pi/issues/6021)). - Restored temporary legacy per-API stream aliases such as `streamSimpleOpenAICompletions` on the compat entrypoint ([#6016](https://github.com/earendil-works/pi/issues/6016), [#6017](https://github.com/earendil-works/pi/issues/6017)). +- Restored runtime `detectCompat` fallback in `openai-completions` for models without explicit compat metadata ([#6020](https://github.com/earendil-works/pi/issues/6020)). ## [0.80.1] - 2026-06-23 diff --git a/packages/ai/src/api/openai-completions.ts b/packages/ai/src/api/openai-completions.ts index 5204815f..e916c52a 100644 --- a/packages/ai/src/api/openai-completions.ts +++ b/packages/ai/src/api/openai-completions.ts @@ -1163,55 +1163,123 @@ function mapStopReason(reason: ChatCompletionChunk.Choice["finish_reason"] | str } } -const DEFAULT_COMPAT: ResolvedOpenAICompletionsCompat = { - supportsStore: true, - supportsDeveloperRole: true, - supportsReasoningEffort: true, - supportsUsageInStreaming: true, - maxTokensField: "max_completion_tokens", - requiresToolResultName: false, - requiresAssistantAfterToolResult: false, - requiresThinkingAsText: false, - requiresReasoningContentOnAssistantMessages: false, - thinkingFormat: "openai", - openRouterRouting: {}, - vercelGatewayRouting: {}, - chatTemplateKwargs: {}, - zaiToolStream: false, - supportsStrictMode: true, - cacheControlFormat: undefined, - sendSessionAffinityHeaders: false, - supportsLongCacheRetention: true, -}; +/** + * Auto-detect compatibility settings from provider name and baseUrl. + * Used as the base when model.compat is not set; explicit model.compat + * entries override these detected values. + */ +function detectCompat(model: Model<"openai-completions">): ResolvedOpenAICompletionsCompat { + const provider = model.provider; + const baseUrl = model.baseUrl; + + const isZai = + provider === "zai" || + provider === "zai-coding-cn" || + baseUrl.includes("api.z.ai") || + baseUrl.includes("open.bigmodel.cn"); + const isTogether = + provider === "together" || baseUrl.includes("api.together.ai") || baseUrl.includes("api.together.xyz"); + const isMoonshot = provider === "moonshotai" || provider === "moonshotai-cn" || baseUrl.includes("api.moonshot."); + const isOpenRouter = provider === "openrouter" || baseUrl.includes("openrouter.ai"); + const isCloudflareWorkersAI = provider === "cloudflare-workers-ai" || baseUrl.includes("api.cloudflare.com"); + const isCloudflareAiGateway = provider === "cloudflare-ai-gateway" || baseUrl.includes("gateway.ai.cloudflare.com"); + const isNvidia = provider === "nvidia" || baseUrl.includes("integrate.api.nvidia.com"); + const isAntLing = provider === "ant-ling" || baseUrl.includes("api.ant-ling.com"); + + const isNonStandard = + isNvidia || + provider === "cerebras" || + baseUrl.includes("cerebras.ai") || + provider === "xai" || + baseUrl.includes("api.x.ai") || + isTogether || + baseUrl.includes("chutes.ai") || + baseUrl.includes("deepseek.com") || + isZai || + isMoonshot || + provider === "opencode" || + baseUrl.includes("opencode.ai") || + isCloudflareWorkersAI || + isCloudflareAiGateway || + isAntLing; + + const useMaxTokens = + baseUrl.includes("chutes.ai") || isMoonshot || isCloudflareAiGateway || isTogether || isNvidia || isAntLing; + + const isGrok = provider === "xai" || baseUrl.includes("api.x.ai"); + const isDeepSeek = provider === "deepseek" || baseUrl.includes("deepseek.com"); + const isOpenRouterDeveloperRoleModel = + isOpenRouter && (model.id.startsWith("anthropic/") || model.id.startsWith("openai/")); + const cacheControlFormat = provider === "openrouter" && model.id.startsWith("anthropic/") ? "anthropic" : undefined; + + return { + supportsStore: !isNonStandard, + supportsDeveloperRole: isOpenRouterDeveloperRoleModel || (!isNonStandard && !isOpenRouter), + supportsReasoningEffort: + !isGrok && !isZai && !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia && !isAntLing, + supportsUsageInStreaming: true, + maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens", + requiresToolResultName: false, + requiresAssistantAfterToolResult: false, + requiresThinkingAsText: false, + requiresReasoningContentOnAssistantMessages: isDeepSeek, + thinkingFormat: isDeepSeek + ? "deepseek" + : isZai + ? "zai" + : isTogether + ? "together" + : isAntLing + ? "ant-ling" + : isOpenRouter + ? "openrouter" + : "openai", + openRouterRouting: {}, + vercelGatewayRouting: {}, + chatTemplateKwargs: {}, + zaiToolStream: false, + supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia, + cacheControlFormat, + sendSessionAffinityHeaders: false, + supportsLongCacheRetention: !( + isTogether || + isCloudflareWorkersAI || + isCloudflareAiGateway || + isNvidia || + isAntLing + ), + }; +} /** * Get resolved compatibility settings for a model. - * Uses explicit generated/custom model.compat over OpenAI-standard defaults. + * Auto-detects from provider/URL then overrides with explicit model.compat. */ function getCompat(model: Model<"openai-completions">): ResolvedOpenAICompletionsCompat { - if (!model.compat) return DEFAULT_COMPAT; + const detected = detectCompat(model); + if (!model.compat) return detected; return { - supportsStore: model.compat.supportsStore ?? DEFAULT_COMPAT.supportsStore, - supportsDeveloperRole: model.compat.supportsDeveloperRole ?? DEFAULT_COMPAT.supportsDeveloperRole, - supportsReasoningEffort: model.compat.supportsReasoningEffort ?? DEFAULT_COMPAT.supportsReasoningEffort, - supportsUsageInStreaming: model.compat.supportsUsageInStreaming ?? DEFAULT_COMPAT.supportsUsageInStreaming, - maxTokensField: model.compat.maxTokensField ?? DEFAULT_COMPAT.maxTokensField, - requiresToolResultName: model.compat.requiresToolResultName ?? DEFAULT_COMPAT.requiresToolResultName, + supportsStore: model.compat.supportsStore ?? detected.supportsStore, + supportsDeveloperRole: model.compat.supportsDeveloperRole ?? detected.supportsDeveloperRole, + supportsReasoningEffort: model.compat.supportsReasoningEffort ?? detected.supportsReasoningEffort, + supportsUsageInStreaming: model.compat.supportsUsageInStreaming ?? detected.supportsUsageInStreaming, + maxTokensField: model.compat.maxTokensField ?? detected.maxTokensField, + requiresToolResultName: model.compat.requiresToolResultName ?? detected.requiresToolResultName, requiresAssistantAfterToolResult: - model.compat.requiresAssistantAfterToolResult ?? DEFAULT_COMPAT.requiresAssistantAfterToolResult, - requiresThinkingAsText: model.compat.requiresThinkingAsText ?? DEFAULT_COMPAT.requiresThinkingAsText, + model.compat.requiresAssistantAfterToolResult ?? detected.requiresAssistantAfterToolResult, + requiresThinkingAsText: model.compat.requiresThinkingAsText ?? detected.requiresThinkingAsText, requiresReasoningContentOnAssistantMessages: model.compat.requiresReasoningContentOnAssistantMessages ?? - DEFAULT_COMPAT.requiresReasoningContentOnAssistantMessages, - thinkingFormat: model.compat.thinkingFormat ?? DEFAULT_COMPAT.thinkingFormat, - openRouterRouting: model.compat.openRouterRouting ?? DEFAULT_COMPAT.openRouterRouting, - vercelGatewayRouting: model.compat.vercelGatewayRouting ?? DEFAULT_COMPAT.vercelGatewayRouting, - chatTemplateKwargs: model.compat.chatTemplateKwargs ?? DEFAULT_COMPAT.chatTemplateKwargs, - zaiToolStream: model.compat.zaiToolStream ?? DEFAULT_COMPAT.zaiToolStream, - supportsStrictMode: model.compat.supportsStrictMode ?? DEFAULT_COMPAT.supportsStrictMode, - cacheControlFormat: model.compat.cacheControlFormat ?? DEFAULT_COMPAT.cacheControlFormat, - sendSessionAffinityHeaders: model.compat.sendSessionAffinityHeaders ?? DEFAULT_COMPAT.sendSessionAffinityHeaders, - supportsLongCacheRetention: model.compat.supportsLongCacheRetention ?? DEFAULT_COMPAT.supportsLongCacheRetention, + detected.requiresReasoningContentOnAssistantMessages, + thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat, + openRouterRouting: model.compat.openRouterRouting ?? {}, + vercelGatewayRouting: model.compat.vercelGatewayRouting ?? detected.vercelGatewayRouting, + chatTemplateKwargs: model.compat.chatTemplateKwargs ?? detected.chatTemplateKwargs, + zaiToolStream: model.compat.zaiToolStream ?? detected.zaiToolStream, + supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode, + cacheControlFormat: model.compat.cacheControlFormat ?? detected.cacheControlFormat, + sendSessionAffinityHeaders: model.compat.sendSessionAffinityHeaders ?? detected.sendSessionAffinityHeaders, + supportsLongCacheRetention: model.compat.supportsLongCacheRetention ?? detected.supportsLongCacheRetention, }; } From 9096d5f9a6ef74c3d3f6c23611b1fc50e347df46 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 23 Jun 2026 23:42:02 +0200 Subject: [PATCH 113/122] docs: update changelog entries --- packages/agent/CHANGELOG.md | 4 ++++ packages/ai/CHANGELOG.md | 3 ++- packages/coding-agent/CHANGELOG.md | 12 ++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 88ccede7..4edd3267 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Renamed the public harness shell execution options type from `ExecutionEnvExecOptions` to `ShellExecOptions`. + ## [0.80.1] - 2026-06-23 ## [0.80.0] - 2026-06-23 diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index be14fead..ae1d73db 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,12 +2,13 @@ ## [Unreleased] -### Breaking Changes +### Changed - Changed `ApiKeyCredential` to use the `auth.json`-compatible discriminator `type: "api_key"` and provider-scoped `env` values instead of `type: "api-key"` and metadata. ### Fixed +- Fixed Anthropic-compatible custom models to use explicit compatibility metadata instead of provider-name heuristics for session-affinity headers and unsupported tool-field omissions. - Fixed request-scoped `apiKey` and `env` values to participate in provider auth resolution, so providers such as Cloudflare can derive request-specific base URLs from explicit call options ([#6021](https://github.com/earendil-works/pi/issues/6021)). - Restored temporary legacy per-API stream aliases such as `streamSimpleOpenAICompletions` on the compat entrypoint ([#6016](https://github.com/earendil-works/pi/issues/6016), [#6017](https://github.com/earendil-works/pi/issues/6017)). - Restored runtime `detectCompat` fallback in `openai-completions` for models without explicit compat metadata ([#6020](https://github.com/earendil-works/pi/issues/6020)). diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 257a54e7..907f2540 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,18 @@ ## [Unreleased] +### Changed + +- Changed inherited pi-ai `ApiKeyCredential` to use the `auth.json`-compatible discriminator `type: "api_key"` and provider-scoped `env` values instead of `type: "api-key"` and metadata. +- Renamed the inherited agent-core public harness shell execution options type from `ExecutionEnvExecOptions` to `ShellExecOptions`. + +### Fixed + +- Fixed inherited Anthropic-compatible custom models to use explicit compatibility metadata instead of provider-name heuristics for session-affinity headers and unsupported tool-field omissions. +- Fixed inherited request-scoped `apiKey` and `env` values to participate in provider auth resolution, so providers such as Cloudflare can derive request-specific base URLs from explicit call options ([#6021](https://github.com/earendil-works/pi/issues/6021)). +- Restored inherited temporary legacy per-API stream aliases such as `streamSimpleOpenAICompletions` on the pi-ai compat entrypoint ([#6016](https://github.com/earendil-works/pi/issues/6016), [#6017](https://github.com/earendil-works/pi/issues/6017)). +- Restored inherited runtime `detectCompat` fallback in `openai-completions` for models without explicit compat metadata ([#6020](https://github.com/earendil-works/pi/issues/6020)). + ## [0.80.1] - 2026-06-23 ### Fixed From 0201806adfa825ab3d7957a4267d46e5030fd357 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 23 Jun 2026 23:45:36 +0200 Subject: [PATCH 114/122] Release v0.80.2 --- package-lock.json | 26 +++++++++---------- packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 +-- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- .../ai/src/providers/openrouter.models.ts | 8 +++--- packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +-- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/gondolin/package-lock.json | 4 +-- .../examples/extensions/gondolin/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +-- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +-- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/npm-shrinkwrap.json | 24 ++++++++--------- packages/coding-agent/package.json | 8 +++--- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- 20 files changed, 54 insertions(+), 54 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9fb93a6d..40227833 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5111,10 +5111,10 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.80.1", + "version": "0.80.2", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.80.1", + "@earendil-works/pi-ai": "^0.80.2", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -5463,7 +5463,7 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.80.1", + "version": "0.80.2", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -5769,12 +5769,12 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.80.1", + "version": "0.80.2", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.80.1", - "@earendil-works/pi-ai": "^0.80.1", - "@earendil-works/pi-tui": "^0.80.1", + "@earendil-works/pi-agent-core": "^0.80.2", + "@earendil-works/pi-ai": "^0.80.2", + "@earendil-works/pi-tui": "^0.80.2", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -5815,32 +5815,32 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.80.1", + "version": "0.80.2", "dependencies": { "@anthropic-ai/sdk": "0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.80.1" + "version": "0.80.2" }, "packages/coding-agent/examples/extensions/gondolin": { "name": "pi-extension-gondolin", - "version": "0.80.1", + "version": "0.80.2", "dependencies": { "@earendil-works/gondolin": "0.12.0" } }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.10.1", + "version": "1.10.2", "dependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.80.1", + "version": "0.80.2", "dependencies": { "ms": "2.1.3" }, @@ -6136,7 +6136,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.80.1", + "version": "0.80.2", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 4edd3267..191710ce 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.80.2] - 2026-06-23 ### Changed diff --git a/packages/agent/package.json b/packages/agent/package.json index b58662d1..7190e909 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.80.1", + "version": "0.80.2", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -29,7 +29,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.80.1", + "@earendil-works/pi-ai": "^0.80.2", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index ae1d73db..0a72cf94 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.80.2] - 2026-06-23 ### Changed diff --git a/packages/ai/package.json b/packages/ai/package.json index 778f8fe5..20858b7a 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.80.1", + "version": "0.80.2", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/ai/src/providers/openrouter.models.ts b/packages/ai/src/providers/openrouter.models.ts index 53326347..0dad51de 100644 --- a/packages/ai/src/providers/openrouter.models.ts +++ b/packages/ai/src/providers/openrouter.models.ts @@ -1859,13 +1859,13 @@ export const OPENROUTER_MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.68, - output: 3.41, - cacheRead: 0.144, + input: 0.74, + output: 3.5, + cacheRead: 0.15, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262144, + maxTokens: 16384, } satisfies Model<"openai-completions">, "nvidia/llama-3.3-nemotron-super-49b-v1.5": { id: "nvidia/llama-3.3-nemotron-super-49b-v1.5", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 907f2540..4c1fc242 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.80.2] - 2026-06-23 ### Changed diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index 2b78cfbd..907489ab 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.80.1", + "version": "0.80.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.80.1", + "version": "0.80.2", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index c0faf5ff..4820c289 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.80.1", + "version": "0.80.2", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index d5b5cca8..38f4684a 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.80.1", + "version": "0.80.2", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/gondolin/package-lock.json b/packages/coding-agent/examples/extensions/gondolin/package-lock.json index 2edc9b61..b29a04e9 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package-lock.json +++ b/packages/coding-agent/examples/extensions/gondolin/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-gondolin", - "version": "0.80.1", + "version": "0.80.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-gondolin", - "version": "0.80.1", + "version": "0.80.2", "dependencies": { "@earendil-works/gondolin": "0.12.0" } diff --git a/packages/coding-agent/examples/extensions/gondolin/package.json b/packages/coding-agent/examples/extensions/gondolin/package.json index 041ba0c6..9cbb476e 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package.json +++ b/packages/coding-agent/examples/extensions/gondolin/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-gondolin", "private": true, - "version": "0.80.1", + "version": "0.80.2", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 5c79587e..71528853 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.10.1", + "version": "1.10.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.10.1", + "version": "1.10.2", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index c090d1a0..f3cc2ac4 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.10.1", + "version": "1.10.2", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index 8bf67b2d..0eb59503 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.80.1", + "version": "0.80.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.80.1", + "version": "0.80.2", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index 9a2fbb46..53e2272f 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.80.1", + "version": "0.80.2", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index 19df1779..467adb3b 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -1,17 +1,17 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.80.1", + "version": "0.80.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent", - "version": "0.80.1", + "version": "0.80.2", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.80.1", - "@earendil-works/pi-ai": "^0.80.1", - "@earendil-works/pi-tui": "^0.80.1", + "@earendil-works/pi-agent-core": "^0.80.2", + "@earendil-works/pi-ai": "^0.80.2", + "@earendil-works/pi-tui": "^0.80.2", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -474,11 +474,11 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.80.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.80.1.tgz", + "version": "0.80.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.80.2.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.80.1", + "@earendil-works/pi-ai": "^0.80.2", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -488,8 +488,8 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.80.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.80.1.tgz", + "version": "0.80.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.80.2.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -512,8 +512,8 @@ } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.80.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.80.1.tgz", + "version": "0.80.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.80.2.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 5404261c..8175de4f 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.80.1", + "version": "0.80.2", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -36,9 +36,9 @@ "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.80.1", - "@earendil-works/pi-ai": "^0.80.1", - "@earendil-works/pi-tui": "^0.80.1", + "@earendil-works/pi-agent-core": "^0.80.2", + "@earendil-works/pi-ai": "^0.80.2", + "@earendil-works/pi-tui": "^0.80.2", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 1b9fba50..d5478cfe 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.80.2] - 2026-06-23 ## [0.80.1] - 2026-06-23 diff --git a/packages/tui/package.json b/packages/tui/package.json index 20dcb8e4..e407a72d 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.80.1", + "version": "0.80.2", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", From 8277bd68968d359dfd95726cc7bbc2fb3cc1aeeb Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 23 Jun 2026 23:45:39 +0200 Subject: [PATCH 115/122] Add [Unreleased] section for next cycle --- packages/agent/CHANGELOG.md | 2 ++ packages/ai/CHANGELOG.md | 2 ++ packages/coding-agent/CHANGELOG.md | 2 ++ packages/tui/CHANGELOG.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 191710ce..bec41594 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.80.2] - 2026-06-23 ### Changed diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 0a72cf94..b03b0353 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.80.2] - 2026-06-23 ### Changed diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 4c1fc242..a432a068 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.80.2] - 2026-06-23 ### Changed diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index d5478cfe..2823c818 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.80.2] - 2026-06-23 ## [0.80.1] - 2026-06-23 From 954ec998140aa2aa66bb861ad1ffa09c47b3ad15 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 23 Jun 2026 23:50:12 +0200 Subject: [PATCH 116/122] fix: upload release assets from visible directory --- .github/workflows/build-binaries.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 9fcb4278..3b8d53ca 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -56,11 +56,11 @@ jobs: run: | set -euo pipefail - mkdir -p .release-assets + mkdir -p release-assets VERSION="${RELEASE_TAG}" VERSION="${VERSION#v}" # Remove 'v' prefix - node scripts/release-notes.mjs extract --version "${VERSION}" --tag "${RELEASE_TAG}" --out .release-assets/RELEASE_NOTES.md + node scripts/release-notes.mjs extract --version "${VERSION}" --tag "${RELEASE_TAG}" --out release-assets/RELEASE_NOTES.md cd packages/coding-agent/binaries @@ -77,14 +77,14 @@ jobs: test -f "${asset}" done - sha256sum "${release_assets[@]}" > "${GITHUB_WORKSPACE}/.release-assets/SHA256SUMS" - cp "${release_assets[@]}" "${GITHUB_WORKSPACE}/.release-assets/" + sha256sum "${release_assets[@]}" > "${GITHUB_WORKSPACE}/release-assets/SHA256SUMS" + cp "${release_assets[@]}" "${GITHUB_WORKSPACE}/release-assets/" - name: Upload GitHub release payload uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: release-assets-${{ env.RELEASE_TAG }} - path: .release-assets/* + path: release-assets/* if-no-files-found: error retention-days: 14 From 978202765f1037a33fed61a74645ba0afb05b8b4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 23 Jun 2026 23:54:34 +0200 Subject: [PATCH 117/122] fix: remove OpenClaw gate --- .github/workflows/openclaw-gate.yml | 122 ---------------------------- 1 file changed, 122 deletions(-) delete mode 100644 .github/workflows/openclaw-gate.yml diff --git a/.github/workflows/openclaw-gate.yml b/.github/workflows/openclaw-gate.yml deleted file mode 100644 index 126d4ebf..00000000 --- a/.github/workflows/openclaw-gate.yml +++ /dev/null @@ -1,122 +0,0 @@ -name: OpenClaw Gate - -on: - issues: - types: [opened] - pull_request_target: - types: [opened] - -jobs: - check-contributor: - runs-on: ubuntu-latest - permissions: - contents: read - issues: write - pull-requests: write - steps: - - name: Check contributor - uses: actions/github-script@v7 - with: - script: | - const isPR = !!context.payload.pull_request; - const author = isPR - ? context.payload.pull_request.user.login - : context.payload.issue.user.login; - const number = isPR - ? context.payload.pull_request.number - : context.payload.issue.number; - const defaultBranch = context.payload.repository.default_branch; - - if (author.endsWith('[bot]') || author === 'dependabot[bot]') { - console.log(`Skipping bot: ${author}`); - return; - } - - const APPROVED_FILE = '.github/APPROVED_CONTRIBUTORS'; - const VALID_CAPABILITIES = new Set(['issue', 'pr']); - - // --- Check APPROVED_CONTRIBUTORS --- - async function getTextFile(path) { - const { data } = await github.rest.repos.getContent({ - owner: context.repo.owner, - repo: context.repo.repo, - path, - ref: defaultBranch, - }); - if (!('content' in data) || typeof data.content !== 'string') { - throw new Error(`Expected file content for ${path}`); - } - return Buffer.from(data.content, 'base64').toString('utf8'); - } - - try { - const content = await getTextFile(APPROVED_FILE); - const approved = new Map(); - for (const rawLine of content.split('\n')) { - const line = rawLine.trim(); - if (!line || line.startsWith('#')) continue; - - const parts = line.split(/\s+/); - if (parts.length !== 2) continue; - - const [username, capability] = parts; - const normalizedCapability = capability.toLowerCase(); - if (!VALID_CAPABILITIES.has(normalizedCapability)) continue; - - approved.set(username.toLowerCase(), normalizedCapability); - } - - if (approved.has(author.toLowerCase())) { - console.log(`${author} is in APPROVED_CONTRIBUTORS, passing`); - return; - } - } catch (err) { - console.log(`Could not read APPROVED_CONTRIBUTORS: ${err.message}`); - } - - // --- Also pass collaborators with write+ access --- - try { - const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: context.repo.owner, - repo: context.repo.repo, - username: author, - }); - if (['admin', 'maintain', 'write'].includes(perm.permission)) { - console.log(`${author} is a collaborator (${perm.permission}), passing`); - return; - } - } catch { - // not a collaborator - } - - // --- Check if user opened issues/PRs on openclaw/openclaw --- - async function hasOpenClawActivity(username) { - try { - const { data } = await github.rest.search.issuesAndPullRequests({ - q: `repo:openclaw/openclaw author:${username}`, - per_page: 1, - }); - if (data.total_count > 0) { - console.log(`${username} has opened ${data.total_count} issues/PRs on openclaw/openclaw`); - return true; - } - } catch (err) { - console.log(`Search failed: ${err.message}`); - } - return false; - } - - const hasActivity = await hasOpenClawActivity(author); - if (!hasActivity) { - console.log(`${author} has no openclaw/openclaw activity, passing`); - return; - } - - // --- Add openclaw label --- - console.log(`${author} has openclaw/openclaw activity, adding label`); - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: number, - labels: ['possibly-openclaw-clanker'], - }); From ec6311beb5b24fc918e5031173608447582d7262 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 23 Jun 2026 23:58:02 +0200 Subject: [PATCH 118/122] fix: skip dirty check before npm publish --- .github/workflows/build-binaries.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 3b8d53ca..dbeb6e53 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -212,9 +212,6 @@ jobs: - name: Test run: npm test - - name: Verify release artifacts are committed - run: git diff --exit-code - - name: Upgrade npm for trusted publishing run: | npm install -g npm@11.16.0 --ignore-scripts From 6338661485a0c71d430e391c2d92833212bbfb85 Mon Sep 17 00:00:00 2001 From: Alexey Zaytsev Date: Wed, 24 Jun 2026 02:53:40 -0500 Subject: [PATCH 119/122] fix(coding-agent): print benchmark timings after TUI stop (#6030) closes #6029 --- packages/coding-agent/src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 50a19bba..bbff87e7 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -810,9 +810,9 @@ export async function main(args: string[], options?: MainOptions) { if (startupBenchmark) { await interactiveMode.init(); time("interactiveMode.init"); - printTimings(); interactiveMode.stop(); stopThemeWatcher(); + printTimings(); if (process.stdout.writableLength > 0) { await new Promise((resolve) => process.stdout.once("drain", resolve)); } From c5440162b87b210b498857985765ba42279603a8 Mon Sep 17 00:00:00 2001 From: haoqixu Date: Wed, 24 Jun 2026 20:12:56 +0800 Subject: [PATCH 120/122] fix(coding-agent): show resources before messages when resuming session --- .../src/modes/interactive/interactive-mode.ts | 44 ++++++---- .../test/interactive-mode-status.test.ts | 40 +++++---- .../5943-session-start-notify.test.ts | 83 +++++++++++++++++++ 3 files changed, 134 insertions(+), 33 deletions(-) diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index fe733f20..7b6dbbe8 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -265,6 +265,7 @@ export interface InteractiveModeOptions { export class InteractiveMode { private runtimeHost: AgentSessionRuntime; private ui: TUI; + private loadedResourcesContainer: Container; private chatContainer: Container; private pendingMessagesContainer: Container; private statusContainer: Container; @@ -401,6 +402,7 @@ export class InteractiveMode { this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor()); this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink()); this.headerContainer = new Container(); + this.loadedResourcesContainer = new Container(); this.chatContainer = new Container(); this.pendingMessagesContainer = new Container(); this.statusContainer = new Container(); @@ -636,8 +638,10 @@ export class InteractiveMode { console.log(theme.fg("dim", `Model scope: ${modelList}${cycleHint}`)); } - // Add header container as first child. Populate it after detectThemeIfUnset. + // Add header container as first child. Populate it after applying theme settings. + // Keep loaded resources before chat so restored session messages never precede them. this.ui.addChild(this.headerContainer); + this.ui.addChild(this.loadedResourcesContainer); this.ui.addChild(this.chatContainer); this.ui.addChild(this.pendingMessagesContainer); @@ -1330,6 +1334,9 @@ export class InteractiveMode { force?: boolean; showDiagnosticsWhenQuiet?: boolean; }): void { + // Resource rendering is idempotent; chat clears no longer clear this separate container. + this.loadedResourcesContainer.clear(); + const showListing = options?.force || this.options.verbose || !this.settingsManager.getQuietStartup(); const showDiagnostics = showListing || options?.showDiagnosticsWhenQuiet === true; if (!showListing && !showDiagnostics) { @@ -1357,8 +1364,8 @@ export class InteractiveMode { 0, 0, ); - this.chatContainer.addChild(section); - this.chatContainer.addChild(new Spacer(1)); + this.loadedResourcesContainer.addChild(section); + this.loadedResourcesContainer.addChild(new Spacer(1)); }; const skillsResult = this.session.resourceLoader.getSkills(); @@ -1395,7 +1402,7 @@ export class InteractiveMode { if (showListing) { const contextFiles = this.session.resourceLoader.getAgentsFiles().agentsFiles; if (contextFiles.length > 0) { - this.chatContainer.addChild(new Spacer(1)); + this.loadedResourcesContainer.addChild(new Spacer(1)); const contextList = contextFiles .map((f) => theme.fg("dim", ` ${this.formatDisplayPath(f.path)}`)) .join("\n"); @@ -1478,17 +1485,19 @@ export class InteractiveMode { const skillDiagnostics = skillsResult.diagnostics; if (skillDiagnostics.length > 0) { const warningLines = this.formatDiagnostics(skillDiagnostics, sourceInfos); - this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0)); - this.chatContainer.addChild(new Spacer(1)); + this.loadedResourcesContainer.addChild( + new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0), + ); + this.loadedResourcesContainer.addChild(new Spacer(1)); } const promptDiagnostics = promptsResult.diagnostics; if (promptDiagnostics.length > 0) { const warningLines = this.formatDiagnostics(promptDiagnostics, sourceInfos); - this.chatContainer.addChild( + this.loadedResourcesContainer.addChild( new Text(`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, 0, 0), ); - this.chatContainer.addChild(new Spacer(1)); + this.loadedResourcesContainer.addChild(new Spacer(1)); } const extensionDiagnostics: ResourceDiagnostic[] = []; @@ -1508,17 +1517,19 @@ export class InteractiveMode { if (extensionDiagnostics.length > 0) { const warningLines = this.formatDiagnostics(extensionDiagnostics, sourceInfos); - this.chatContainer.addChild( + this.loadedResourcesContainer.addChild( new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0), ); - this.chatContainer.addChild(new Spacer(1)); + this.loadedResourcesContainer.addChild(new Spacer(1)); } const themeDiagnostics = themesResult.diagnostics; if (themeDiagnostics.length > 0) { const warningLines = this.formatDiagnostics(themeDiagnostics, sourceInfos); - this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0)); - this.chatContainer.addChild(new Spacer(1)); + this.loadedResourcesContainer.addChild( + new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0), + ); + this.loadedResourcesContainer.addChild(new Spacer(1)); } } } @@ -1651,6 +1662,7 @@ export class InteractiveMode { } private renderCurrentSessionState(): void { + this.loadedResourcesContainer.clear(); this.chatContainer.clear(); this.pendingMessagesContainer.clear(); this.compactionQueuedMessages = []; @@ -3606,9 +3618,11 @@ export class InteractiveMode { if (isExpandable(activeHeader)) { activeHeader.setExpanded(expanded); } - for (const child of this.chatContainer.children) { - if (isExpandable(child)) { - child.setExpanded(expanded); + for (const container of [this.loadedResourcesContainer, this.chatContainer]) { + for (const child of container.children) { + if (isExpandable(child)) { + child.setExpanded(expanded); + } } } this.ui.requestRender(); diff --git a/packages/coding-agent/test/interactive-mode-status.test.ts b/packages/coding-agent/test/interactive-mode-status.test.ts index 6ba7db91..7d75d383 100644 --- a/packages/coding-agent/test/interactive-mode-status.test.ts +++ b/packages/coding-agent/test/interactive-mode-status.test.ts @@ -119,11 +119,13 @@ describe("InteractiveMode.showStatus", () => { describe("InteractiveMode.setToolsExpanded", () => { test("applies expansion state to the active header and chat entries", () => { const header = { setExpanded: vi.fn() }; + const loadedResourcesChild = { setExpanded: vi.fn() }; const chatChild = { setExpanded: vi.fn() }; const fakeThis: any = { toolOutputExpanded: false, customHeader: undefined, builtInHeader: header, + loadedResourcesContainer: { children: [loadedResourcesChild] }, chatContainer: { children: [chatChild] }, ui: { requestRender: vi.fn() }, }; @@ -132,6 +134,7 @@ describe("InteractiveMode.setToolsExpanded", () => { expect(fakeThis.toolOutputExpanded).toBe(true); expect(header.setExpanded).toHaveBeenCalledWith(true); + expect(loadedResourcesChild.setExpanded).toHaveBeenCalledWith(true); expect(chatChild.setExpanded).toHaveBeenCalledWith(true); expect(fakeThis.ui.requestRender).toHaveBeenCalledTimes(1); }); @@ -441,6 +444,7 @@ describe("InteractiveMode.showLoadedResources", () => { const fakeThis: any = { options: { verbose: options.verbose ?? false }, toolOutputExpanded: options.toolOutputExpanded ?? false, + loadedResourcesContainer: new Container(), chatContainer: new Container(), settingsManager: { getQuietStartup: () => options.quietStartup, @@ -619,7 +623,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - const output = renderAll(fakeThis.chatContainer); + const output = renderAll(fakeThis.loadedResourcesContainer); expect(output).toContain("[Skills]"); expect(output).toContain("commit"); expect(output).not.toContain("resource-list"); @@ -636,7 +640,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - const output = renderAll(fakeThis.chatContainer); + const output = renderAll(fakeThis.loadedResourcesContainer); expect(output).toContain("[Skills]"); expect(output).toContain("resource-list"); expect(output).not.toContain("commit"); @@ -654,7 +658,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - const output = renderAll(fakeThis.chatContainer); + const output = renderAll(fakeThis.loadedResourcesContainer); expect(output).toContain("[Skills]"); expect(output).toContain("resource-list"); expect(output).not.toContain("commit"); @@ -670,7 +674,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - const output = renderAll(fakeThis.chatContainer); + const output = renderAll(fakeThis.loadedResourcesContainer); expect(output).toContain("[Extensions]"); expect(output).toContain("answer.ts, btw.ts"); expect(output).not.toContain("extensions/answer.ts"); @@ -687,7 +691,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` + expect(normalizeRenderedOutput(fakeThis.loadedResourcesContainer)).toMatchInlineSnapshot(` "[Extensions] @scope/pi-scoped, answer.ts, cli-extension.ts, HazAT/pi-interactive-subagents, HazAT/pi-interactive-subagents:subagents, local-index, pi-markdown-preview, user-index"`); }); @@ -733,7 +737,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` + expect(normalizeRenderedOutput(fakeThis.loadedResourcesContainer)).toMatchInlineSnapshot(` "[Extensions] alpha/one, beta/one, gamma/one"`); }); @@ -761,7 +765,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` + expect(normalizeRenderedOutput(fakeThis.loadedResourcesContainer)).toMatchInlineSnapshot(` "[Extensions] plan-mode"`); }); @@ -789,7 +793,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` + expect(normalizeRenderedOutput(fakeThis.loadedResourcesContainer)).toMatchInlineSnapshot(` "[Extensions] plan-mode"`); }); @@ -826,7 +830,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` + expect(normalizeRenderedOutput(fakeThis.loadedResourcesContainer)).toMatchInlineSnapshot(` "[Extensions] plan-mode, webfetch.ts"`); }); @@ -863,7 +867,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` + expect(normalizeRenderedOutput(fakeThis.loadedResourcesContainer)).toMatchInlineSnapshot(` "[Extensions] bar, foo"`); }); @@ -900,7 +904,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` + expect(normalizeRenderedOutput(fakeThis.loadedResourcesContainer)).toMatchInlineSnapshot(` "[Extensions] alpha/tools, beta/tools"`); }); @@ -928,7 +932,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` + expect(normalizeRenderedOutput(fakeThis.loadedResourcesContainer)).toMatchInlineSnapshot(` "[Extensions] main.ts"`); }); @@ -956,7 +960,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` + expect(normalizeRenderedOutput(fakeThis.loadedResourcesContainer)).toMatchInlineSnapshot(` "[Extensions] pi-markdown-preview"`); }); @@ -972,7 +976,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` + expect(normalizeRenderedOutput(fakeThis.loadedResourcesContainer)).toMatchInlineSnapshot(` "[Extensions] project /tmp/project/.pi/extensions/answer.ts @@ -1003,7 +1007,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - const output = renderAll(fakeThis.chatContainer).replace(/\\/g, "/"); + const output = renderAll(fakeThis.loadedResourcesContainer).replace(/\\/g, "/"); expect(output).toContain("[Context]"); expect(output).toContain("~/.pi/agent/AGENTS.md, AGENTS.md"); expect(output).not.toContain(`${cwd.replace(/\\/g, "/")}/AGENTS.md`); @@ -1023,7 +1027,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - const output = renderAll(fakeThis.chatContainer).replace(/\\/g, "/"); + const output = renderAll(fakeThis.loadedResourcesContainer).replace(/\\/g, "/"); expect(output).toContain("[Context]"); expect(output).toContain("~/.pi/agent/AGENTS.md"); expect(output).toContain("~/Development/pi-mono/AGENTS.md"); @@ -1042,7 +1046,7 @@ describe("InteractiveMode.showLoadedResources", () => { showDiagnosticsWhenQuiet: true, }); - expect(fakeThis.chatContainer.children).toHaveLength(0); + expect(fakeThis.loadedResourcesContainer.children).toHaveLength(0); }); test("still shows diagnostics on quiet startup when requested", () => { @@ -1057,7 +1061,7 @@ describe("InteractiveMode.showLoadedResources", () => { showDiagnosticsWhenQuiet: true, }); - const output = renderAll(fakeThis.chatContainer); + const output = renderAll(fakeThis.loadedResourcesContainer); expect(output).toContain("[Skill conflicts]"); expect(output).not.toContain("[Skills]"); }); diff --git a/packages/coding-agent/test/suite/regressions/5943-session-start-notify.test.ts b/packages/coding-agent/test/suite/regressions/5943-session-start-notify.test.ts index db7ce26d..2c8d88e0 100644 --- a/packages/coding-agent/test/suite/regressions/5943-session-start-notify.test.ts +++ b/packages/coding-agent/test/suite/regressions/5943-session-start-notify.test.ts @@ -1,4 +1,5 @@ import { fauxAssistantMessage } from "@earendil-works/pi-ai"; +import { Container, Text } from "@earendil-works/pi-tui"; import { describe, expect, it, vi } from "vitest"; import type { AgentSessionEvent } from "../../../src/core/agent-session.ts"; import type { ExtensionUIContext } from "../../../src/core/extensions/index.ts"; @@ -43,6 +44,35 @@ function createUiContext( }; } +type LoadedResourcesResult = { [K in keyof T]: T[K] } & { diagnostics: [] }; + +type LoadedResourcesContext = { + loadedResourcesContainer: Container; + chatContainer: Container; + options: { verbose?: boolean }; + settingsManager: { getQuietStartup: () => boolean }; + sessionManager: { getCwd: () => string }; + session: { + promptTemplates: []; + resourceLoader: { + getAgentsFiles: () => LoadedResourcesResult<{ agentsFiles: Array<{ path: string }> }>; + getSkills: () => LoadedResourcesResult<{ skills: [] }>; + getPrompts: () => LoadedResourcesResult<{ prompts: [] }>; + getThemes: () => LoadedResourcesResult<{ themes: [] }>; + getExtensions: () => { extensions: []; errors: [] }; + }; + extensionRunner: { + getCommandDiagnostics: () => []; + getShortcutDiagnostics: () => []; + getRegisteredCommands: () => []; + }; + }; + getStartupExpansionState: () => boolean; + formatDisplayPath: (resourcePath: string) => string; + formatContextPath: (resourcePath: string) => string; + getBuiltInCommandConflictDiagnostics: (extensionRunner: LoadedResourcesContext["session"]["extensionRunner"]) => []; +}; + type RebindContext = { unsubscribe?: () => void; applyRuntimeSettings: () => void; @@ -97,6 +127,10 @@ type ReloadCommandContext = { }; type InteractiveModePrototype = { + showLoadedResources( + this: LoadedResourcesContext, + options?: { extensions?: Array<{ path: string }>; force?: boolean; showDiagnosticsWhenQuiet?: boolean }, + ): void; rebindCurrentSession(this: RebindContext, options?: { renderBeforeBind?: boolean }): Promise; handleReloadCommand(this: ReloadCommandContext): Promise; }; @@ -183,7 +217,56 @@ function getMessageText(event: MessageEvent): string { .join(""); } +function createLoadedResourcesContext(): LoadedResourcesContext { + return { + loadedResourcesContainer: new Container(), + chatContainer: new Container(), + options: { verbose: true }, + settingsManager: { getQuietStartup: () => false }, + sessionManager: { getCwd: () => "/repo" }, + session: { + promptTemplates: [], + resourceLoader: { + getAgentsFiles: () => ({ agentsFiles: [{ path: "/repo/AGENTS.md" }], diagnostics: [] }), + getSkills: () => ({ skills: [], diagnostics: [] }), + getPrompts: () => ({ prompts: [], diagnostics: [] }), + getThemes: () => ({ themes: [], diagnostics: [] }), + getExtensions: () => ({ extensions: [], errors: [] }), + }, + extensionRunner: { + getCommandDiagnostics: () => [], + getShortcutDiagnostics: () => [], + getRegisteredCommands: () => [], + }, + }, + getStartupExpansionState: () => false, + formatDisplayPath: (resourcePath) => resourcePath, + formatContextPath: (resourcePath) => resourcePath.replace("/repo/", ""), + getBuiltInCommandConflictDiagnostics: () => [], + }; +} + describe("regression #5943: session_start transient UI", () => { + it("renders loaded resources before restored messages without stale entries", () => { + initTheme("dark", false); + const context = createLoadedResourcesContext(); + const root = new Container(); + root.addChild(context.loadedResourcesContainer); + root.addChild(context.chatContainer); + context.loadedResourcesContainer.addChild(new Text("stale resources", 0, 0)); + context.chatContainer.addChild(new Text("restored message", 0, 0)); + + interactiveModePrototype.showLoadedResources.call(context); + + const chatRendered = context.chatContainer.render(80).join("\n"); + expect(chatRendered).toContain("restored message"); + expect(chatRendered).not.toContain("[Context]"); + + const rendered = root.render(80).join("\n"); + expect(rendered).not.toContain("stale resources"); + expect(rendered.indexOf("[Context]")).toBeLessThan(rendered.indexOf("restored message")); + }); + it("renders replacement session state before session_start handlers can notify", async () => { const events: string[] = []; const harness = await createHarness({ From c29bbc095829e352d8555527c553fb04928a571c Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 24 Jun 2026 14:45:50 +0200 Subject: [PATCH 121/122] docs(agent): update models phase 9 plan --- packages/agent/docs/models.md | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/agent/docs/models.md b/packages/agent/docs/models.md index c634a267..15914371 100644 --- a/packages/agent/docs/models.md +++ b/packages/agent/docs/models.md @@ -860,20 +860,21 @@ Check items off as they land. Keep this list current; it is the working state fo coding-agent replaces AuthStorage and ModelRegistry's internals with `FileCredentialStore` + a `MutableModels` collection. AgentSession itself stays (AgentHarness adoption is pi 2.0); only its model/auth substrate swaps. Layering is strictly one-directional: ```txt -FileCredentialStore (auth.json, locked) + --api-key overlay + $ENV/!command resolution +FileCredentialStore (auth.json, locked, $ENV/!command resolution) + explicit --api-key overlay ↑ MutableModels: builtin factories (wrapped per models.json config) + custom providers (models.json ∪ extensions) ↑ -ModelRegistry: async facade — reads delegate to the collection; registerProvider/login/logout/status for extensions + UI +ModelRegistry: compatibility facade — sync last-known reads delegate to the collection; registerProvider/login/logout/status for extensions + UI ↑ -AgentSession / sdk / interactive-mode (await added; stream via models) +AgentSession / sdk / interactive-mode (stream via models; await only auth/refresh paths) ``` Decisions: - `AuthStorage` is deleted as a type — it would otherwise depend on provider auth while provider auth depends on its store (circular). Its surface splits: `get`/`set`/`remove` -> `CredentialStore`; `getApiKey` -> `Models.getAuth`; `login`/`logout`/`getAuthStatus` -> ModelRegistry facade methods over `provider.auth.oauth` + the store. -- Runtime `--api-key` overrides are a 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` become async, delegating to the collection (no materialized snapshot, no sync lies; dynamic providers like llama.cpp work). The extension-facing `modelRegistry` surface changes accordingly (breaking, changelogged); extensions also get the collection itself as the forward API. +- `FileCredentialStore` is self-contained (path, locking, parse/write, chmod, error buffering) and owns `auth.json` semantics, including `$ENV`/`!command` resolution for stored API-key credentials. Persisted values stay raw; resolution returns copies for auth use. +- 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. - Copilot: stored-credential baseUrl applied in the wrapped `getModels()` (extension-visible models stay correct) plus per-request `toAuth().baseUrl`. @@ -884,14 +885,15 @@ Ordering for new sessions: 1. [x] pi-ai rework first: `Provider.getModels()` sync + optional `refreshModels()`; `Models.getModels`/`getModel` sync, `Models.refresh(provider?)` async; `createProvider` takes `models` array + optional `refreshModels` fetcher (in-flight dedupe). Reverses Phase 1's async-listing decision — see "Provider model listing" for rationale (sync-or-async unions breed latent sync assumptions; async-only breaks sync consumer surfaces like extension `find`/`getAll`). 2. [x] Cloudflare provider auth in pi-ai factories: Workers AI and AI Gateway validate their required account/gateway env/config and return resolved `baseUrl`, provider-scoped env, and header suppression/override metadata from provider auth. 3. [ ] Add `FileCredentialStore` in coding-agent. - - Implement the pi-ai `CredentialStore` interface over the existing `auth.json` lock backend (`FileAuthStorageBackend` / `InMemoryAuthStorageBackend` can be reused or renamed). + - Implement the pi-ai `CredentialStore` interface as a self-contained `auth.json` store; do not depend on the old `AuthStorageBackend` abstraction, though its lock/retry semantics may be ported. - Preserve the existing file format. `ApiKeyCredential` uses `{ type: "api_key", key?, env? }`, matching today's `auth.json`; do not translate `env` into metadata or rewrite discriminators. + - Resolve `$ENV`/`!command` in stored API-key `key` and `env` values out of the box using an injected execution/config environment. `$ENV` lookup should come from that environment, and `!command` should run through the shared shell execution path rather than direct `execSync`. + - Persist raw config values; resolved credentials returned for auth use must be copies and must not rewrite `$ENV`/`!command` strings unless a caller explicitly stores new values. - `read(provider)` returns the current credential snapshot and records parse/storage errors for status UI parity. - `modify(provider, fn)` must lock, re-read, run `fn`, merge-write the provider entry, chmod `0600`, and return the post-write credential. - `delete(provider)` must lock and remove only that provider's entry. - - Add file-backed and in-memory tests covering lock/RMW behavior, `api_key` reads, OAuth reads, provider `env` preservation, delete, parse errors, and concurrent refresh-style modifications. -4. [ ] Add store decorators for coding-agent policy. - - `withConfigValues(store, policy)` resolves stored API-key credentials whose `key` or `env` values use `$ENV` or `!command`, using existing `resolve-config-value.ts` semantics. Command execution stays in coding-agent, not pi-ai. + - Add file-backed and in-memory tests covering lock/RMW behavior, `api_key` reads with config-value resolution, OAuth reads, provider `env` preservation, delete, parse errors, and concurrent refresh-style modifications. +4. [ ] Add runtime override overlay for coding-agent policy. - `withRuntimeOverrides(store, overrides)` implements CLI `--api-key`: read returns an ephemeral `{ type: "api_key", key }` for each overridden provider, masking stored OAuth/API credentials without persisting. - Runtime overrides must apply even to OAuth-capable providers; every provider registered in coding-agent must retain or gain an `apiKey` auth slot so the overlay is meaningful. - Tests cover precedence: runtime override > stored credential > models.json config auth > ambient provider env, with stored credential blocking ambient fallback. @@ -916,21 +918,21 @@ Ordering for new sessions: - Preserve existing extension runtime compatibility through the `/compat` alias until Phase 10. 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()` become async and delegate to the collection/status checks. Update extension-facing types and changelog this breaking change. - - `refresh()` rebuilds provider layers and calls `models.refresh()` where needed; no global api-registry reset should be part of the new path except compat-only grace behavior. + - `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. + - `refresh()` is the explicit async freshness boundary: rebuild provider layers and call `models.refresh()` where needed; no global api-registry reset should be part of the new path except compat-only grace behavior. - `registerProvider()`/`unregisterProvider()` mutate provider layers and rebuild the collection. - Facade auth ops (`login`, `logout`, provider status, available OAuth providers) drive `provider.auth.{apiKey,oauth}` and the `CredentialStore`; no `AuthStorage` type remains. - Legacy `registerApiProvider` writes stay only for `/compat` callers and are removed in Phase 10. 9. [ ] Rewire consumers. - `AgentSession` stream function resolves through `ModelRegistry`/`Models`, not `getApiKeyAndHeaders()` + compat globals. - SDK options replace `authStorage` with `credentials?: CredentialStore` or an agent-dir-backed default; update `sdk.md` and examples. - - `model-resolver`, `--list-models`, model selector, login/logout/status UI, and provider attribution await async model/auth APIs. + - `model-resolver`, `--list-models`, model selector, login/logout/status UI, and provider attribution use sync last-known model reads and await only explicit refresh/auth operations. - CLI `--api-key` populates the runtime override decorator instead of mutating `AuthStorage`. - Keep extension loader root-to-compat alias until Phase 10, but expose the new collection/facade as the forward API. 10. [ ] Test migration and real-provider validation. - - Unit tests for `FileCredentialStore`, config-value decorators, provider decoration, extension OAuth adapter, ModelRegistry async facade, and consumer rewiring. + - Unit tests for `FileCredentialStore`, runtime override overlay, provider decoration, extension OAuth adapter, Models-backed ModelRegistry facade, and consumer rewiring. - Regression tests for Cloudflare account/gateway env, Copilot OAuth baseUrl wrapping, runtime `--api-key` precedence, `$ENV`/`!command` resolution, and stored credential blocking ambient fallback. - - Update existing tests that assume sync `ModelRegistry.getAll/find/getAvailable`. + - Update existing tests for sync last-known `ModelRegistry.getAll/find/getAvailable` plus explicit async refresh behavior. - Run targeted non-e2e suites plus tmux validation of login flows against real providers (Anthropic OAuth/API key, OpenAI Codex OAuth, GitHub Copilot OAuth, Cloudflare AI Gateway, Bedrock if credentials are available). ### Phase 10 — compat deletion (pi 2.0 era, separate) From 371adcf37130629ffb9bbeed9f5548ce08ffa93b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 24 Jun 2026 18:12:25 +0200 Subject: [PATCH 122/122] fix(coding-agent): retry explicit provider retry errors closes #6019 --- packages/ai/CHANGELOG.md | 4 + packages/ai/src/index.ts | 1 + packages/ai/src/utils/retry.ts | 96 +++++++++++++++++++ packages/ai/test/retry.test.ts | 38 ++++++++ packages/coding-agent/CHANGELOG.md | 4 + .../coding-agent/src/core/agent-session.ts | 22 +---- ...19-explicit-provider-retry-message.test.ts | 31 ++++++ 7 files changed, 178 insertions(+), 18 deletions(-) create mode 100644 packages/ai/src/utils/retry.ts create mode 100644 packages/ai/test/retry.test.ts create mode 100644 packages/coding-agent/test/suite/regressions/6019-explicit-provider-retry-message.test.ts diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index b03b0353..03818dc3 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed retry classification for provider errors that explicitly tell callers to retry the request ([#6019](https://github.com/earendil-works/pi/issues/6019)). + ## [0.80.2] - 2026-06-23 ### Changed diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 6c3f9675..57c448cc 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -43,5 +43,6 @@ export type { OAuthSelectPrompt, } from "./utils/oauth/types.ts"; export * from "./utils/overflow.ts"; +export * from "./utils/retry.ts"; export * from "./utils/typebox-helpers.ts"; export * from "./utils/validation.ts"; diff --git a/packages/ai/src/utils/retry.ts b/packages/ai/src/utils/retry.ts new file mode 100644 index 00000000..d3a95a0a --- /dev/null +++ b/packages/ai/src/utils/retry.ts @@ -0,0 +1,96 @@ +import type { AssistantMessage } from "../types.ts"; + +function buildProviderErrorPattern(patterns: readonly string[]): RegExp { + return new RegExp(patterns.join("|"), "i"); +} + +const NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN = buildProviderErrorPattern([ + // OpenCode Go/free-tier limits returned as 429 JSON error types by OpenCode's + // Zen API. These are subscription/account limits, not transient throttles. + "GoUsageLimitError", + "FreeUsageLimitError", + + // OpenCode Go subscription-limit text asks users to enable available-balance + // usage after rolling/weekly/monthly limits are reached. + "Monthly usage limit reached", + "available balance", + + // Generic quota/budget/billing exhaustion. `insufficient_quota` is OpenAI's + // quota/billing error code; the other strings cover common gateway wording. + "insufficient_quota", + "out of budget", + "quota exceeded", + "billing", +]); + +const RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([ + // Generic provider load, HTTP status, and server-side transient failures. + "overloaded", + "rate.?limit", + "too many requests", + "429", + "500", + "502", + "503", + "504", + "service.?unavailable", + "server.?error", + "internal.?error", + + // Wrapper/provider text for transient upstream failures, including OpenRouter + // "Provider returned error" responses (#2264). + "provider.?returned.?error", + + // Network, proxy, and fetch transport failures. This includes OpenAI Codex + // raw-fetch failures such as "upstream connect", "connection refused", and + // "reset before headers" (#733), plus OpenRouter connection drops (#3317). + "network.?error", + "connection.?error", + "connection.?refused", + "connection.?lost", + "other side closed", + "fetch failed", + "upstream.?connect", + "reset before headers", + "socket hang up", + "timed? out", + "timeout", + "terminated", + + // WebSocket transports can report close/error text instead of HTTP/fetch text. + "websocket.?closed", + "websocket.?error", + + // Premature stream endings from SDKs and transports. Anthropic can throw + // "stream ended without ..." and "Anthropic stream ended before message_stop" + // (#4433); Bedrock/Smithy can throw an HTTP/2 no-response error (#3594). + "ended without", + "stream ended before message_stop", + "http2 request did not get a response", + + // Provider-requested retry delay cap failures should flow through the outer + // retry policy so callers can surface/abort the backoff (#1123). + "retry delay", + + // Explicit retry guidance emitted mid-stream by OpenAI Responses and Bedrock + // stream exceptions (#6019). + "you can retry your request", + "try your request again", + "please retry your request", +]); + +/** + * Classifies whether a failed assistant message looks like a transient provider + * or transport error, so callers can decide if the last assistant turn should be + * restarted. + * + * This does not implement retry policy. Callers should first handle context + * overflow separately, then apply their own retry budget, backoff, and reporting + * before restarting the assistant turn. + */ +export function isRetryableAssistantError(message: AssistantMessage): boolean { + if (message.stopReason !== "error" || !message.errorMessage) return false; + const errorMessage = message.errorMessage; + if (NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN.test(errorMessage)) return false; + return RETRYABLE_PROVIDER_ERROR_PATTERN.test(errorMessage); +} diff --git a/packages/ai/test/retry.test.ts b/packages/ai/test/retry.test.ts new file mode 100644 index 00000000..17273c70 --- /dev/null +++ b/packages/ai/test/retry.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; +import { fauxAssistantMessage } from "../src/providers/faux.ts"; +import { isRetryableAssistantError } from "../src/utils/retry.ts"; + +const openAIExplicitRetryMessage = + "An error occurred while processing your request. You can retry your request, or contact us through our help center at help.openai.com if the error persists. Please include the request ID req_******** in your message."; +const bedrockExplicitRetryMessage = + '{"message":"The system encountered an unexpected error during processing. Try your request again."}'; + +describe("provider retry classification", () => { + it("matches explicit provider retry guidance", () => { + expect( + isRetryableAssistantError( + fauxAssistantMessage("", { stopReason: "error", errorMessage: openAIExplicitRetryMessage }), + ), + ).toBe(true); + expect( + isRetryableAssistantError( + fauxAssistantMessage("", { stopReason: "error", errorMessage: bedrockExplicitRetryMessage }), + ), + ).toBe(true); + }); + + it("keeps provider limit errors non-retryable", () => { + expect( + isRetryableAssistantError( + fauxAssistantMessage("", { stopReason: "error", errorMessage: "429 quota exceeded" }), + ), + ).toBe(false); + }); + + it("classifies assistant error messages", () => { + expect( + isRetryableAssistantError(fauxAssistantMessage("", { stopReason: "error", errorMessage: "overloaded_error" })), + ).toBe(true); + expect(isRetryableAssistantError(fauxAssistantMessage("not an error"))).toBe(false); + }); +}); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a432a068..3dd5d2e1 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed auto-retry for provider stream errors that explicitly tell callers to retry the request ([#6019](https://github.com/earendil-works/pi/issues/6019)). + ## [0.80.2] - 2026-06-23 ### Changed diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 65a6ebd8..fafa1031 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -29,6 +29,7 @@ import { cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, + isRetryableAssistantError, modelsAreEqual, resetApiProviders, streamSimple, @@ -2482,29 +2483,14 @@ export class AgentSession { // Auto-Retry // ========================================================================= - private _isNonRetryableProviderLimitError(errorMessage: string): boolean { - return /GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test( - errorMessage, - ); - } - /** * Check if an error is retryable (overloaded, rate limit, server errors). * Context overflow errors are NOT retryable (handled by compaction instead). */ private _isRetryableError(message: AssistantMessage): boolean { - if (message.stopReason !== "error" || !message.errorMessage) return false; - - // Context overflow is handled by compaction, not retry - const contextWindow = this.model?.contextWindow ?? 0; - if (isContextOverflow(message, contextWindow)) return false; - - const err = message.errorMessage; - if (this._isNonRetryableProviderLimitError(err)) return false; - // Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504, service unavailable, network/connection errors (including connection lost), WebSocket transport closes/errors, fetch failed, premature stream endings, HTTP/2 closed before response, terminated, retry delay exceeded - return /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i.test( - err, - ); + // Context overflow is handled by compaction, not retry. + if (isContextOverflow(message, this.model?.contextWindow ?? 0)) return false; + return isRetryableAssistantError(message); } /** diff --git a/packages/coding-agent/test/suite/regressions/6019-explicit-provider-retry-message.test.ts b/packages/coding-agent/test/suite/regressions/6019-explicit-provider-retry-message.test.ts new file mode 100644 index 00000000..aa4889c3 --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/6019-explicit-provider-retry-message.test.ts @@ -0,0 +1,31 @@ +import { fauxAssistantMessage } from "@earendil-works/pi-ai"; +import { describe, expect, it } from "vitest"; +import { createHarness } from "../harness.ts"; + +const openAIExplicitRetryMessage = + "An error occurred while processing your request. You can retry your request, or contact us through our help center at help.openai.com if the error persists. Please include the request ID req_******** in your message."; +const bedrockExplicitRetryMessage = + '{"message":"The system encountered an unexpected error during processing. Try your request again."}'; + +describe("regression: issue 6019 explicit provider retry messages", () => { + it.each([ + ["openai", openAIExplicitRetryMessage], + ["bedrock", bedrockExplicitRetryMessage], + ])("retries %s explicit retry guidance", async (_provider, errorMessage) => { + const harness = await createHarness({ settings: { retry: { enabled: true, maxRetries: 3, baseDelayMs: 1 } } }); + try { + harness.setResponses([ + fauxAssistantMessage("", { stopReason: "error", errorMessage }), + fauxAssistantMessage("recovered"), + ]); + + await harness.session.prompt("test"); + + expect(harness.faux.state.callCount).toBe(2); + expect(harness.eventsOfType("auto_retry_start").map((event) => event.errorMessage)).toEqual([errorMessage]); + expect(harness.eventsOfType("auto_retry_end").map((event) => event.success)).toEqual([true]); + } finally { + harness.cleanup(); + } + }); +});