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/CHANGELOG.md b/packages/agent/CHANGELOG.md index 0d6e2f67..a48ede06 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -2,6 +2,12 @@ ## [Unreleased] +### Breaking Changes + +- `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.10] - 2026-06-22 ## [0.79.9] - 2026-06-20 diff --git a/packages/agent/docs/models.md b/packages/agent/docs/models.md new file mode 100644 index 00000000..05307be5 --- /dev/null +++ b/packages/agent/docs/models.md @@ -0,0 +1,918 @@ +# Models architecture + +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/api/` and are reusable/lazy. +- 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: 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. + +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. +- 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 + +Target source layout: + +```txt +packages/ai/src/ + index.ts # core exports only; no built-in provider imports + 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, shared resolveProviderAuth(), 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 + 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 + openai.ts + openai.models.ts # generated OpenAI catalog + openai-codex.ts + openai-codex.models.ts + anthropic.ts + anthropic.models.ts + 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(), builtinImagesModels(), getBuiltin*() + utils/oauth/ # OAuth flow implementations (node), lazy-loaded +``` + +`src/index.ts` must stay core-only. It must not import: + +- generated model catalogs +- built-in provider factories +- provider SDK implementations +- Node-only OAuth modules +- `providers/all` +- `compat` + +Provider, API, and compat 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 +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 SDK implementations; provider streams use lazy wrappers. + +## Core runtime: Models + +`Models` is a provider collection plus auth application and stream convenience. No stream registry, no auth resolver strategy object. + +```ts +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; + + /** 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): 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. + * 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, + 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; +} +``` + +Removed concepts: + +```txt +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 +``` + +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 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 { + readonly id: string; + readonly name: string; + + readonly baseUrl?: string; + readonly headers?: Record; + + /** + * 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; + + /** 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; + + streamSimple(model: Model, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream; +} +``` + +There is no `Provider.api` field. `model.api` carries API identity; the provider dispatches internally (see `createProvider()`). + +`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. + +### Typed stream options + +Full stream options are API-specific. `Model` pays off by deriving the option type from the API: + +```ts +// 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; +``` + +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. + +## Provider model listing + +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. + +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 `refreshModels()`. + +## Streaming path + +`Models.stream()` finds the provider by `model.provider`, resolves auth, merges it into request options, and delegates: + +```ts +function stream(model, context, options) { + const provider = this.getProvider(model.provider); + if (!provider) { + // produce an error stream, not a throw — see Error behavior + } + + // 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()` 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 refreshes the provider and re-reads (`await models.refresh(p); models.getModel(p, id)`) before starting the turn. + +## API implementations under `src/api` + +An API implementation is reusable stream behavior. It is not a provider. + +Uniform export contract — every real implementation module exports exactly: + +```ts +// src/api/anthropic-messages.ts — imports SDKs +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. `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?: 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 +export const anthropicMessagesApi = (): ProviderStreams => lazyApi(() => import("./anthropic-messages.ts")); +``` + +Import chain: + +```txt +provider module -> lazy API wrapper -> dynamic import(real API impl) -> SDK deps +``` + +Notes: + +- 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 (OpenAI-completions: OpenRouter, Groq, Cerebras, xAI, ZAI, ...). They share lazy API objects by reference: + +```ts +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; + +export function openrouterProvider(): Provider { + return createProvider({ + id: "openrouter", + name: "OpenRouter", + baseUrl: "https://openrouter.ai/api/v1", + auth: { apiKey: envApiKeyAuth("OpenRouter API key", ["OPENROUTER_API_KEY"]) }, + models: OPENROUTER_MODELS, + api: openAICompletionsApi(), + }); +} +``` + +This copies Vercel AI SDK's useful property: users import concrete providers; shared protocol implementation is internal. + +## Auth + +Request auth output stays small: + +```ts +export interface ModelAuth { + apiKey?: string; + headers?: Record; + baseUrl?: string; +} +``` + +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 + +`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 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; + + /** + * 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; +} + +export interface OAuthAuth { + name: string; // "Anthropic (Claude Pro/Max)" + + login(callbacks: AuthLoginCallbacks): 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 interface AuthResult { + auth: ModelAuth; + /** Human-readable label for status UI: "ANTHROPIC_API_KEY", "OAuth", "~/.aws/credentials". */ + source?: string; +} + +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 ApiKeyCredential { + type: "api-key"; + key?: string; + metadata?: Record; // e.g. Cloudflare accountId/gatewayId +} + +export interface OAuthCredential extends OAuthCredentials { + type: "oauth"; // access, refresh, expires from 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. + +### Credential store + +The app injects storage; `pi-ai` ships an in-memory default. Keyed by provider id, one credential per provider: + +```ts +export interface CredentialStore { + /** 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; +} +``` + +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)` 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): + +```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 +``` + +Properties: + +- 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`. + +### Replacing AuthStorage + +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 + +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): Promise; + notify(event: AuthEvent): void; +} + +/** `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 } +); + +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 }; +``` + +`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 attachment + +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 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: lazyOAuth({ + name: "Anthropic (Claude Pro/Max)", + load: () => import("../utils/oauth/anthropic.ts").then((m) => m.anthropicOAuth), + }), + }, + models: ANTHROPIC_MODELS, + api: anthropicMessagesApi(), + }); +} +``` + +`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 lazyOAuth(input: { + name: string; + load: () => Promise; +}): 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 + +`models.json` is a provider wrapper layer. It does not mutate providers in place: + +```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), + + getModels: () => applyModelOverrides(base.getModels(), overrides.models), + refreshModels: base.refreshModels?.bind(base), + + stream: base.stream, + streamSimple: base.streamSimple, + }; +} +``` + +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`. + +## Custom providers: createProvider() + +One helper builds providers from parts; it handles both single-API and mixed-API providers: + +```ts +export function createProvider(input: { + id: string; + name?: string; // default: id + baseUrl?: string; + headers?: Record; + auth: ProviderAuth; // required, at least one of apiKey/oauth (no "no-auth" providers) + /** 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; +``` + +- 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 +{ + "providers": { + "my-openai-proxy": { + "api": "openai-completions", + "baseUrl": "https://proxy.example/v1", + "models": [ ... ] + } + } +} +``` + +## 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 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. + +- `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 (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 + +Typed, sync, generated-catalog-only helpers live with the catalogs (exported from `providers/all`): + +```ts +getBuiltinModel(provider, id) // sync, typed overloads from generated catalog +getBuiltinModels(provider) // sync +getBuiltinProviders() // sync +``` + +Runtime lookup is always the async instance API: `await models.getModel(...)`. + +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 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 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`. + +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/*": "./dist/providers/*.js", + "./api/*": "./dist/api/*.js" + } +} +``` + +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. + +- `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`). + +## coding-agent next phase (not this pass) + +coding-agent builds providers in layers and binds them per session: + +```txt +built-in providers (builtinModels) +-> models.json provider wrappers / custom providers (createProvider) +-> extension provider wrappers/additions +``` + +```ts +sessionModels.clearProviders(); +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: + +- 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) + +## 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 + +- [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/` + +- [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 + +- [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 + +- [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 + +- [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 + +- [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. `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) + +- [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 + +- [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 — coding-agent on Models + CredentialStore (in scope) + +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 + ↑ +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`, 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 + +- [ ] Web OAuth implementations (sitegeist-style) as an alternative `OAuthAuth`. +- [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 + +`undefined` means not found or not configured. Real failures reject or become stream errors. + +```ts +export type ModelsErrorCode = + | "model_source" // provider model refresh 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()` produces stream errors (error event + error result) for async setup failures; it does not throw after returning the stream. +- `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/agent/src/agent-loop.ts b/packages/agent/src/agent-loop.ts index 77bd9568..d93458d0 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..1d09b054 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"; +import type { AssistantMessage, ImageContent, Model, Models, UserMessage } from "@earendil-works/pi-ai"; import { runAgentLoop } from "../agent-loop.ts"; import type { AgentContext, @@ -75,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(); @@ -178,6 +161,7 @@ export class AgentHarness< > { readonly env: ExecutionEnv; private session: Session; + readonly models: Models; private phase: AgentHarnessPhase = "idle"; private runAbortController?: AbortController; private runPromise?: Promise; @@ -186,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[]; @@ -200,10 +183,10 @@ 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; - this.getApiKeyAndHeaders = options.getApiKeyAndHeaders; this.validateUniqueNames( (options.tools ?? []).map((tool) => tool.name), "Duplicate tool name(s)", @@ -376,13 +359,9 @@ 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 streamSimple(model, context, { + return this.models.streamSimple(model, context, { cacheRetention: requestOptions.cacheRetention, headers: requestOptions.headers, maxRetries: requestOptions.maxRetries, @@ -401,7 +380,6 @@ export class AgentHarness< sessionId: turnState.sessionId, timeoutMs: requestOptions.timeoutMs, transport: requestOptions.transport, - apiKey: auth?.apiKey, }); }; } @@ -713,8 +691,6 @@ export class AgentHarness< try { const model = this.model; if (!model) throw new AgentHarnessError("invalid_state", "No model set for compaction"); - 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; @@ -731,15 +707,7 @@ export class AgentHarness< const provided = hookResult?.compaction; const compactResult = provided ? { ok: true as const, value: provided } - : await compact( - preparation, - 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( @@ -792,12 +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"); - 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, 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 c1824ebf..fdf1df49 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, Models } from "@earendil-works/pi-ai"; + import type { AgentMessage } from "../../types.ts"; import { convertToLlm, @@ -49,12 +49,10 @@ export interface CollectEntriesResult { /** Options for generating a branch summary. */ export interface GenerateBranchSummaryOptions { + /** Provider collection the summarization request goes through; owns auth resolution. */ + models: Models; /** Model used for summarization. */ model: Model; - /** API key forwarded to the provider. */ - 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. */ @@ -202,7 +200,7 @@ export async function generateBranchSummary( entries: SessionTreeEntry[], options: GenerateBranchSummaryOptions, ): Promise> { - const { 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; @@ -230,10 +228,10 @@ 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 }, + { 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 dba753d7..d6874c33 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"; -import { completeSimple } from "@earendil-works/pi-ai"; +import type { AssistantMessage, ImageContent, Model, Models, TextContent, Usage } from "@earendil-works/pi-ai"; import type { AgentMessage, ThinkingLevel } from "../../types.ts"; import { convertToLlm, @@ -455,10 +454,9 @@ 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, - headers?: Record, signal?: AbortSignal, customInstructions?: string, previousSummary?: string, @@ -490,10 +488,10 @@ 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 completeSimple( + const response = await models.completeSimple( model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, completionOptions, @@ -626,9 +624,8 @@ 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, - headers?: Record, customInstructions?: string, signal?: AbortSignal, thinkingLevel?: ThinkingLevel, @@ -655,25 +652,16 @@ export async function compact( messagesToSummarize.length > 0 ? generateSummary( messagesToSummarize, + models, model, settings.reserveTokens, - apiKey, - headers, signal, customInstructions, previousSummary, thinkingLevel, ) : Promise.resolve(ok("No prior history.")), - generateTurnPrefixSummary( - turnPrefixMessages, - 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); @@ -681,10 +669,9 @@ export async function compact( } else { const summaryResult = await generateSummary( messagesToSummarize, + models, model, settings.reserveTokens, - apiKey, - headers, signal, customInstructions, previousSummary, @@ -706,10 +693,9 @@ export async function compact( } async function generateTurnPrefixSummary( messages: AgentMessage[], + models: Models, model: Model, reserveTokens: number, - apiKey: string, - headers?: Record, signal?: AbortSignal, thinkingLevel?: ThinkingLevel, ): Promise> { @@ -728,12 +714,12 @@ async function generateTurnPrefixSummary( }, ]; - const response = await completeSimple( + const response = await models.completeSimple( 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 4756ca84..f7bdf6dd 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. + */ + models: Models; tools?: TTool[]; /** * Concrete resources available to explicit invocation methods and system-prompt callbacks. @@ -818,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/src/types.ts b/packages/agent/src/types.ts index cb99a79a..abfa3de6 100644 --- a/packages/agent/src/types.ts +++ b/packages/agent/src/types.ts @@ -1,11 +1,13 @@ import type { + Api, AssistantMessage, AssistantMessageEvent, + AssistantMessageEventStream, + Context, ImageContent, Message, Model, SimpleStreamOptions, - streamSimple, TextContent, Tool, ToolResultMessage, @@ -13,7 +15,8 @@ import type { 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/agent.test.ts b/packages/agent/test/agent.test.ts index 4cc51f74..5fa27c5a 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 { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { Agent, type AgentEvent, type AgentTool, type AgentToolUpdateCallback } from "../src/index.ts"; diff --git a/packages/agent/test/harness/agent-harness-stream.test.ts b/packages/agent/test/harness/agent-harness-stream.test.ts index ee79564b..f5a4021d 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); @@ -27,10 +36,9 @@ 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 = 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(), @@ -51,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" }, @@ -68,21 +76,19 @@ 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 }); }); 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 +97,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 +140,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 +155,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 +181,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 +190,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 1d24eb4c..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"; -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..95148c3b 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; @@ -445,19 +440,9 @@ describe("harness compaction", () => { }, ]); getOrThrow( - await generateSummary( - messages, - 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([ @@ -466,9 +451,7 @@ describe("harness compaction", () => { return fauxAssistantMessage("## Goal\nTest summary"); }, ]); - getOrThrow( - await generateSummary(messages, 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); @@ -479,17 +462,7 @@ describe("harness compaction", () => { }, ]); getOrThrow( - await generateSummary( - messages, - 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"); }); @@ -508,16 +481,7 @@ describe("harness compaction", () => { ]); const summary = getOrThrow( - await generateSummary( - messages, - 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"); @@ -529,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, 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" }, @@ -537,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, abortedModel, 2000, "test-key"); + const abortedResult = await generateSummary(messages, models, abortedModel, 2000); expect(abortedResult).toMatchObject({ ok: false, error: { code: "aborted", message: "stopped" } }); }); @@ -565,7 +529,7 @@ describe("harness compaction", () => { settings: { enabled: true, reserveTokens: 500000, keepRecentTokens: 20000 }, }; - getOrThrow(await compact(preparation, model, "test-key")); + getOrThrow(await compact(preparation, models, model)); expect(seenOptions.map((options) => options?.maxTokens)).toEqual([128000, 128000]); }); @@ -583,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, historyModel, "test-key")).toMatchObject({ + expect(await compact(preparation, models, historyModel)).toMatchObject({ ok: false, error: { code: "summarization_failed", message: "Summarization failed: history failed" }, }); @@ -591,8 +555,8 @@ describe("harness compaction", () => { const { model: invalidModel } = createFauxModel(false); const invalidResult = await compact( { ...preparation, messagesToSummarize: [], firstKeptEntryId: "" }, + models, invalidModel, - "test-key", ); expect(invalidResult).toMatchObject({ ok: false, error: { code: "invalid_session" } }); }); @@ -617,7 +581,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, undefined, undefined, "high")); expect(seenOptions[0]).toMatchObject({ reasoning: "high" }); }); @@ -636,14 +600,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)).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)).toMatchObject({ ok: false, error: { code: "aborted", message: "prefix stopped" }, }); @@ -662,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!, 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(); diff --git a/packages/agent/test/scratch/simple.ts b/packages/agent/test/scratch/simple.ts index de6ba3a2..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"; +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 }) => [ 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/CHANGELOG.md b/packages/ai/CHANGELOG.md index e61d93dc..8a43f32f 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,9 +2,26 @@ ## [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. +- 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 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()`. +- `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)). + ### 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)). +- 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/ai/README.md b/packages/ai/README.md index 7fcd359b..eaa6906e 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,26 +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) - - [Provider-Scoped Environment Overrides](#provider-scoped-environment-overrides) - - [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 @@ -90,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 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. -// Fully typed with auto-complete support for both providers and models -const model = getModel('openai', 'gpt-4o-mini'); +```typescript +import { Type, type Context, type Tool } from '@earendil-works/pi-ai'; +import { builtinModels } from '@earendil-works/pi-ai/providers/all'; + +// 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')!; // Define tools with TypeBox schemas for type safety and validation const tools: Tool[] = [{ @@ -108,12 +117,13 @@ 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) { @@ -156,7 +166,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; } } @@ -168,7 +178,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', @@ -190,7 +199,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); } @@ -199,7 +208,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') { @@ -210,6 +219,182 @@ for (const block of response.content) { } ``` +Snippets in the rest of this README assume a `models` collection set up like this (with the relevant providers 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 + +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'; +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 (as in Quick Start): + +```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. `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 + +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 }); +// 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. + +### 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. @@ -217,7 +402,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 = { @@ -252,11 +437,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) { @@ -297,7 +482,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') { @@ -338,15 +523,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') { @@ -399,9 +582,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')) { @@ -411,13 +593,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() }] }); @@ -431,21 +614,21 @@ 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. +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 '@mariozechner/pi-ai'; +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) { @@ -458,19 +641,32 @@ for (const block of result.output) { } ``` +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. + +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 generateImages(model, { +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' } ] -}, { - apiKey: process.env.OPENROUTER_API_KEY }); ``` @@ -483,14 +679,14 @@ console.log(model.output); // ['image'] or ['image', 'text'] ### Notes and Limitations -- Use `getImageModel(...)`, not `getModel(...)`. -- Use `generateImages()`, not `stream()` / `complete()`. +- 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 `stream()` / `complete()` APIs with a model that supports image input. +- 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 @@ -500,16 +696,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) { @@ -517,8 +708,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' }); @@ -535,33 +726,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 @@ -569,7 +766,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) { @@ -600,11 +797,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 @@ -614,7 +811,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 @@ -622,21 +819,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 }); @@ -666,7 +862,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() } ] }; @@ -674,14 +870,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 @@ -689,7 +885,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)); } @@ -698,147 +894,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)', @@ -852,53 +917,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', @@ -925,6 +1008,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. @@ -962,30 +1075,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 @@ -993,98 +1173,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. 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 { 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' }); @@ -1092,69 +1260,21 @@ 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 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` | -| 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' -}); -``` - ### 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 API key discovery and provider configuration such as Cloudflare account IDs, Azure OpenAI settings, Vertex project/location, Bedrock settings, `PI_CACHE_RETENTION`, and `HTTP_PROXY`/`HTTPS_PROXY`. +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`. ```typescript -const model = getModel('cloudflare-ai-gateway', 'workers-ai/@cf/moonshotai/kimi-k2.6'); +const models = builtinModels(); +const model = models.getModel('cloudflare-ai-gateway', 'workers-ai/@cf/moonshotai/kimi-k2.6')!; -const response = await complete(model, context, { +const response = await models.complete(model, context, { env: { CLOUDFLARE_API_KEY: '...', CLOUDFLARE_ACCOUNT_ID: 'account-id', @@ -1165,24 +1285,47 @@ const response = await complete(model, context, { Use this when one process needs different provider settings per request, or when ambient environment variables should not leak into a provider call. -### 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 @@ -1194,8 +1337,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 @@ -1206,23 +1347,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 @@ -1239,76 +1363,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. @@ -1316,45 +1373,67 @@ 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: +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 identifier to `KnownApi` (for example `"bedrock-converse-stream"`) -- Create an options interface extending `StreamOptions` (for example `BedrockOptions`) +- 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. Provider Implementation (`src/providers/`) +#### 2. API Implementation (`src/api/.ts`, only for a new API) -Create a new provider file (for example `amazon-bedrock.ts`) that exports: +Create a new API implementation file (for example `bedrock-converse-stream.ts`) that exports exactly `stream` and `streamSimple`, plus: -- `stream()` function returning `AssistantMessageEventStream` -- `streamSimple()` for `SimpleStreamOptions` mapping -- Provider-specific options interface +- 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`) -#### 3. API Registry Integration (`src/providers/register-builtins.ts`) +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`. -- 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`) +#### 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` +- 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: @@ -1370,6 +1449,7 @@ Create or update test files to cover the new provider: - `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. diff --git a/packages/ai/package.json b/packages/ai/package.json index 5ba97133..41392a2b 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -5,42 +5,59 @@ "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/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 4f9cc78d..b6bcde6e 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 { @@ -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); @@ -2111,62 +2111,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/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/api/anthropic-messages.ts b/packages/ai/src/api/anthropic-messages.ts new file mode 100644 index 00000000..a546db75 --- /dev/null +++ b/packages/ai/src/api/anthropic-messages.ts @@ -0,0 +1,1242 @@ +import Anthropic from "@anthropic-ai/sdk"; +import type { + CacheControlEphemeral, + ContentBlockParam, + MessageCreateParamsStreaming, + MessageParam, + RawMessageStreamEvent, + RefusalStopDetails, +} from "@anthropic-ai/sdk/resources/messages.js"; +import { calculateCost } from "../models.ts"; +import type { + AnthropicMessagesCompat, + Api, + AssistantMessage, + CacheRetention, + Context, + ImageContent, + Message, + Model, + ProviderEnv, + SimpleStreamOptions, + StopReason, + StreamFunction, + StreamOptions, + TextContent, + ThinkingContent, + Tool, + ToolCall, + ToolResultMessage, +} from "../types.ts"; +import { AssistantMessageEventStream } from "../utils/event-stream.ts"; +import { headersToRecord } from "../utils/headers.ts"; +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"; + +/** + * Resolve cache retention preference. + * Defaults to "short" and uses PI_CACHE_RETENTION for backward compatibility. + */ +function resolveCacheRetention(cacheRetention?: CacheRetention, env?: ProviderEnv): CacheRetention { + if (cacheRetention) { + return cacheRetention; + } + if (getProviderEnvValue("PI_CACHE_RETENTION", env) === "long") { + return "long"; + } + return "short"; +} + +function getCacheControl( + model: Model<"anthropic-messages">, + cacheRetention?: CacheRetention, + env?: ProviderEnv, +): { retention: CacheRetention; cacheControl?: CacheControlEphemeral } { + const retention = resolveCacheRetention(cacheRetention, env); + if (retention === "none") { + return { retention }; + } + const ttl = retention === "long" && getAnthropicCompat(model).supportsLongCacheRetention ? "1h" : undefined; + return { + retention, + cacheControl: { type: "ephemeral", ...(ttl && { ttl }) }, + }; +} + +// Stealth mode: Mimic Claude Code's tool naming exactly +const claudeCodeVersion = "2.1.75"; + +// Claude Code 2.x tool names (canonical casing) +// Source: https://cchistory.mariozechner.at/data/prompts-2.1.11.md +// To update: https://github.com/badlogic/cchistory +const claudeCodeTools = [ + "Read", + "Write", + "Edit", + "Bash", + "Grep", + "Glob", + "AskUserQuestion", + "EnterPlanMode", + "ExitPlanMode", + "KillShell", + "NotebookEdit", + "Skill", + "Task", + "TaskOutput", + "TodoWrite", + "WebFetch", + "WebSearch", +]; + +const ccToolLookup = new Map(claudeCodeTools.map((t) => [t.toLowerCase(), t])); + +// Convert tool name to CC canonical casing if it matches (case-insensitive) +const toClaudeCodeName = (name: string) => ccToolLookup.get(name.toLowerCase()) ?? name; +const fromClaudeCodeName = (name: string, tools?: Tool[]) => { + if (tools && tools.length > 0) { + const lowerName = name.toLowerCase(); + const matchedTool = tools.find((tool) => tool.name.toLowerCase() === lowerName); + if (matchedTool) return matchedTool.name; + } + return name; +}; + +/** + * Convert content blocks to Anthropic API format + */ +function convertContentBlocks(content: (TextContent | ImageContent)[]): + | string + | Array< + | { type: "text"; text: string } + | { + type: "image"; + source: { + type: "base64"; + media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; + data: string; + }; + } + > { + // If only text blocks, return as concatenated string for simplicity + const hasImages = content.some((c) => c.type === "image"); + if (!hasImages) { + return sanitizeSurrogates(content.map((c) => (c as TextContent).text).join("\n")); + } + + // If we have images, convert to content block array + const blocks = content.map((block) => { + if (block.type === "text") { + return { + type: "text" as const, + text: sanitizeSurrogates(block.text), + }; + } + return { + type: "image" as const, + source: { + type: "base64" as const, + media_type: block.mimeType as "image/jpeg" | "image/png" | "image/gif" | "image/webp", + data: block.data, + }, + }; + }); + + // If only images (no text), add placeholder text block + const hasText = blocks.some((b) => b.type === "text"); + if (!hasText) { + blocks.unshift({ + type: "text" as const, + text: "(see attached image)", + }); + } + + return blocks; +} + +export type AnthropicEffort = "low" | "medium" | "high" | "xhigh" | "max"; + +export type AnthropicThinkingDisplay = "summarized" | "omitted"; + +const FINE_GRAINED_TOOL_STREAMING_BETA = "fine-grained-tool-streaming-2025-05-14"; +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, + supportsTemperature: model.compat?.supportsTemperature ?? true, + allowEmptySignature: model.compat?.allowEmptySignature ?? false, + }; +} + +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 `streamSimple()` maps + * a simple reasoning level to this option, or callers set it explicitly). + */ + thinkingEnabled?: boolean; + /** + * Token budget for extended thinking (older models only). + * Ignored for adaptive thinking models. + * Default: 1024 when `thinkingEnabled` is true and no budget is provided. + */ + thinkingBudgetTokens?: number; + /** + * Effort level for adaptive thinking models. + * Controls how much thinking Claude allocates: + * - "max": Always thinks with no constraints (Opus 4.6 only) + * - "xhigh": Highest reasoning level (Opus 4.7+, Fable 5) + * - "high": Always thinks, deep reasoning + * - "medium": Moderate thinking, may skip for simple queries + * - "low": Minimal thinking, skips for simple tasks + * Ignored for older models. + * Default: omitted unless `streamSimple()` maps a simple reasoning + * level to this option. + */ + effort?: AnthropicEffort; + /** + * Controls how thinking content is returned in API responses. + * - "summarized": Thinking blocks contain summarized thinking text. + * - "omitted": Thinking blocks return an empty thinking field; the encrypted + * signature still travels back for multi-turn continuity. Use for faster + * time-to-first-text-token when your UI does not surface thinking. + * + * Note: Anthropic's API default for Claude Opus 4.7 and Claude Mythos Preview + * is "omitted". We default to "summarized" here to keep behavior consistent + * with older Claude 4 models. Set this explicitly to "omitted" to opt in. + * Default: "summarized" when thinking is enabled. + */ + thinkingDisplay?: AnthropicThinkingDisplay; + /** + * Whether to request the interleaved thinking beta header for non-adaptive + * thinking models. Adaptive thinking models have interleaved thinking built in, + * so the header is skipped for them regardless of this setting. + * Default: true. + */ + interleavedThinking?: boolean; + /** + * Anthropic tool choice behavior. String values map to Anthropic's built-in + * choices; `{ type: "tool", name }` forces a specific tool. + * Default: omitted (Anthropic default behavior, currently equivalent to auto). + */ + toolChoice?: "auto" | "any" | "none" | { type: "tool"; name: string }; + /** + * Pre-built Anthropic client instance. When provided, skips internal client + * construction entirely. Use this to inject alternative SDK clients such as + * `AnthropicVertex` that shares the same messaging API. + */ + client?: Anthropic; +} + +function mergeHeaders(...headerSources: (Record | undefined)[]): Record { + const merged: Record = {}; + for (const headers of headerSources) { + if (headers) { + Object.assign(merged, headers); + } + } + return merged; +} + +interface ServerSentEvent { + event: string | null; + data: string; + raw: string[]; +} + +interface SseDecoderState { + event: string | null; + data: string[]; + raw: string[]; +} + +const ANTHROPIC_MESSAGE_EVENTS: ReadonlySet = new Set([ + "message_start", + "message_delta", + "message_stop", + "content_block_start", + "content_block_delta", + "content_block_stop", +]); + +function flushSseEvent(state: SseDecoderState): ServerSentEvent | null { + if (!state.event && state.data.length === 0) { + return null; + } + + const event: ServerSentEvent = { + event: state.event, + data: state.data.join("\n"), + raw: [...state.raw], + }; + state.event = null; + state.data = []; + state.raw = []; + return event; +} + +function decodeSseLine(line: string, state: SseDecoderState): ServerSentEvent | null { + if (line === "") { + return flushSseEvent(state); + } + + state.raw.push(line); + if (line.startsWith(":")) { + return null; + } + + const delimiterIndex = line.indexOf(":"); + const fieldName = delimiterIndex === -1 ? line : line.slice(0, delimiterIndex); + let value = delimiterIndex === -1 ? "" : line.slice(delimiterIndex + 1); + if (value.startsWith(" ")) { + value = value.slice(1); + } + + if (fieldName === "event") { + state.event = value; + } else if (fieldName === "data") { + state.data.push(value); + } + + return null; +} + +function nextLineBreakIndex(text: string): number { + const carriageReturnIndex = text.indexOf("\r"); + const newlineIndex = text.indexOf("\n"); + if (carriageReturnIndex === -1) { + return newlineIndex; + } + if (newlineIndex === -1) { + return carriageReturnIndex; + } + return Math.min(carriageReturnIndex, newlineIndex); +} + +function consumeLine(text: string): { line: string; rest: string } | null { + const lineBreakIndex = nextLineBreakIndex(text); + if (lineBreakIndex === -1) { + return null; + } + + let nextIndex = lineBreakIndex + 1; + if (text[lineBreakIndex] === "\r" && text[nextIndex] === "\n") { + nextIndex += 1; + } + + return { + line: text.slice(0, lineBreakIndex), + rest: text.slice(nextIndex), + }; +} + +async function* iterateSseMessages( + body: ReadableStream, + signal?: AbortSignal, +): AsyncGenerator { + const reader = body.getReader(); + const decoder = new TextDecoder(); + const state: SseDecoderState = { event: null, data: [], raw: [] }; + let buffer = ""; + + try { + while (true) { + if (signal?.aborted) { + throw new Error("Request was aborted"); + } + + const { value, done } = await reader.read(); + if (done) { + break; + } + + buffer += decoder.decode(value, { stream: true }); + let consumed = consumeLine(buffer); + while (consumed) { + buffer = consumed.rest; + const event = decodeSseLine(consumed.line, state); + if (event) { + yield event; + } + consumed = consumeLine(buffer); + } + } + + buffer += decoder.decode(); + let consumed = consumeLine(buffer); + while (consumed) { + buffer = consumed.rest; + const event = decodeSseLine(consumed.line, state); + if (event) { + yield event; + } + consumed = consumeLine(buffer); + } + + if (buffer.length > 0) { + const event = decodeSseLine(buffer, state); + if (event) { + yield event; + } + } + + const trailingEvent = flushSseEvent(state); + if (trailingEvent) { + yield trailingEvent; + } + } finally { + reader.releaseLock(); + } +} + +async function* iterateAnthropicEvents( + response: Response, + signal?: AbortSignal, +): AsyncGenerator { + if (!response.body) { + throw new Error("Attempted to iterate over an Anthropic response with no body"); + } + + let sawMessageStart = false; + let sawMessageEnd = false; + + for await (const sse of iterateSseMessages(response.body, signal)) { + if (sse.event === "error") { + throw new Error(sse.data); + } + + if (!ANTHROPIC_MESSAGE_EVENTS.has(sse.event ?? "")) { + continue; + } + + try { + const event = parseJsonWithRepair(sse.data); + if (event.type === "message_start") { + sawMessageStart = true; + } else if (event.type === "message_stop") { + sawMessageEnd = true; + } + yield event; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error( + `Could not parse Anthropic SSE event ${sse.event}: ${message}; data=${sse.data}; raw=${sse.raw.join("\\n")}`, + ); + } + } + + if (sawMessageStart && !sawMessageEnd) { + throw new Error("Anthropic stream ended before message_stop"); + } +} + +export const stream: StreamFunction<"anthropic-messages", AnthropicOptions> = ( + model: Model<"anthropic-messages">, + context: Context, + options?: AnthropicOptions, +): AssistantMessageEventStream => { + const stream = new AssistantMessageEventStream(); + + (async () => { + const output: AssistantMessage = { + role: "assistant", + content: [], + api: model.api as 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(), + }; + + try { + let client: Anthropic; + let isOAuth: boolean; + + if (options?.client) { + client = options.client; + isOAuth = false; + } else { + const apiKey = options?.apiKey; + if (!apiKey) { + throw new Error(`No API key for provider: ${model.provider}`); + } + + let copilotDynamicHeaders: Record | undefined; + if (model.provider === "github-copilot") { + const hasImages = hasCopilotVisionInput(context.messages); + copilotDynamicHeaders = buildCopilotDynamicHeaders({ + messages: context.messages, + hasImages, + }); + } + + const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env); + const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId; + + const created = createClient( + model, + apiKey, + options?.interleavedThinking ?? true, + shouldUseFineGrainedToolStreamingBeta(model, context), + options?.headers, + copilotDynamicHeaders, + cacheSessionId, + options?.env, + ); + client = created.client; + isOAuth = created.isOAuthToken; + } + let params = buildParams(model, context, isOAuth, options); + const nextParams = await options?.onPayload?.(params, model); + if (nextParams !== undefined) { + params = nextParams as MessageCreateParamsStreaming; + } + const requestOptions = { + ...(options?.signal ? { signal: options.signal } : {}), + ...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}), + maxRetries: options?.maxRetries ?? 0, + }; + const response = await client.messages.create({ ...params, stream: true }, requestOptions).asResponse(); + await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model); + stream.push({ type: "start", partial: output }); + + type Block = (ThinkingContent | TextContent | (ToolCall & { partialJson: string })) & { index: number }; + const blocks = output.content as Block[]; + + for await (const event of iterateAnthropicEvents(response, options?.signal)) { + if (event.type === "message_start") { + output.responseId = event.message.id; + // Capture initial token usage from message_start event + // This ensures we have input token counts even if the stream is aborted early + output.usage.input = event.message.usage.input_tokens || 0; + output.usage.output = event.message.usage.output_tokens || 0; + output.usage.cacheRead = event.message.usage.cache_read_input_tokens || 0; + output.usage.cacheWrite = event.message.usage.cache_creation_input_tokens || 0; + output.usage.cacheWrite1h = event.message.usage.cache_creation?.ephemeral_1h_input_tokens || 0; + // Anthropic doesn't provide total_tokens, compute from components + output.usage.totalTokens = + output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite; + calculateCost(model, output.usage); + } else if (event.type === "content_block_start") { + if (event.content_block.type === "text") { + const block: Block = { + type: "text", + text: "", + index: event.index, + }; + output.content.push(block); + stream.push({ type: "text_start", contentIndex: output.content.length - 1, partial: output }); + } else if (event.content_block.type === "thinking") { + const block: Block = { + type: "thinking", + thinking: "", + thinkingSignature: "", + index: event.index, + }; + output.content.push(block); + stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output }); + } else if (event.content_block.type === "redacted_thinking") { + const block: Block = { + type: "thinking", + thinking: "[Reasoning redacted]", + thinkingSignature: event.content_block.data, + redacted: true, + index: event.index, + }; + output.content.push(block); + stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output }); + } else if (event.content_block.type === "tool_use") { + const block: Block = { + type: "toolCall", + id: event.content_block.id, + name: isOAuth + ? fromClaudeCodeName(event.content_block.name, context.tools) + : event.content_block.name, + arguments: (event.content_block.input as Record) ?? {}, + partialJson: "", + index: event.index, + }; + output.content.push(block); + stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output }); + } + } else if (event.type === "content_block_delta") { + if (event.delta.type === "text_delta") { + const index = blocks.findIndex((b) => b.index === event.index); + const block = blocks[index]; + if (block && block.type === "text") { + block.text += event.delta.text; + stream.push({ + type: "text_delta", + contentIndex: index, + delta: event.delta.text, + partial: output, + }); + } + } else if (event.delta.type === "thinking_delta") { + const index = blocks.findIndex((b) => b.index === event.index); + const block = blocks[index]; + if (block && block.type === "thinking") { + block.thinking += event.delta.thinking; + stream.push({ + type: "thinking_delta", + contentIndex: index, + delta: event.delta.thinking, + partial: output, + }); + } + } else if (event.delta.type === "input_json_delta") { + const index = blocks.findIndex((b) => b.index === event.index); + const block = blocks[index]; + if (block && block.type === "toolCall") { + block.partialJson += event.delta.partial_json; + block.arguments = parseStreamingJson(block.partialJson); + stream.push({ + type: "toolcall_delta", + contentIndex: index, + delta: event.delta.partial_json, + partial: output, + }); + } + } else if (event.delta.type === "signature_delta") { + const index = blocks.findIndex((b) => b.index === event.index); + const block = blocks[index]; + if (block && block.type === "thinking") { + block.thinkingSignature = block.thinkingSignature || ""; + block.thinkingSignature += event.delta.signature; + } + } + } else if (event.type === "content_block_stop") { + const index = blocks.findIndex((b) => b.index === event.index); + const block = blocks[index]; + if (block) { + delete (block as any).index; + if (block.type === "text") { + stream.push({ + type: "text_end", + contentIndex: index, + content: block.text, + partial: output, + }); + } else if (block.type === "thinking") { + stream.push({ + type: "thinking_end", + contentIndex: index, + content: block.thinking, + partial: output, + }); + } else if (block.type === "toolCall") { + block.arguments = parseStreamingJson(block.partialJson); + // Finalize in-place and strip the scratch buffer so replay only + // carries parsed arguments. + delete (block as { partialJson?: string }).partialJson; + stream.push({ + type: "toolcall_end", + contentIndex: index, + toolCall: block, + partial: output, + }); + } + } + } else if (event.type === "message_delta") { + if (event.delta.stop_reason) { + const stopReasonResult = mapStopReason(event.delta.stop_reason, event.delta.stop_details); + output.stopReason = stopReasonResult.stopReason; + if (stopReasonResult.errorMessage) { + output.errorMessage = stopReasonResult.errorMessage; + } + } + // Only update usage fields if present (not null). + // Preserves input_tokens from message_start when proxies omit it in message_delta. + if (event.usage.input_tokens != null) { + output.usage.input = event.usage.input_tokens; + } + if (event.usage.output_tokens != null) { + output.usage.output = event.usage.output_tokens; + } + if (event.usage.cache_read_input_tokens != null) { + output.usage.cacheRead = event.usage.cache_read_input_tokens; + } + if (event.usage.cache_creation_input_tokens != null) { + output.usage.cacheWrite = event.usage.cache_creation_input_tokens; + } + // Anthropic doesn't provide total_tokens, compute from components + output.usage.totalTokens = + output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite; + calculateCost(model, output.usage); + } + } + + if (options?.signal?.aborted) { + throw new Error("Request was aborted"); + } + + if (output.stopReason === "aborted" || output.stopReason === "error") { + throw new Error(output.errorMessage || "An unknown error occurred"); + } + + stream.push({ type: "done", reason: output.stopReason, message: output }); + stream.end(); + } catch (error) { + for (const block of output.content) { + delete (block as { index?: number }).index; + // partialJson is only a streaming scratch buffer; never persist it. + delete (block as { partialJson?: string }).partialJson; + } + output.stopReason = options?.signal?.aborted ? "aborted" : "error"; + output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error); + stream.push({ type: "error", reason: output.stopReason, error: output }); + stream.end(); + } + })(); + + return stream; +}; + +/** + * Map ThinkingLevel to Anthropic effort levels for adaptive thinking. + * Note: effort "max" is only valid on Opus 4.6, while Opus 4.7+ and Fable 5 support "xhigh". + */ +function mapThinkingLevelToEffort( + model: Model<"anthropic-messages">, + level: SimpleStreamOptions["reasoning"], +): AnthropicEffort { + const mapped = level ? model.thinkingLevelMap?.[level] : undefined; + if (typeof mapped === "string") return mapped as AnthropicEffort; + + switch (level) { + case "minimal": + case "low": + return "low"; + case "medium": + return "medium"; + case "high": + return "high"; + default: + return "high"; + } +} + +export const streamSimple: StreamFunction<"anthropic-messages", SimpleStreamOptions> = ( + model: Model<"anthropic-messages">, + context: Context, + options?: SimpleStreamOptions, +): AssistantMessageEventStream => { + const apiKey = options?.apiKey; + if (!apiKey) { + throw new Error(`No API key for provider: ${model.provider}`); + } + + const base = buildBaseOptions(model, options, apiKey); + if (!options?.reasoning) { + 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 stream(model, context, { + ...base, + thinkingEnabled: true, + effort, + } satisfies AnthropicOptions); + } + + // Undefined means the caller did not request an output cap; let the helper use the model cap. + // Do not coerce to 0 here, or the thinking budget would become the entire max_tokens value. + const adjusted = adjustMaxTokensForThinking( + base.maxTokens, + model.maxTokens, + options.reasoning, + options.thinkingBudgets, + ); + + return stream(model, context, { + ...base, + maxTokens: adjusted.maxTokens, + thinkingEnabled: true, + thinkingBudgetTokens: adjusted.thinkingBudget, + } satisfies AnthropicOptions); +}; + +function isOAuthToken(apiKey: string): boolean { + return apiKey.includes("sk-ant-oat"); +} + +function createClient( + model: Model<"anthropic-messages">, + apiKey: string, + interleavedThinking: boolean, + useFineGrainedToolStreamingBeta: boolean, + optionsHeaders?: Record, + 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; + const betaFeatures: string[] = []; + if (useFineGrainedToolStreamingBeta) { + betaFeatures.push(FINE_GRAINED_TOOL_STREAMING_BETA); + } + if (needsInterleavedBeta) { + 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, + baseURL: model.baseUrl, + dangerouslyAllowBrowser: true, + defaultHeaders: mergeHeaders( + { + accept: "application/json", + "anthropic-dangerous-direct-browser-access": "true", + ...(betaFeatures.length > 0 ? { "anthropic-beta": betaFeatures.join(",") } : {}), + }, + model.headers, + dynamicHeaders, + optionsHeaders, + ), + }); + + return { client, isOAuthToken: false }; + } + + // OAuth: Bearer auth, Claude Code identity headers + if (isOAuthToken(apiKey)) { + const client = new Anthropic({ + apiKey: null, + authToken: apiKey, + baseURL: model.baseUrl, + dangerouslyAllowBrowser: true, + defaultHeaders: mergeHeaders( + { + accept: "application/json", + "anthropic-dangerous-direct-browser-access": "true", + "anthropic-beta": ["claude-code-20250219", "oauth-2025-04-20", ...betaFeatures].join(","), + "user-agent": `claude-cli/${claudeCodeVersion}`, + "x-app": "cli", + }, + model.headers, + optionsHeaders, + ), + }); + + return { client, isOAuthToken: true }; + } + + // API key auth + const sessionAffinityHeaders: Record = + sessionId && getAnthropicCompat(model).sendSessionAffinityHeaders ? { "x-session-affinity": sessionId } : {}; + const client = new Anthropic({ + apiKey, + 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, + ), + }); + + return { client, isOAuthToken: false }; +} + +function buildParams( + model: Model<"anthropic-messages">, + context: Context, + isOAuthToken: boolean, + options?: AnthropicOptions, +): MessageCreateParamsStreaming { + const { cacheControl } = getCacheControl(model, options?.cacheRetention, options?.env); + const compat = getAnthropicCompat(model); + const params: MessageCreateParamsStreaming = { + model: model.id, + messages: convertMessages(context.messages, model, isOAuthToken, cacheControl, compat.allowEmptySignature), + max_tokens: options?.maxTokens ?? model.maxTokens, + stream: true, + }; + + // For OAuth tokens, we MUST include Claude Code identity + if (isOAuthToken) { + params.system = [ + { + type: "text", + text: "You are Claude Code, Anthropic's official CLI for Claude.", + ...(cacheControl ? { cache_control: cacheControl } : {}), + }, + ]; + if (context.systemPrompt) { + params.system.push({ + type: "text", + text: sanitizeSurrogates(context.systemPrompt), + ...(cacheControl ? { cache_control: cacheControl } : {}), + }); + } + } else if (context.systemPrompt) { + // Add cache control to system prompt for non-OAuth tokens + params.system = [ + { + type: "text", + text: sanitizeSurrogates(context.systemPrompt), + ...(cacheControl ? { cache_control: cacheControl } : {}), + }, + ]; + } + + // Temperature is incompatible with extended thinking and unsupported on Claude Opus 4.7+. + if (options?.temperature !== undefined && !options?.thinkingEnabled && compat.supportsTemperature) { + params.temperature = options.temperature; + } + + if (context.tools && context.tools.length > 0) { + params.tools = convertTools( + context.tools, + isOAuthToken, + compat.supportsEagerToolInputStreaming, + compat.supportsCacheControlOnTools ? cacheControl : undefined, + ); + } + + // Configure thinking mode: adaptive, budget-based, or explicitly disabled. + if (model.reasoning) { + if (options?.thinkingEnabled) { + // Default to "summarized" so Opus 4.7 and Mythos Preview behave like + // older Claude 4 models (whose API default is also "summarized"). + const display: AnthropicThinkingDisplay = options.thinkingDisplay ?? "summarized"; + if (model.compat?.forceAdaptiveThinking === true) { + // Adaptive thinking: Claude decides when and how much to think. + params.thinking = { type: "adaptive", display }; + if (options.effort) { + // The Anthropic SDK types can lag newly supported effort values such as "xhigh". + params.output_config = + options.effort === "xhigh" + ? ({ effort: options.effort } as unknown as NonNullable< + MessageCreateParamsStreaming["output_config"] + >) + : { effort: options.effort }; + } + } else { + // Budget-based thinking for older models + params.thinking = { + type: "enabled", + budget_tokens: options.thinkingBudgetTokens || 1024, + display, + }; + } + } else if (options?.thinkingEnabled === false && model.thinkingLevelMap?.off !== null) { + params.thinking = { type: "disabled" }; + } + } + + if (options?.metadata) { + const userId = options.metadata.user_id; + if (typeof userId === "string") { + params.metadata = { user_id: userId }; + } + } + + if (options?.toolChoice) { + if (typeof options.toolChoice === "string") { + params.tool_choice = { type: options.toolChoice }; + } else { + params.tool_choice = options.toolChoice; + } + } + + return params; +} + +// Normalize tool call IDs to match Anthropic's required pattern and length +function normalizeToolCallId(id: string): string { + return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64); +} + +function convertMessages( + messages: Message[], + model: Model<"anthropic-messages">, + isOAuthToken: boolean, + cacheControl?: CacheControlEphemeral, + allowEmptySignature = false, +): MessageParam[] { + const params: MessageParam[] = []; + + // Transform messages for cross-provider compatibility + const transformedMessages = transformMessages(messages, model, normalizeToolCallId); + + for (let i = 0; i < transformedMessages.length; i++) { + const msg = transformedMessages[i]; + + if (msg.role === "user") { + if (typeof msg.content === "string") { + if (msg.content.trim().length > 0) { + params.push({ + role: "user", + content: sanitizeSurrogates(msg.content), + }); + } + } else { + const blocks: ContentBlockParam[] = msg.content.map((item) => { + if (item.type === "text") { + return { + type: "text", + text: sanitizeSurrogates(item.text), + }; + } else { + return { + type: "image", + source: { + type: "base64", + media_type: item.mimeType as "image/jpeg" | "image/png" | "image/gif" | "image/webp", + data: item.data, + }, + }; + } + }); + const filteredBlocks = blocks.filter((b) => { + if (b.type === "text") { + return b.text.trim().length > 0; + } + return true; + }); + if (filteredBlocks.length === 0) continue; + params.push({ + role: "user", + content: filteredBlocks, + }); + } + } else if (msg.role === "assistant") { + const blocks: ContentBlockParam[] = []; + + for (const block of msg.content) { + if (block.type === "text") { + if (block.text.trim().length === 0) continue; + blocks.push({ + type: "text", + text: sanitizeSurrogates(block.text), + }); + } else if (block.type === "thinking") { + // Redacted thinking: pass the opaque payload back as redacted_thinking + if (block.redacted) { + blocks.push({ + type: "redacted_thinking", + data: block.thinkingSignature!, + }); + continue; + } + if (block.thinking.trim().length === 0) continue; + // If thinking signature is missing/empty (e.g., from aborted stream), + // convert to plain text for Anthropic. Some compatible providers emit + // and accept empty signatures, so let marked models preserve the block. + if (!block.thinkingSignature || block.thinkingSignature.trim().length === 0) { + blocks.push( + allowEmptySignature + ? { + type: "thinking", + thinking: sanitizeSurrogates(block.thinking), + signature: "", + } + : { + type: "text", + text: sanitizeSurrogates(block.thinking), + }, + ); + } else { + blocks.push({ + type: "thinking", + thinking: sanitizeSurrogates(block.thinking), + signature: block.thinkingSignature, + }); + } + } else if (block.type === "toolCall") { + blocks.push({ + type: "tool_use", + id: block.id, + name: isOAuthToken ? toClaudeCodeName(block.name) : block.name, + input: block.arguments ?? {}, + }); + } + } + if (blocks.length === 0) continue; + params.push({ + role: "assistant", + content: blocks, + }); + } else if (msg.role === "toolResult") { + // Collect all consecutive toolResult messages, needed for z.ai Anthropic endpoint + const toolResults: ContentBlockParam[] = []; + + // Add the current tool result + toolResults.push({ + type: "tool_result", + tool_use_id: msg.toolCallId, + content: convertContentBlocks(msg.content), + is_error: msg.isError, + }); + + // Look ahead for consecutive toolResult messages + let j = i + 1; + while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") { + const nextMsg = transformedMessages[j] as ToolResultMessage; // We know it's a toolResult + toolResults.push({ + type: "tool_result", + tool_use_id: nextMsg.toolCallId, + content: convertContentBlocks(nextMsg.content), + is_error: nextMsg.isError, + }); + j++; + } + + // Skip the messages we've already processed + i = j - 1; + + // Add a single user message with all tool results + params.push({ + role: "user", + content: toolResults, + }); + } + } + + // Add cache_control to the last user message to cache conversation history + if (cacheControl && params.length > 0) { + const lastMessage = params[params.length - 1]; + if (lastMessage.role === "user") { + if (Array.isArray(lastMessage.content)) { + const lastBlock = lastMessage.content[lastMessage.content.length - 1]; + if ( + lastBlock && + (lastBlock.type === "text" || lastBlock.type === "image" || lastBlock.type === "tool_result") + ) { + (lastBlock as any).cache_control = cacheControl; + } + } else if (typeof lastMessage.content === "string") { + lastMessage.content = [ + { + type: "text", + text: lastMessage.content, + cache_control: cacheControl, + }, + ] as any; + } + } + } + + return params; +} + +function shouldUseFineGrainedToolStreamingBeta(model: Model<"anthropic-messages">, context: Context): boolean { + return !!context.tools?.length && !getAnthropicCompat(model).supportsEagerToolInputStreaming; +} + +function convertTools( + tools: Tool[], + isOAuthToken: boolean, + supportsEagerToolInputStreaming: boolean, + cacheControl?: CacheControlEphemeral, +): Anthropic.Messages.Tool[] { + if (!tools) return []; + + return tools.map((tool, index) => { + const schema = tool.parameters as { properties?: unknown; required?: string[] }; + + return { + name: isOAuthToken ? toClaudeCodeName(tool.name) : tool.name, + description: tool.description, + ...(supportsEagerToolInputStreaming ? { eager_input_streaming: true } : {}), + input_schema: { + type: "object", + properties: schema.properties ?? {}, + required: schema.required ?? [], + }, + ...(cacheControl && index === tools.length - 1 ? { cache_control: cacheControl } : {}), + }; + }); +} + +function mapStopReason( + reason: Anthropic.Messages.StopReason | string, + stopDetails?: RefusalStopDetails | null, +): { stopReason: StopReason; errorMessage?: string } { + switch (reason) { + case "end_turn": + return { stopReason: "stop" }; + case "max_tokens": + return { stopReason: "length" }; + case "tool_use": + return { stopReason: "toolUse" }; + case "refusal": + return { + stopReason: "error", + errorMessage: stopDetails?.explanation || `The model refused to complete the request`, + }; + case "pause_turn": // Stop is good enough -> resubmit + return { stopReason: "stop" }; + case "stop_sequence": + return { stopReason: "stop" }; // We don't supply stop sequences, so this should never happen + case "sensitive": // Content flagged by safety filters (not yet in SDK types) + return { stopReason: "error" }; + default: + // Handle unknown stop reasons gracefully (API may add new values) + throw new Error(`Unhandled stop reason: ${reason}`); + } +} 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/api/azure-openai-responses.ts b/packages/ai/src/api/azure-openai-responses.ts new file mode 100644 index 00000000..8150a82b --- /dev/null +++ b/packages/ai/src/api/azure-openai-responses.ts @@ -0,0 +1,299 @@ +import { AzureOpenAI } from "openai"; +import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js"; +import { clampThinkingLevel } from "../models.ts"; +import type { + Api, + AssistantMessage, + Context, + Model, + SimpleStreamOptions, + StreamFunction, + StreamOptions, +} from "../types.ts"; +import { AssistantMessageEventStream } from "../utils/event-stream.ts"; +import { headersToRecord } from "../utils/headers.ts"; +import { getProviderEnvValue } from "../utils/provider-env.ts"; +import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts"; +import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.ts"; +import { buildBaseOptions } from "./simple-options.ts"; + +const DEFAULT_AZURE_API_VERSION = "v1"; +const AZURE_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode", "azure-openai-responses"]); + +function parseDeploymentNameMap(value: string | undefined): Map { + const map = new Map(); + if (!value) return map; + for (const entry of value.split(",")) { + const trimmed = entry.trim(); + if (!trimmed) continue; + const [modelId, deploymentName] = trimmed.split("=", 2); + if (!modelId || !deploymentName) continue; + map.set(modelId.trim(), deploymentName.trim()); + } + return map; +} + +function resolveDeploymentName(model: Model<"azure-openai-responses">, options?: AzureOpenAIResponsesOptions): string { + if (options?.azureDeploymentName) { + return options.azureDeploymentName; + } + const mappedDeployment = parseDeploymentNameMap( + getProviderEnvValue("AZURE_OPENAI_DEPLOYMENT_NAME_MAP", options?.env), + ).get(model.id); + return mappedDeployment || model.id; +} + +function formatAzureOpenAIError(error: unknown): string { + if (error instanceof Error) { + const status = (error as Error & { status?: unknown }).status; + const statusCode = typeof status === "number" ? status : undefined; + if (statusCode !== undefined) { + return `Azure OpenAI API error (${statusCode}): ${error.message}`; + } + return error.message; + } + try { + return JSON.stringify(error); + } catch { + return String(error); + } +} + +// Azure OpenAI Responses-specific options +export interface AzureOpenAIResponsesOptions extends StreamOptions { + reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh"; + reasoningSummary?: "auto" | "detailed" | "concise" | null; + azureApiVersion?: string; + azureResourceName?: string; + azureBaseUrl?: string; + azureDeploymentName?: string; +} + +/** + * Generate function for Azure OpenAI Responses API + */ +export const stream: StreamFunction<"azure-openai-responses", AzureOpenAIResponsesOptions> = ( + model: Model<"azure-openai-responses">, + context: Context, + options?: AzureOpenAIResponsesOptions, +): AssistantMessageEventStream => { + const stream = new AssistantMessageEventStream(); + + // Start async processing + (async () => { + const deploymentName = resolveDeploymentName(model, options); + + const output: AssistantMessage = { + role: "assistant", + content: [], + api: "azure-openai-responses" as 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(), + }; + + try { + // Create Azure OpenAI client + const apiKey = options?.apiKey; + if (!apiKey) { + throw new Error(`No API key for provider: ${model.provider}`); + } + const client = createClient(model, apiKey, options); + let params = buildParams(model, context, options, deploymentName); + const nextParams = await options?.onPayload?.(params, model); + if (nextParams !== undefined) { + params = nextParams as ResponseCreateParamsStreaming; + } + const requestOptions = { + ...(options?.signal ? { signal: options.signal } : {}), + ...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}), + maxRetries: options?.maxRetries ?? 0, + }; + const { data: openaiStream, response } = await client.responses.create(params, requestOptions).withResponse(); + await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model); + stream.push({ type: "start", partial: output }); + + await processResponsesStream(openaiStream, output, stream, model); + + if (options?.signal?.aborted) { + throw new Error("Request was aborted"); + } + + if (output.stopReason === "aborted" || output.stopReason === "error") { + throw new Error("An unknown error occurred"); + } + + stream.push({ type: "done", reason: output.stopReason, message: output }); + stream.end(); + } catch (error) { + for (const block of output.content) { + delete (block as { index?: number }).index; + // partialJson is only a streaming scratch buffer; never persist it. + delete (block as { partialJson?: string }).partialJson; + } + output.stopReason = options?.signal?.aborted ? "aborted" : "error"; + output.errorMessage = formatAzureOpenAIError(error); + stream.push({ type: "error", reason: output.stopReason, error: output }); + stream.end(); + } + })(); + + return stream; +}; + +export const streamSimple: StreamFunction<"azure-openai-responses", SimpleStreamOptions> = ( + model: Model<"azure-openai-responses">, + context: Context, + options?: SimpleStreamOptions, +): AssistantMessageEventStream => { + const apiKey = options?.apiKey; + if (!apiKey) { + throw new Error(`No API key for provider: ${model.provider}`); + } + + const base = buildBaseOptions(model, options, apiKey); + const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined; + const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning; + + return stream(model, context, { + ...base, + reasoningEffort, + } satisfies AzureOpenAIResponsesOptions); +}; + +function normalizeAzureBaseUrl(baseUrl: string): string { + const trimmed = baseUrl.trim().replace(/\/+$/, ""); + let url: URL; + try { + url = new URL(trimmed); + } catch { + throw new Error(`Invalid Azure OpenAI base URL: ${baseUrl}`); + } + + const isAzureHost = + url.hostname.endsWith(".openai.azure.com") || url.hostname.endsWith(".cognitiveservices.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")) { + url.pathname = "/openai/v1"; + url.search = ""; + } + + return url.toString().replace(/\/+$/, ""); +} + +function buildDefaultBaseUrl(resourceName: string): string { + return `https://${resourceName}.openai.azure.com/openai/v1`; +} + +function resolveAzureConfig( + model: Model<"azure-openai-responses">, + options?: AzureOpenAIResponsesOptions, +): { baseUrl: string; apiVersion: string } { + const apiVersion = + options?.azureApiVersion || + getProviderEnvValue("AZURE_OPENAI_API_VERSION", options?.env) || + DEFAULT_AZURE_API_VERSION; + + const baseUrl = + options?.azureBaseUrl?.trim() || getProviderEnvValue("AZURE_OPENAI_BASE_URL", options?.env)?.trim() || undefined; + const resourceName = options?.azureResourceName || getProviderEnvValue("AZURE_OPENAI_RESOURCE_NAME", options?.env); + + let resolvedBaseUrl = baseUrl; + + if (!resolvedBaseUrl && resourceName) { + resolvedBaseUrl = buildDefaultBaseUrl(resourceName); + } + + if (!resolvedBaseUrl && model.baseUrl) { + resolvedBaseUrl = model.baseUrl; + } + + if (!resolvedBaseUrl) { + throw new Error( + "Azure OpenAI base URL is required. Set AZURE_OPENAI_BASE_URL or AZURE_OPENAI_RESOURCE_NAME, or pass azureBaseUrl, azureResourceName, or model.baseUrl.", + ); + } + + return { + baseUrl: normalizeAzureBaseUrl(resolvedBaseUrl), + apiVersion, + }; +} + +function createClient(model: Model<"azure-openai-responses">, apiKey: string, options?: AzureOpenAIResponsesOptions) { + const headers = { ...model.headers }; + + if (options?.headers) { + Object.assign(headers, options.headers); + } + + const { baseUrl, apiVersion } = resolveAzureConfig(model, options); + + return new AzureOpenAI({ + apiKey, + apiVersion, + dangerouslyAllowBrowser: true, + defaultHeaders: headers, + baseURL: baseUrl, + }); +} + +function buildParams( + model: Model<"azure-openai-responses">, + context: Context, + options: AzureOpenAIResponsesOptions | undefined, + deploymentName: string, +) { + const messages = convertResponsesMessages(model, context, AZURE_TOOL_CALL_PROVIDERS); + + const params: ResponseCreateParamsStreaming = { + model: deploymentName, + input: messages, + stream: true, + prompt_cache_key: clampOpenAIPromptCacheKey(options?.sessionId), + store: false, + }; + + if (options?.maxTokens) { + params.max_output_tokens = options?.maxTokens; + } + + if (options?.temperature !== undefined) { + params.temperature = options?.temperature; + } + + if (context.tools && context.tools.length > 0) { + params.tools = convertResponsesTools(context.tools); + } + + if (model.reasoning) { + if (options?.reasoningEffort || options?.reasoningSummary) { + const effort = options?.reasoningEffort + ? (model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort) + : "medium"; + params.reasoning = { + effort: effort as NonNullable["effort"], + summary: options?.reasoningSummary || "auto", + }; + params.include = ["reasoning.encrypted_content"]; + } else if (model.thinkingLevelMap?.off !== null) { + params.reasoning = { + effort: (model.thinkingLevelMap?.off ?? "none") as NonNullable["effort"], + }; + } + } + + return params; +} 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/api/bedrock-converse-stream.ts b/packages/ai/src/api/bedrock-converse-stream.ts new file mode 100644 index 00000000..51d609fc --- /dev/null +++ b/packages/ai/src/api/bedrock-converse-stream.ts @@ -0,0 +1,1061 @@ +import type { Agent as HttpsAgent } from "node:https"; +import { + BedrockRuntimeClient, + type BedrockRuntimeClientConfig, + BedrockRuntimeServiceException, + StopReason as BedrockStopReason, + type Tool as BedrockTool, + CachePointType, + CacheTTL, + type ContentBlock, + type ContentBlockDeltaEvent, + type ContentBlockStartEvent, + type ContentBlockStopEvent, + ConversationRole, + ConverseStreamCommand, + type ConverseStreamMetadataEvent, + ImageFormat, + type Message, + type SystemContentBlock, + type ToolChoice, + type ToolConfiguration, + type ToolResultContentBlock, + ToolResultStatus, +} from "@aws-sdk/client-bedrock-runtime"; +import { NodeHttpHandler } from "@smithy/node-http-handler"; +import type { BuildMiddleware, DocumentType, MetadataBearer } from "@smithy/types"; +import { HttpProxyAgent } from "http-proxy-agent"; +import { HttpsProxyAgent } from "https-proxy-agent"; +import { calculateCost } from "../models.ts"; +import type { + Api, + AssistantMessage, + CacheRetention, + Context, + ImageContent, + Model, + ProviderEnv, + SimpleStreamOptions, + StopReason, + StreamFunction, + StreamOptions, + TextContent, + ThinkingBudgets, + ThinkingContent, + ThinkingLevel, + Tool, + ToolCall, + ToolResultMessage, +} from "../types.ts"; +import { AssistantMessageEventStream } from "../utils/event-stream.ts"; +import { parseStreamingJson } from "../utils/json-parse.ts"; +import { resolveHttpProxyUrlForTarget } from "../utils/node-http-proxy.ts"; +import { getProviderEnvValue } from "../utils/provider-env.ts"; +import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; +import { adjustMaxTokensForThinking, buildBaseOptions, clampReasoning } from "./simple-options.ts"; +import { transformMessages } from "./transform-messages.ts"; + +export type BedrockThinkingDisplay = "summarized" | "omitted"; + +export interface BedrockOptions extends StreamOptions { + region?: string; + profile?: string; + toolChoice?: "auto" | "any" | "none" | { type: "tool"; name: string }; + /* See https://docs.aws.amazon.com/bedrock/latest/userguide/inference-reasoning.html for supported models. */ + reasoning?: ThinkingLevel; + /* Custom token budgets per thinking level. Overrides default budgets. */ + thinkingBudgets?: ThinkingBudgets; + /* Only supported by Claude 4.x models, see https://docs.aws.amazon.com/bedrock/latest/userguide/claude-messages-extended-thinking.html#claude-messages-extended-thinking-tool-use-interleaved */ + interleavedThinking?: boolean; + /** + * Controls how Claude's thinking content is returned in responses. + * - "summarized": Thinking blocks contain summarized thinking text (default here). + * - "omitted": Thinking content is redacted but the signature still travels back + * for multi-turn continuity, reducing time-to-first-text-token. + * + * Note: Anthropic's API default for Claude Opus 4.8 and Mythos Preview is + * "omitted". We default to "summarized" here to keep behavior consistent with + * older Claude 4 models. Only applies to Claude models on Bedrock. + */ + thinkingDisplay?: BedrockThinkingDisplay; + /** Key-value pairs attached to the inference request for cost allocation tagging. + * Keys: max 64 chars, no `aws:` prefix. Values: max 256 chars. Max 50 pairs. + * Tags appear in AWS Cost Explorer split cost allocation data. + * @see https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ConverseStream.html */ + requestMetadata?: Record; + /** Bearer token for Bedrock API key authentication. + * When set, bypasses SigV4 signing and sends Authorization: Bearer instead. + * Requires `bedrock:CallWithBearerToken` IAM permission on the token's identity. + * Set via AWS_BEARER_TOKEN_BEDROCK env var or pass directly. + * @see https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonbedrock.html */ + bearerToken?: string; +} + +type Block = (TextContent | ThinkingContent | ToolCall) & { index?: number; partialJson?: string }; + +const EMPTY_TEXT_PLACEHOLDER = ""; + +export const stream: StreamFunction<"bedrock-converse-stream", BedrockOptions> = ( + model: Model<"bedrock-converse-stream">, + context: Context, + options: BedrockOptions = {}, +): AssistantMessageEventStream => { + const stream = new AssistantMessageEventStream(); + + (async () => { + const output: AssistantMessage = { + role: "assistant", + content: [], + api: "bedrock-converse-stream" as 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(), + }; + + const blocks = output.content as Block[]; + + const config: BedrockRuntimeClientConfig = { + profile: options.profile || getProviderEnvValue("AWS_PROFILE", options.env), + }; + const configuredRegion = getConfiguredBedrockRegion(options); + const hasAmbientConfiguredProfile = Boolean(getProviderEnvValue("AWS_PROFILE")); + const endpointRegion = getStandardBedrockEndpointRegion(model.baseUrl); + const useExplicitEndpoint = shouldUseExplicitBedrockEndpoint( + model.baseUrl, + configuredRegion, + hasAmbientConfiguredProfile, + ); + + // Only pin standard AWS Bedrock runtime endpoints when no region or ambient AWS_PROFILE is configured. + // This preserves custom endpoints (VPC/proxy) from #3402 without forcing built-in + // catalog defaults such as us-east-1 to override AWS_REGION/AWS_PROFILE. + if (useExplicitEndpoint) { + config.endpoint = model.baseUrl; + } + + // Resolve bearer token for Bedrock API key auth. + const skipAuth = getProviderEnvValue("AWS_BEDROCK_SKIP_AUTH", options.env) === "1"; + const bearerToken = + options.bearerToken || getProviderEnvValue("AWS_BEARER_TOKEN_BEDROCK", options.env) || undefined; + const useBearerToken = bearerToken !== undefined && !skipAuth; + + // in Node.js/Bun environment only + if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) { + // Region resolution: ARN-embedded > explicit option > env vars > SDK default chain. + // When the model ID is an inference profile ARN, extract the region from it. + // This avoids conflicts with AWS_REGION set for other services. + const arnRegionMatch = model.id.match(/^arn:aws(?:-[a-z0-9-]+)?:bedrock:([a-z0-9-]+):/); + if (arnRegionMatch) { + config.region = arnRegionMatch[1]; + } else if (configuredRegion) { + config.region = configuredRegion; + } else if (endpointRegion && useExplicitEndpoint) { + config.region = endpointRegion; + } else if (!hasAmbientConfiguredProfile) { + config.region = "us-east-1"; + } + + // Support proxies that don't need authentication + if (skipAuth) { + config.credentials = { + accessKeyId: "dummy-access-key", + secretAccessKey: "dummy-secret-key", + }; + } + + const credentials = getConfiguredBedrockCredentials(options.env); + if (!skipAuth && credentials) { + config.credentials = credentials; + } + + const proxyUrl = resolveHttpProxyUrlForTarget(model.baseUrl, options.env); + if (proxyUrl) { + // Bedrock runtime uses NodeHttp2Handler by default since v3.798.0, which is based + // on `http2` module and has no support for http agent. + // Use NodeHttpHandler to support HTTP(S) proxy agents. + config.requestHandler = new NodeHttpHandler({ + httpAgent: new HttpProxyAgent(proxyUrl), + httpsAgent: new HttpsProxyAgent(proxyUrl) as unknown as HttpsAgent, + }); + } else if (getProviderEnvValue("AWS_BEDROCK_FORCE_HTTP1", options.env) === "1") { + // Some custom endpoints require HTTP/1.1 instead of HTTP/2 + config.requestHandler = new NodeHttpHandler(); + } + } else { + // Non-Node environment (browser): fall back to us-east-1 since + // there's no config file resolution available. + config.region = + configuredRegion || (endpointRegion && useExplicitEndpoint ? endpointRegion : undefined) || "us-east-1"; + } + + if (useBearerToken) { + config.token = { token: bearerToken }; + config.authSchemePreference = ["httpBearerAuth"]; + } + + try { + const client = new BedrockRuntimeClient(config); + if (options.headers && Object.keys(options.headers).length > 0) { + addCustomHeadersMiddleware(client, options.headers); + } + const cacheRetention = resolveCacheRetention(options.cacheRetention, options.env); + const inferenceMaxTokens = options.maxTokens ?? (isAnthropicClaudeModel(model) ? model.maxTokens : undefined); + let commandInput = { + modelId: model.id, + messages: convertMessages(context, model, cacheRetention, options.env), + system: buildSystemPrompt(context.systemPrompt, model, cacheRetention, options.env), + inferenceConfig: { + ...(inferenceMaxTokens !== undefined && { maxTokens: inferenceMaxTokens }), + ...(options.temperature !== undefined && { temperature: options.temperature }), + }, + toolConfig: convertToolConfig(context.tools, options.toolChoice), + additionalModelRequestFields: buildAdditionalModelRequestFields(model, options), + ...(options.requestMetadata !== undefined && { requestMetadata: options.requestMetadata }), + }; + const nextCommandInput = await options?.onPayload?.(commandInput, model); + if (nextCommandInput !== undefined) { + commandInput = nextCommandInput as typeof commandInput; + } + const command = new ConverseStreamCommand(commandInput); + + const response = await client.send(command, { abortSignal: options.signal }); + if (response.$metadata.httpStatusCode !== undefined) { + const responseHeaders: Record = {}; + if (response.$metadata.requestId) { + responseHeaders["x-amzn-requestid"] = response.$metadata.requestId; + } + await options?.onResponse?.({ status: response.$metadata.httpStatusCode, headers: responseHeaders }, model); + } + + for await (const item of response.stream!) { + if (item.messageStart) { + if (item.messageStart.role !== ConversationRole.ASSISTANT) { + throw new Error("Unexpected assistant message start but got user message start instead"); + } + stream.push({ type: "start", partial: output }); + } else if (item.contentBlockStart) { + handleContentBlockStart(item.contentBlockStart, blocks, output, stream); + } else if (item.contentBlockDelta) { + handleContentBlockDelta(item.contentBlockDelta, blocks, output, stream); + } else if (item.contentBlockStop) { + handleContentBlockStop(item.contentBlockStop, blocks, output, stream); + } else if (item.messageStop) { + output.stopReason = mapStopReason(item.messageStop.stopReason); + } else if (item.metadata) { + handleMetadata(item.metadata, model, output); + } else if (item.internalServerException) { + throw item.internalServerException; + } else if (item.modelStreamErrorException) { + throw item.modelStreamErrorException; + } else if (item.validationException) { + throw item.validationException; + } else if (item.throttlingException) { + throw item.throttlingException; + } else if (item.serviceUnavailableException) { + throw item.serviceUnavailableException; + } + } + + if (options.signal?.aborted) { + throw new Error("Request was aborted"); + } + + if (output.stopReason === "error" || output.stopReason === "aborted") { + throw new Error("An unknown error occurred"); + } + + stream.push({ type: "done", reason: output.stopReason, message: output }); + stream.end(); + } catch (error) { + for (const block of output.content) { + delete (block as Block).index; + // partialJson is only a streaming scratch buffer; never persist it. + delete (block as Block).partialJson; + } + output.stopReason = options.signal?.aborted ? "aborted" : "error"; + output.errorMessage = formatBedrockError(error); + stream.push({ type: "error", reason: output.stopReason, error: output }); + stream.end(); + } + })(); + + return stream; +}; + +/** + * Human-readable prefixes for Bedrock SDK exception names. + * The downstream retry logic in agent-session matches patterns like + * `server.?error` and `service.?unavailable`, so we preserve the legacy + * prefix format rather than using the raw SDK exception name. + */ +const BEDROCK_ERROR_PREFIXES: Record = { + InternalServerException: "Internal server error", + ModelStreamErrorException: "Model stream error", + ValidationException: "Validation error", + ThrottlingException: "Throttling error", + ServiceUnavailableException: "Service unavailable", +}; + +/** + * Some models reject the account/profile's configured Bedrock data retention mode + * (e.g. "data retention mode 'default' is not available for this model"). Point + * users at the AWS docs explaining how to configure a supported mode. + */ +const BEDROCK_DATA_RETENTION_DOCS_URL = "https://docs.aws.amazon.com/bedrock/latest/userguide/data-retention.html"; + +/** + * Format a Bedrock error with a human-readable prefix. + * AWS SDK exceptions (both from `client.send()` and from stream event items) + * extend BedrockRuntimeServiceException. We map the `.name` to a stable + * human-readable prefix so downstream consumers (retry logic, context-overflow + * detection) can distinguish error categories via simple string matching. + */ +function formatBedrockError(error: unknown): string { + const message = error instanceof Error ? error.message : JSON.stringify(error); + const dataRetentionHint = /data retention mode/i.test(message) + ? ` See ${BEDROCK_DATA_RETENTION_DOCS_URL} for supported data retention modes.` + : ""; + if (error instanceof BedrockRuntimeServiceException) { + const prefix = BEDROCK_ERROR_PREFIXES[error.name] ?? error.name; + return `${prefix}: ${message}${dataRetentionHint}`; + } + return `${message}${dataRetentionHint}`; +} + +/** + * Header keys that must never be overwritten by caller-supplied headers. + * `host` and `x-amz-*` participate in the SigV4 canonical request; `authorization` + * is owned by SigV4 or the bearer-token path (config.token + authSchemePreference). + * Compared case-insensitively (caller key is lower-cased before lookup). + */ +const RESERVED_HEADER_EXACT = new Set(["authorization", "host"]); + +function isReservedHeader(key: string): boolean { + const lower = key.toLowerCase(); + return lower.startsWith("x-amz-") || RESERVED_HEADER_EXACT.has(lower); +} + +/** + * Attach caller-supplied headers to the outgoing Bedrock request via a Smithy + * `build`-step middleware. The `build` step runs after request serialisation but + * before SigV4 signing, so injected headers are covered by the signature. Reserved + * SigV4 / auth headers (`x-amz-*`, `authorization`, `host`) are silently skipped; + * all other caller headers override any existing same-named header on the request. + */ +function addCustomHeadersMiddleware(client: BedrockRuntimeClient, headers: Record): void { + const middleware: BuildMiddleware = (next) => async (args) => { + const request = args.request; + if (request && typeof request === "object" && "headers" in request) { + const requestHeaders = (request as { headers: Record }).headers; + for (const [key, value] of Object.entries(headers)) { + if (!isReservedHeader(key)) { + requestHeaders[key] = value; + } + } + } + return next(args); + }; + client.middlewareStack.add(middleware, { step: "build", name: "pi-ai-custom-headers", priority: "low" }); +} + +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 stream(model, context, { ...base, reasoning: undefined } satisfies BedrockOptions); + } + + if (isAnthropicClaudeModel(model)) { + if (supportsAdaptiveThinking(model.id, model.name)) { + return stream(model, context, { + ...base, + reasoning: options.reasoning, + thinkingBudgets: options.thinkingBudgets, + } satisfies BedrockOptions); + } + + // Undefined means the caller did not request an output cap; let the helper use the model cap. + // Do not coerce to 0 here, or the thinking budget would become the entire maxTokens value. + const adjusted = adjustMaxTokensForThinking( + base.maxTokens, + model.maxTokens, + options.reasoning, + options.thinkingBudgets, + ); + + return stream(model, context, { + ...base, + maxTokens: adjusted.maxTokens, + reasoning: options.reasoning, + thinkingBudgets: { + ...(options.thinkingBudgets || {}), + [clampReasoning(options.reasoning)!]: adjusted.thinkingBudget, + }, + } satisfies BedrockOptions); + } + + return stream(model, context, { + ...base, + reasoning: options.reasoning, + thinkingBudgets: options.thinkingBudgets, + } satisfies BedrockOptions); +}; + +function handleContentBlockStart( + event: ContentBlockStartEvent, + blocks: Block[], + output: AssistantMessage, + stream: AssistantMessageEventStream, +): void { + const index = event.contentBlockIndex!; + const start = event.start; + + if (start?.toolUse) { + const block: Block = { + type: "toolCall", + id: start.toolUse.toolUseId || "", + name: start.toolUse.name || "", + arguments: {}, + partialJson: "", + index, + }; + output.content.push(block); + stream.push({ type: "toolcall_start", contentIndex: blocks.length - 1, partial: output }); + } +} + +function handleContentBlockDelta( + event: ContentBlockDeltaEvent, + blocks: Block[], + output: AssistantMessage, + stream: AssistantMessageEventStream, +): void { + const contentBlockIndex = event.contentBlockIndex!; + const delta = event.delta; + let index = blocks.findIndex((b) => b.index === contentBlockIndex); + let block = blocks[index]; + + if (delta?.text !== undefined) { + // If no text block exists yet, create one, as `handleContentBlockStart` is not sent for text blocks + if (!block) { + const newBlock: Block = { type: "text", text: "", index: contentBlockIndex }; + output.content.push(newBlock); + index = blocks.length - 1; + block = blocks[index]; + stream.push({ type: "text_start", contentIndex: index, partial: output }); + } + if (block.type === "text") { + block.text += delta.text; + stream.push({ type: "text_delta", contentIndex: index, delta: delta.text, partial: output }); + } + } else if (delta?.toolUse && block?.type === "toolCall") { + block.partialJson = (block.partialJson || "") + (delta.toolUse.input || ""); + block.arguments = parseStreamingJson(block.partialJson); + stream.push({ type: "toolcall_delta", contentIndex: index, delta: delta.toolUse.input || "", partial: output }); + } else if (delta?.reasoningContent) { + let thinkingBlock = block; + let thinkingIndex = index; + + if (!thinkingBlock) { + const newBlock: Block = { type: "thinking", thinking: "", thinkingSignature: "", index: contentBlockIndex }; + output.content.push(newBlock); + thinkingIndex = blocks.length - 1; + thinkingBlock = blocks[thinkingIndex]; + stream.push({ type: "thinking_start", contentIndex: thinkingIndex, partial: output }); + } + + if (thinkingBlock?.type === "thinking") { + if (delta.reasoningContent.text) { + thinkingBlock.thinking += delta.reasoningContent.text; + stream.push({ + type: "thinking_delta", + contentIndex: thinkingIndex, + delta: delta.reasoningContent.text, + partial: output, + }); + } + if (delta.reasoningContent.signature) { + thinkingBlock.thinkingSignature = + (thinkingBlock.thinkingSignature || "") + delta.reasoningContent.signature; + } + } + } +} + +function handleMetadata( + event: ConverseStreamMetadataEvent, + model: Model<"bedrock-converse-stream">, + output: AssistantMessage, +): void { + if (event.usage) { + output.usage.input = event.usage.inputTokens || 0; + output.usage.output = event.usage.outputTokens || 0; + output.usage.cacheRead = event.usage.cacheReadInputTokens || 0; + output.usage.cacheWrite = event.usage.cacheWriteInputTokens || 0; + output.usage.totalTokens = event.usage.totalTokens || output.usage.input + output.usage.output; + calculateCost(model, output.usage); + } +} + +function handleContentBlockStop( + event: ContentBlockStopEvent, + blocks: Block[], + output: AssistantMessage, + stream: AssistantMessageEventStream, +): void { + const index = blocks.findIndex((b) => b.index === event.contentBlockIndex); + const block = blocks[index]; + if (!block) return; + delete (block as Block).index; + + switch (block.type) { + case "text": + stream.push({ type: "text_end", contentIndex: index, content: block.text, partial: output }); + break; + case "thinking": + stream.push({ type: "thinking_end", contentIndex: index, content: block.thinking, partial: output }); + break; + case "toolCall": + block.arguments = parseStreamingJson(block.partialJson); + // Finalize in-place and strip the scratch buffer so replay only + // carries parsed arguments. + delete (block as Block).partialJson; + stream.push({ type: "toolcall_end", contentIndex: index, toolCall: block, partial: output }); + break; + } +} + +/** + * Check if the model supports adaptive thinking (Opus 4.6+, Sonnet 4.6). + * Checks both model ID and model name to support application inference profiles + * whose ARNs don't contain the model name. + */ +function getModelMatchCandidates(modelId: string, modelName?: string): string[] { + const values = modelName ? [modelId, modelName] : [modelId]; + return values.flatMap((value) => { + const lower = value.toLowerCase(); + return [lower, lower.replace(/[\s_.:]+/g, "-")]; + }); +} + +function supportsAdaptiveThinking(modelId: string, modelName?: string): boolean { + const candidates = getModelMatchCandidates(modelId, modelName); + return candidates.some( + (s) => + s.includes("opus-4-6") || + s.includes("opus-4-7") || + s.includes("opus-4-8") || + s.includes("sonnet-4-6") || + s.includes("fable-5"), + ); +} + +function supportsNativeXhighEffort(model: Model<"bedrock-converse-stream">): boolean { + const candidates = getModelMatchCandidates(model.id, model.name); + return candidates.some((s) => s.includes("opus-4-7") || s.includes("opus-4-8") || s.includes("fable-5")); +} + +function mapThinkingLevelToEffort( + model: Model<"bedrock-converse-stream">, + level: SimpleStreamOptions["reasoning"], +): "low" | "medium" | "high" | "xhigh" | "max" { + if (level === "xhigh" && supportsNativeXhighEffort(model)) return "xhigh"; + + const mapped = level ? model.thinkingLevelMap?.[level] : undefined; + if (typeof mapped === "string") return mapped as "low" | "medium" | "high" | "xhigh" | "max"; + + switch (level) { + case "minimal": + case "low": + return "low"; + case "medium": + return "medium"; + case "high": + return "high"; + default: + return "high"; + } +} + +/** + * Resolve cache retention preference. + * Defaults to "short" and uses PI_CACHE_RETENTION for backward compatibility. + */ +function resolveCacheRetention(cacheRetention?: CacheRetention, env?: ProviderEnv): CacheRetention { + if (cacheRetention) { + return cacheRetention; + } + if (getProviderEnvValue("PI_CACHE_RETENTION", env) === "long") { + return "long"; + } + return "short"; +} + +/** + * Check if the model is an Anthropic Claude model on Bedrock. + * Checks both model ID and model name to support application inference profiles + * whose ARNs don't contain the model name. + */ +function isAnthropicClaudeModel(model: Model<"bedrock-converse-stream">): boolean { + const id = model.id.toLowerCase(); + const name = model.name?.toLowerCase() ?? ""; + return ( + id.includes("anthropic.claude") || + id.includes("anthropic/claude") || + name.includes("anthropic.claude") || + name.includes("anthropic/claude") || + name.includes("claude") + ); +} + +/** + * Check if the model supports prompt caching. + * Supported: Claude 3.5 Haiku, Claude 3.7 Sonnet, Claude 4.x models + * + * For base models and system-defined inference profiles the model ID / ARN + * contains the model name, so we can decide locally. + * + * For application inference profiles (whose ARNs don't contain the model name), + * also checks model.name which is user-controlled via models.json or registerProvider. + * As a last resort, set AWS_BEDROCK_FORCE_CACHE=1 to enable cache points. + * Amazon Nova models have automatic caching and don't need explicit cache points. + */ +function supportsPromptCaching(model: Model<"bedrock-converse-stream">, env?: ProviderEnv): boolean { + const candidates = getModelMatchCandidates(model.id, model.name); + + const hasClaudeRef = candidates.some((s) => s.includes("claude")); + if (!hasClaudeRef) { + // Application inference profiles don't contain the model name in the ARN. + // Allow users to force cache points via environment variable. + if (getProviderEnvValue("AWS_BEDROCK_FORCE_CACHE", env) === "1") return true; + return false; + } + // Claude 4.x models (opus-4, sonnet-4, haiku-4) + if (candidates.some((s) => s.includes("-4-"))) return true; + // Claude 3.7 Sonnet + if (candidates.some((s) => s.includes("claude-3-7-sonnet"))) return true; + // Claude 3.5 Haiku + if (candidates.some((s) => s.includes("claude-3-5-haiku"))) return true; + return false; +} + +/** + * Check if the model supports thinking signatures in reasoningContent. + * Only Anthropic Claude models support the signature field. + * Other models (OpenAI, Qwen, Minimax, Moonshot, etc.) reject it with: + * "This model doesn't support the reasoningContent.reasoningText.signature field" + * + * Checks both model ID and model name to support application inference profiles. + */ +function supportsThinkingSignature(model: Model<"bedrock-converse-stream">): boolean { + return isAnthropicClaudeModel(model); +} + +function buildSystemPrompt( + systemPrompt: string | undefined, + model: Model<"bedrock-converse-stream">, + cacheRetention: CacheRetention, + env?: ProviderEnv, +): SystemContentBlock[] | undefined { + if (!systemPrompt) return undefined; + + const blocks: SystemContentBlock[] = [{ text: sanitizeSurrogates(systemPrompt) }]; + + // Add cache point for supported Claude models when caching is enabled + if (cacheRetention !== "none" && supportsPromptCaching(model, env)) { + blocks.push({ + cachePoint: { type: CachePointType.DEFAULT, ...(cacheRetention === "long" ? { ttl: CacheTTL.ONE_HOUR } : {}) }, + }); + } + + return blocks; +} + +function normalizeToolCallId(id: string): string { + const sanitized = id.replace(/[^a-zA-Z0-9_-]/g, "_"); + return sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized; +} + +function createNonBlankTextBlock(text: string): ContentBlock.TextMember | undefined { + const sanitized = sanitizeSurrogates(text); + return sanitized.trim().length === 0 ? undefined : { text: sanitized }; +} + +function createRequiredTextBlock(text: string): ContentBlock.TextMember { + return createNonBlankTextBlock(text) ?? { text: EMPTY_TEXT_PLACEHOLDER }; +} + +function convertToolResultContent(content: (TextContent | ImageContent)[]): ToolResultContentBlock[] { + const result: ToolResultContentBlock[] = []; + for (const c of content) { + if (c.type === "image") { + result.push({ image: createImageBlock(c.mimeType, c.data) }); + } else { + const textBlock = createNonBlankTextBlock(c.text); + if (textBlock) result.push(textBlock); + } + } + if (result.length === 0) result.push({ text: EMPTY_TEXT_PLACEHOLDER }); + return result; +} + +function convertMessages( + context: Context, + model: Model<"bedrock-converse-stream">, + cacheRetention: CacheRetention, + env?: ProviderEnv, +): Message[] { + const result: Message[] = []; + const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId); + + for (let i = 0; i < transformedMessages.length; i++) { + const m = transformedMessages[i]; + + switch (m.role) { + case "user": { + const content: ContentBlock[] = []; + if (typeof m.content === "string") { + content.push(createRequiredTextBlock(m.content)); + } else { + for (const c of m.content) { + switch (c.type) { + case "text": { + const textBlock = createNonBlankTextBlock(c.text); + if (textBlock) content.push(textBlock); + break; + } + case "image": + content.push({ image: createImageBlock(c.mimeType, c.data) }); + break; + default: + continue; + } + } + if (content.length === 0) content.push({ text: EMPTY_TEXT_PLACEHOLDER }); + } + result.push({ + role: ConversationRole.USER, + content, + }); + break; + } + case "assistant": { + // Skip assistant messages with empty content (e.g., from aborted requests) + // Bedrock rejects messages with empty content arrays + if (m.content.length === 0) { + continue; + } + const contentBlocks: ContentBlock[] = []; + for (const c of m.content) { + switch (c.type) { + case "text": { + // Skip empty text blocks + const textBlock = createNonBlankTextBlock(c.text); + if (!textBlock) continue; + contentBlocks.push(textBlock); + break; + } + case "toolCall": + contentBlocks.push({ + toolUse: { toolUseId: c.id, name: c.name, input: c.arguments }, + }); + break; + case "thinking": { + // Skip empty thinking blocks + const thinking = sanitizeSurrogates(c.thinking); + if (thinking.trim().length === 0) continue; + // Only Anthropic models support the signature field in reasoningText. + // For other models, we omit the signature to avoid errors like: + // "This model doesn't support the reasoningContent.reasoningText.signature field" + if (supportsThinkingSignature(model)) { + // Signatures arrive after thinking deltas. If a partial or externally + // persisted message lacks a signature, Bedrock rejects the replayed + // reasoning block. Fall back to plain text, matching Anthropic. + if (!c.thinkingSignature || c.thinkingSignature.trim().length === 0) { + contentBlocks.push({ text: thinking }); + } else { + contentBlocks.push({ + reasoningContent: { + reasoningText: { + text: thinking, + signature: c.thinkingSignature, + }, + }, + }); + } + } else { + contentBlocks.push({ + reasoningContent: { + reasoningText: { text: thinking }, + }, + }); + } + break; + } + default: + continue; + } + } + // Skip if all content blocks were filtered out + if (contentBlocks.length === 0) { + continue; + } + result.push({ + role: ConversationRole.ASSISTANT, + content: contentBlocks, + }); + break; + } + case "toolResult": { + // Collect all consecutive toolResult messages into a single user message + // Bedrock requires all tool results to be in one message + const toolResults: ContentBlock.ToolResultMember[] = []; + + // Add current tool result with all content blocks combined + toolResults.push({ + toolResult: { + toolUseId: m.toolCallId, + content: convertToolResultContent(m.content), + status: m.isError ? ToolResultStatus.ERROR : ToolResultStatus.SUCCESS, + }, + }); + + // Look ahead for consecutive toolResult messages + let j = i + 1; + while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") { + const nextMsg = transformedMessages[j] as ToolResultMessage; + toolResults.push({ + toolResult: { + toolUseId: nextMsg.toolCallId, + content: convertToolResultContent(nextMsg.content), + status: nextMsg.isError ? ToolResultStatus.ERROR : ToolResultStatus.SUCCESS, + }, + }); + j++; + } + + // Skip the messages we've already processed + i = j - 1; + + result.push({ + role: ConversationRole.USER, + content: toolResults, + }); + break; + } + default: + continue; + } + } + + // Add cache point to the last user message for supported Claude models when caching is enabled + if (cacheRetention !== "none" && supportsPromptCaching(model, env) && result.length > 0) { + const lastMessage = result[result.length - 1]; + if (lastMessage.role === ConversationRole.USER && lastMessage.content) { + (lastMessage.content as ContentBlock[]).push({ + cachePoint: { + type: CachePointType.DEFAULT, + ...(cacheRetention === "long" ? { ttl: CacheTTL.ONE_HOUR } : {}), + }, + }); + } + } + + return result; +} + +function convertToolConfig( + tools: Tool[] | undefined, + toolChoice: BedrockOptions["toolChoice"], +): ToolConfiguration | undefined { + if (!tools?.length || toolChoice === "none") return undefined; + + const bedrockTools: BedrockTool[] = tools.map((tool) => ({ + toolSpec: { + name: tool.name, + description: tool.description, + inputSchema: { json: tool.parameters as unknown as DocumentType }, + }, + })); + + let bedrockToolChoice: ToolChoice | undefined; + switch (toolChoice) { + case "auto": + bedrockToolChoice = { auto: {} }; + break; + case "any": + bedrockToolChoice = { any: {} }; + break; + default: + if (toolChoice?.type === "tool") { + bedrockToolChoice = { tool: { name: toolChoice.name } }; + } + } + + return { tools: bedrockTools, toolChoice: bedrockToolChoice }; +} + +function mapStopReason(reason: string | undefined): StopReason { + switch (reason) { + case BedrockStopReason.END_TURN: + case BedrockStopReason.STOP_SEQUENCE: + return "stop"; + case BedrockStopReason.MAX_TOKENS: + case BedrockStopReason.MODEL_CONTEXT_WINDOW_EXCEEDED: + return "length"; + case BedrockStopReason.TOOL_USE: + return "toolUse"; + default: + return "error"; + } +} + +function getConfiguredBedrockRegion(options: BedrockOptions): string | undefined { + return ( + options.region || + getProviderEnvValue("AWS_REGION", options.env) || + getProviderEnvValue("AWS_DEFAULT_REGION", options.env) || + undefined + ); +} + +function getConfiguredBedrockCredentials(env?: ProviderEnv): BedrockRuntimeClientConfig["credentials"] | undefined { + const accessKeyId = getProviderEnvValue("AWS_ACCESS_KEY_ID", env); + const secretAccessKey = getProviderEnvValue("AWS_SECRET_ACCESS_KEY", env); + if (!accessKeyId || !secretAccessKey) { + return undefined; + } + const sessionToken = getProviderEnvValue("AWS_SESSION_TOKEN", env); + return { + accessKeyId, + secretAccessKey, + ...(sessionToken ? { sessionToken } : {}), + }; +} + +function getStandardBedrockEndpointRegion(baseUrl: string | undefined): string | undefined { + if (!baseUrl) { + return undefined; + } + + try { + const { hostname } = new URL(baseUrl); + const match = hostname.toLowerCase().match(/^bedrock-runtime(?:-fips)?\.([a-z0-9-]+)\.amazonaws\.com(?:\.cn)?$/); + return match?.[1]; + } catch { + return undefined; + } +} + +function shouldUseExplicitBedrockEndpoint( + baseUrl: string, + configuredRegion: string | undefined, + hasAmbientConfiguredProfile: boolean, +): boolean { + const endpointRegion = getStandardBedrockEndpointRegion(baseUrl); + if (!endpointRegion) { + return true; + } + + return !configuredRegion && !hasAmbientConfiguredProfile; +} + +function isGovCloudBedrockTarget(model: Model<"bedrock-converse-stream">, options: BedrockOptions): boolean { + const region = getConfiguredBedrockRegion(options); + if (region?.toLowerCase().startsWith("us-gov-")) { + return true; + } + + const modelId = model.id.toLowerCase(); + return modelId.startsWith("us-gov.") || modelId.startsWith("arn:aws-us-gov:"); +} + +function buildAdditionalModelRequestFields( + model: Model<"bedrock-converse-stream">, + options: BedrockOptions, +): Record | undefined { + if (!options.reasoning || !model.reasoning) { + return undefined; + } + + if (isAnthropicClaudeModel(model)) { + // GovCloud Bedrock currently rejects the Claude thinking.display field. + // Omit it there until the GovCloud Converse schema catches up. + const display = isGovCloudBedrockTarget(model, options) ? undefined : (options.thinkingDisplay ?? "summarized"); + const result: Record = supportsAdaptiveThinking(model.id, model.name) + ? { + thinking: { type: "adaptive", ...(display !== undefined ? { display } : {}) }, + output_config: { effort: mapThinkingLevelToEffort(model, options.reasoning) }, + } + : (() => { + const defaultBudgets: Record = { + minimal: 1024, + low: 2048, + medium: 8192, + high: 16384, + xhigh: 16384, // Claude doesn't support xhigh, clamp to high + }; + + // Custom budgets override defaults (xhigh not in ThinkingBudgets, use high) + const level = options.reasoning === "xhigh" ? "high" : options.reasoning; + const budget = options.thinkingBudgets?.[level] ?? defaultBudgets[options.reasoning]; + + return { + thinking: { + type: "enabled", + budget_tokens: budget, + ...(display !== undefined ? { display } : {}), + }, + }; + })(); + + if (!supportsAdaptiveThinking(model.id, model.name) && (options.interleavedThinking ?? true)) { + result.anthropic_beta = ["interleaved-thinking-2025-05-14"]; + } + + return result; + } + + return undefined; +} + +function createImageBlock(mimeType: string, data: string) { + let format: ImageFormat; + switch (mimeType) { + case "image/jpeg": + case "image/jpg": + format = ImageFormat.JPEG; + break; + case "image/png": + format = ImageFormat.PNG; + break; + case "image/gif": + format = ImageFormat.GIF; + break; + case "image/webp": + format = ImageFormat.WEBP; + break; + default: + throw new Error(`Unknown image type: ${mimeType}`); + } + + const binaryString = atob(data); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + + return { source: { bytes }, format }; +} 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/api/google-generative-ai.ts b/packages/ai/src/api/google-generative-ai.ts new file mode 100644 index 00000000..d28f97c4 --- /dev/null +++ b/packages/ai/src/api/google-generative-ai.ts @@ -0,0 +1,504 @@ +import { + type GenerateContentConfig, + type GenerateContentParameters, + GoogleGenAI, + type ThinkingConfig, +} from "@google/genai"; +import { calculateCost, clampThinkingLevel } from "../models.ts"; +import type { + Api, + AssistantMessage, + Context, + Model, + SimpleStreamOptions, + StreamFunction, + StreamOptions, + TextContent, + ThinkingBudgets, + ThinkingContent, + ThinkingLevel, + ToolCall, +} from "../types.ts"; +import { AssistantMessageEventStream } from "../utils/event-stream.ts"; +import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; +import type { GoogleThinkingLevel } from "./google-shared.ts"; +import { + convertMessages, + convertTools, + isThinkingPart, + mapStopReason, + mapToolChoice, + retainThoughtSignature, +} from "./google-shared.ts"; +import { buildBaseOptions } from "./simple-options.ts"; + +export interface GoogleOptions extends StreamOptions { + toolChoice?: "auto" | "none" | "any"; + thinking?: { + enabled: boolean; + budgetTokens?: number; // -1 for dynamic, 0 to disable + level?: GoogleThinkingLevel; + }; +} + +// Counter for generating unique tool call IDs +let toolCallCounter = 0; + +export const stream: StreamFunction<"google-generative-ai", GoogleOptions> = ( + model: Model<"google-generative-ai">, + context: Context, + options?: GoogleOptions, +): AssistantMessageEventStream => { + const stream = new AssistantMessageEventStream(); + + (async () => { + const output: AssistantMessage = { + role: "assistant", + content: [], + api: "google-generative-ai" as 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(), + }; + + try { + const apiKey = options?.apiKey; + if (!apiKey) { + throw new Error(`No API key for provider: ${model.provider}`); + } + const client = createClient(model, apiKey, options?.headers); + let params = buildParams(model, context, options); + const nextParams = await options?.onPayload?.(params, model); + if (nextParams !== undefined) { + params = nextParams as GenerateContentParameters; + } + const googleStream = await client.models.generateContentStream(params); + + stream.push({ type: "start", partial: output }); + let currentBlock: TextContent | ThinkingContent | null = null; + const blocks = output.content; + const blockIndex = () => blocks.length - 1; + for await (const chunk of googleStream) { + // @google/genai documents GenerateContentResponse.responseId as an output-only field + // used to identify each response. Keep the first non-empty one from the stream. + output.responseId ||= chunk.responseId; + const candidate = chunk.candidates?.[0]; + if (candidate?.content?.parts) { + for (const part of candidate.content.parts) { + if (part.text !== undefined) { + const isThinking = isThinkingPart(part); + if ( + !currentBlock || + (isThinking && currentBlock.type !== "thinking") || + (!isThinking && currentBlock.type !== "text") + ) { + if (currentBlock) { + if (currentBlock.type === "text") { + stream.push({ + type: "text_end", + contentIndex: blocks.length - 1, + content: currentBlock.text, + partial: output, + }); + } else { + stream.push({ + type: "thinking_end", + contentIndex: blockIndex(), + content: currentBlock.thinking, + partial: output, + }); + } + } + if (isThinking) { + currentBlock = { type: "thinking", thinking: "", thinkingSignature: undefined }; + output.content.push(currentBlock); + stream.push({ type: "thinking_start", contentIndex: blockIndex(), partial: output }); + } else { + currentBlock = { type: "text", text: "" }; + output.content.push(currentBlock); + stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output }); + } + } + if (currentBlock.type === "thinking") { + currentBlock.thinking += part.text; + currentBlock.thinkingSignature = retainThoughtSignature( + currentBlock.thinkingSignature, + part.thoughtSignature, + ); + stream.push({ + type: "thinking_delta", + contentIndex: blockIndex(), + delta: part.text, + partial: output, + }); + } else { + currentBlock.text += part.text; + currentBlock.textSignature = retainThoughtSignature( + currentBlock.textSignature, + part.thoughtSignature, + ); + stream.push({ + type: "text_delta", + contentIndex: blockIndex(), + delta: part.text, + partial: output, + }); + } + } + + if (part.functionCall) { + if (currentBlock) { + if (currentBlock.type === "text") { + stream.push({ + type: "text_end", + contentIndex: blockIndex(), + content: currentBlock.text, + partial: output, + }); + } else { + stream.push({ + type: "thinking_end", + contentIndex: blockIndex(), + content: currentBlock.thinking, + partial: output, + }); + } + currentBlock = null; + } + + // Generate unique ID if not provided or if it's a duplicate + const providedId = part.functionCall.id; + const needsNewId = + !providedId || output.content.some((b) => b.type === "toolCall" && b.id === providedId); + const toolCallId = needsNewId + ? `${part.functionCall.name}_${Date.now()}_${++toolCallCounter}` + : providedId; + + const toolCall: ToolCall = { + type: "toolCall", + id: toolCallId, + name: part.functionCall.name || "", + arguments: (part.functionCall.args as Record) ?? {}, + ...(part.thoughtSignature && { thoughtSignature: part.thoughtSignature }), + }; + + output.content.push(toolCall); + stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output }); + stream.push({ + type: "toolcall_delta", + contentIndex: blockIndex(), + delta: JSON.stringify(toolCall.arguments), + partial: output, + }); + stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output }); + } + } + } + + if (candidate?.finishReason) { + output.stopReason = mapStopReason(candidate.finishReason); + if (output.content.some((b) => b.type === "toolCall")) { + output.stopReason = "toolUse"; + } + } + + if (chunk.usageMetadata) { + output.usage = { + input: + (chunk.usageMetadata.promptTokenCount || 0) - (chunk.usageMetadata.cachedContentTokenCount || 0), + output: + (chunk.usageMetadata.candidatesTokenCount || 0) + (chunk.usageMetadata.thoughtsTokenCount || 0), + cacheRead: chunk.usageMetadata.cachedContentTokenCount || 0, + cacheWrite: 0, + totalTokens: chunk.usageMetadata.totalTokenCount || 0, + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + total: 0, + }, + }; + calculateCost(model, output.usage); + } + } + + if (currentBlock) { + if (currentBlock.type === "text") { + stream.push({ + type: "text_end", + contentIndex: blockIndex(), + content: currentBlock.text, + partial: output, + }); + } else { + stream.push({ + type: "thinking_end", + contentIndex: blockIndex(), + content: currentBlock.thinking, + partial: output, + }); + } + } + + if (options?.signal?.aborted) { + throw new Error("Request was aborted"); + } + + if (output.stopReason === "aborted" || output.stopReason === "error") { + throw new Error("An unknown error occurred"); + } + + stream.push({ type: "done", reason: output.stopReason, message: output }); + stream.end(); + } catch (error) { + // Remove internal index property used during streaming + for (const block of output.content) { + if ("index" in block) { + delete (block as { index?: number }).index; + } + } + output.stopReason = options?.signal?.aborted ? "aborted" : "error"; + output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error); + stream.push({ type: "error", reason: output.stopReason, error: output }); + stream.end(); + } + })(); + + return stream; +}; + +export const streamSimple: StreamFunction<"google-generative-ai", SimpleStreamOptions> = ( + model: Model<"google-generative-ai">, + context: Context, + options?: SimpleStreamOptions, +): AssistantMessageEventStream => { + const apiKey = options?.apiKey; + if (!apiKey) { + throw new Error(`No API key for provider: ${model.provider}`); + } + + const base = buildBaseOptions(model, options, apiKey); + if (!options?.reasoning) { + return stream(model, context, { ...base, thinking: { enabled: false } } satisfies GoogleOptions); + } + + const clampedReasoning = clampThinkingLevel(model, options.reasoning); + const effort = (clampedReasoning === "off" ? "high" : clampedReasoning) as ClampedThinkingLevel; + const googleModel = model as Model<"google-generative-ai">; + + if (isGemini3ProModel(googleModel) || isGemini3FlashModel(googleModel) || isGemma4Model(googleModel)) { + return stream(model, context, { + ...base, + thinking: { + enabled: true, + level: getThinkingLevel(effort, googleModel), + }, + } satisfies GoogleOptions); + } + + return stream(model, context, { + ...base, + thinking: { + enabled: true, + budgetTokens: getGoogleBudget(googleModel, effort, options.thinkingBudgets), + }, + } satisfies GoogleOptions); +}; + +function createClient( + model: Model<"google-generative-ai">, + apiKey?: string, + optionsHeaders?: Record, +): 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 }; + } + + return new GoogleGenAI({ + apiKey, + httpOptions: Object.keys(httpOptions).length > 0 ? httpOptions : undefined, + }); +} + +function buildParams( + model: Model<"google-generative-ai">, + context: Context, + options: GoogleOptions = {}, +): GenerateContentParameters { + const contents = convertMessages(model, context); + + const generationConfig: GenerateContentConfig = {}; + if (options.temperature !== undefined) { + generationConfig.temperature = options.temperature; + } + if (options.maxTokens !== undefined) { + generationConfig.maxOutputTokens = options.maxTokens; + } + + const config: GenerateContentConfig = { + ...(Object.keys(generationConfig).length > 0 && generationConfig), + ...(context.systemPrompt && { systemInstruction: sanitizeSurrogates(context.systemPrompt) }), + ...(context.tools && context.tools.length > 0 && { tools: convertTools(context.tools) }), + }; + + if (context.tools && context.tools.length > 0 && options.toolChoice) { + config.toolConfig = { + functionCallingConfig: { + mode: mapToolChoice(options.toolChoice), + }, + }; + } else { + config.toolConfig = undefined; + } + + if (options.thinking?.enabled && model.reasoning) { + const thinkingConfig: ThinkingConfig = { includeThoughts: true }; + if (options.thinking.level !== undefined) { + // Cast to any since our GoogleThinkingLevel mirrors Google's ThinkingLevel enum values + thinkingConfig.thinkingLevel = options.thinking.level as any; + } else if (options.thinking.budgetTokens !== undefined) { + thinkingConfig.thinkingBudget = options.thinking.budgetTokens; + } + config.thinkingConfig = thinkingConfig; + } else if (model.reasoning && options.thinking && !options.thinking.enabled) { + config.thinkingConfig = getDisabledThinkingConfig(model); + } + + if (options.signal) { + if (options.signal.aborted) { + throw new Error("Request aborted"); + } + config.abortSignal = options.signal; + } + + const params: GenerateContentParameters = { + model: model.id, + contents, + config, + }; + + return params; +} + +type ClampedThinkingLevel = Exclude; + +function isGemma4Model(model: Model<"google-generative-ai">): boolean { + return /gemma-?4/.test(model.id.toLowerCase()); +} + +function isGemini3ProModel(model: Model<"google-generative-ai">): boolean { + return /gemini-3(?:\.\d+)?-pro/.test(model.id.toLowerCase()); +} + +function isGemini3FlashModel(model: Model<"google-generative-ai">): boolean { + const id = model.id.toLowerCase(); + return /gemini-3(?:\.\d+)?-flash/.test(id) || id === "gemini-flash-latest" || id === "gemini-flash-lite-latest"; +} + +function getDisabledThinkingConfig(model: Model<"google-generative-ai">): ThinkingConfig { + // Google docs: Gemini 3.1 Pro cannot disable thinking, and Gemini 3 Flash / Flash-Lite + // do not support full thinking-off either. For Gemini 3 models, use the lowest supported + // thinkingLevel without includeThoughts so hidden thinking remains invisible to pi. + if (isGemini3ProModel(model)) { + return { thinkingLevel: "LOW" as any }; + } + if (isGemini3FlashModel(model)) { + return { thinkingLevel: "MINIMAL" as any }; + } + if (isGemma4Model(model)) { + return { thinkingLevel: "MINIMAL" as any }; + } + + // Gemini 2.x supports disabling via thinkingBudget = 0. + return { thinkingBudget: 0 }; +} + +function getThinkingLevel(effort: ClampedThinkingLevel, model: Model<"google-generative-ai">): GoogleThinkingLevel { + if (isGemini3ProModel(model)) { + switch (effort) { + case "minimal": + case "low": + return "LOW"; + case "medium": + case "high": + return "HIGH"; + } + } + if (isGemma4Model(model)) { + switch (effort) { + case "minimal": + case "low": + return "MINIMAL"; + case "medium": + case "high": + return "HIGH"; + } + } + switch (effort) { + case "minimal": + return "MINIMAL"; + case "low": + return "LOW"; + case "medium": + return "MEDIUM"; + case "high": + return "HIGH"; + } +} + +function getGoogleBudget( + model: Model<"google-generative-ai">, + effort: ClampedThinkingLevel, + customBudgets?: ThinkingBudgets, +): number { + if (customBudgets?.[effort] !== undefined) { + return customBudgets[effort]!; + } + + if (model.id.includes("2.5-pro")) { + const budgets: Record = { + minimal: 128, + low: 2048, + medium: 8192, + high: 32768, + }; + return budgets[effort]; + } + + if (model.id.includes("2.5-flash-lite")) { + const budgets: Record = { + minimal: 512, + low: 2048, + medium: 8192, + high: 24576, + }; + return budgets[effort]; + } + + if (model.id.includes("2.5-flash")) { + const budgets: Record = { + minimal: 128, + low: 2048, + medium: 8192, + high: 24576, + }; + return budgets[effort]; + } + + return -1; +} 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/api/google-vertex.ts b/packages/ai/src/api/google-vertex.ts new file mode 100644 index 00000000..79087c61 --- /dev/null +++ b/packages/ai/src/api/google-vertex.ts @@ -0,0 +1,582 @@ +import { + type GenerateContentConfig, + type GenerateContentParameters, + GoogleGenAI, + type HttpOptions, + ResourceScope, + type ThinkingConfig, + ThinkingLevel, +} from "@google/genai"; +import { calculateCost, clampThinkingLevel } from "../models.ts"; +import type { + Api, + AssistantMessage, + Context, + Model, + ThinkingLevel as PiThinkingLevel, + ProviderEnv, + SimpleStreamOptions, + StreamFunction, + StreamOptions, + TextContent, + ThinkingBudgets, + ThinkingContent, + ToolCall, +} from "../types.ts"; +import { AssistantMessageEventStream } from "../utils/event-stream.ts"; +import { getProviderEnvValue } from "../utils/provider-env.ts"; +import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; +import type { GoogleThinkingLevel } from "./google-shared.ts"; +import { + convertMessages, + convertTools, + isThinkingPart, + mapStopReason, + mapToolChoice, + retainThoughtSignature, +} from "./google-shared.ts"; +import { buildBaseOptions } from "./simple-options.ts"; + +export interface GoogleVertexOptions extends StreamOptions { + toolChoice?: "auto" | "none" | "any"; + thinking?: { + enabled: boolean; + budgetTokens?: number; // -1 for dynamic, 0 to disable + level?: GoogleThinkingLevel; + }; + project?: string; + location?: string; +} + +const API_VERSION = "v1"; +const GCP_VERTEX_CREDENTIALS_MARKER = "gcp-vertex-credentials"; + +const THINKING_LEVEL_MAP: Record = { + THINKING_LEVEL_UNSPECIFIED: ThinkingLevel.THINKING_LEVEL_UNSPECIFIED, + MINIMAL: ThinkingLevel.MINIMAL, + LOW: ThinkingLevel.LOW, + MEDIUM: ThinkingLevel.MEDIUM, + HIGH: ThinkingLevel.HIGH, +}; + +// Counter for generating unique tool call IDs +let toolCallCounter = 0; + +export const stream: StreamFunction<"google-vertex", GoogleVertexOptions> = ( + model: Model<"google-vertex">, + context: Context, + options?: GoogleVertexOptions, +): AssistantMessageEventStream => { + const stream = new AssistantMessageEventStream(); + + (async () => { + const output: AssistantMessage = { + role: "assistant", + content: [], + api: "google-vertex" as 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(), + }; + + try { + const apiKey = resolveApiKey(options); + // Create the client using either a Vertex API key, if provided, or ADC with project and location + const client = apiKey + ? createClientWithApiKey(model, apiKey, options?.headers) + : createClient(model, resolveProject(options), resolveLocation(options), options?.headers, options?.env); + let params = buildParams(model, context, options); + const nextParams = await options?.onPayload?.(params, model); + if (nextParams !== undefined) { + params = nextParams as GenerateContentParameters; + } + const googleStream = await client.models.generateContentStream(params); + + stream.push({ type: "start", partial: output }); + let currentBlock: TextContent | ThinkingContent | null = null; + const blocks = output.content; + const blockIndex = () => blocks.length - 1; + for await (const chunk of googleStream) { + // Vertex uses the same @google/genai GenerateContentResponse type as Gemini. + // responseId is documented there as an output-only identifier for each response. + output.responseId ||= chunk.responseId; + const candidate = chunk.candidates?.[0]; + if (candidate?.content?.parts) { + for (const part of candidate.content.parts) { + if (part.text !== undefined) { + const isThinking = isThinkingPart(part); + if ( + !currentBlock || + (isThinking && currentBlock.type !== "thinking") || + (!isThinking && currentBlock.type !== "text") + ) { + if (currentBlock) { + if (currentBlock.type === "text") { + stream.push({ + type: "text_end", + contentIndex: blocks.length - 1, + content: currentBlock.text, + partial: output, + }); + } else { + stream.push({ + type: "thinking_end", + contentIndex: blockIndex(), + content: currentBlock.thinking, + partial: output, + }); + } + } + if (isThinking) { + currentBlock = { type: "thinking", thinking: "", thinkingSignature: undefined }; + output.content.push(currentBlock); + stream.push({ type: "thinking_start", contentIndex: blockIndex(), partial: output }); + } else { + currentBlock = { type: "text", text: "" }; + output.content.push(currentBlock); + stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output }); + } + } + if (currentBlock.type === "thinking") { + currentBlock.thinking += part.text; + currentBlock.thinkingSignature = retainThoughtSignature( + currentBlock.thinkingSignature, + part.thoughtSignature, + ); + stream.push({ + type: "thinking_delta", + contentIndex: blockIndex(), + delta: part.text, + partial: output, + }); + } else { + currentBlock.text += part.text; + currentBlock.textSignature = retainThoughtSignature( + currentBlock.textSignature, + part.thoughtSignature, + ); + stream.push({ + type: "text_delta", + contentIndex: blockIndex(), + delta: part.text, + partial: output, + }); + } + } + + if (part.functionCall) { + if (currentBlock) { + if (currentBlock.type === "text") { + stream.push({ + type: "text_end", + contentIndex: blockIndex(), + content: currentBlock.text, + partial: output, + }); + } else { + stream.push({ + type: "thinking_end", + contentIndex: blockIndex(), + content: currentBlock.thinking, + partial: output, + }); + } + currentBlock = null; + } + + const providedId = part.functionCall.id; + const needsNewId = + !providedId || output.content.some((b) => b.type === "toolCall" && b.id === providedId); + const toolCallId = needsNewId + ? `${part.functionCall.name}_${Date.now()}_${++toolCallCounter}` + : providedId; + + const toolCall: ToolCall = { + type: "toolCall", + id: toolCallId, + name: part.functionCall.name || "", + arguments: (part.functionCall.args as Record) ?? {}, + ...(part.thoughtSignature && { thoughtSignature: part.thoughtSignature }), + }; + + output.content.push(toolCall); + stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output }); + stream.push({ + type: "toolcall_delta", + contentIndex: blockIndex(), + delta: JSON.stringify(toolCall.arguments), + partial: output, + }); + stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output }); + } + } + } + + if (candidate?.finishReason) { + output.stopReason = mapStopReason(candidate.finishReason); + if (output.content.some((b) => b.type === "toolCall")) { + output.stopReason = "toolUse"; + } + } + + if (chunk.usageMetadata) { + output.usage = { + input: + (chunk.usageMetadata.promptTokenCount || 0) - (chunk.usageMetadata.cachedContentTokenCount || 0), + output: + (chunk.usageMetadata.candidatesTokenCount || 0) + (chunk.usageMetadata.thoughtsTokenCount || 0), + cacheRead: chunk.usageMetadata.cachedContentTokenCount || 0, + cacheWrite: 0, + totalTokens: chunk.usageMetadata.totalTokenCount || 0, + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + total: 0, + }, + }; + calculateCost(model, output.usage); + } + } + + if (currentBlock) { + if (currentBlock.type === "text") { + stream.push({ + type: "text_end", + contentIndex: blockIndex(), + content: currentBlock.text, + partial: output, + }); + } else { + stream.push({ + type: "thinking_end", + contentIndex: blockIndex(), + content: currentBlock.thinking, + partial: output, + }); + } + } + + if (options?.signal?.aborted) { + throw new Error("Request was aborted"); + } + + if (output.stopReason === "aborted" || output.stopReason === "error") { + throw new Error("An unknown error occurred"); + } + + stream.push({ type: "done", reason: output.stopReason, message: output }); + stream.end(); + } catch (error) { + // Remove internal index property used during streaming + for (const block of output.content) { + if ("index" in block) { + delete (block as { index?: number }).index; + } + } + output.stopReason = options?.signal?.aborted ? "aborted" : "error"; + output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error); + stream.push({ type: "error", reason: output.stopReason, error: output }); + stream.end(); + } + })(); + + return stream; +}; + +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 stream(model, context, { + ...base, + thinking: { enabled: false }, + } satisfies GoogleVertexOptions); + } + + const clampedReasoning = clampThinkingLevel(model, options.reasoning); + const effort = (clampedReasoning === "off" ? "high" : clampedReasoning) as ClampedThinkingLevel; + const geminiModel = model as unknown as Model<"google-generative-ai">; + + if (isGemini3ProModel(geminiModel) || isGemini3FlashModel(geminiModel)) { + return stream(model, context, { + ...base, + thinking: { + enabled: true, + level: getGemini3ThinkingLevel(effort, geminiModel), + }, + } satisfies GoogleVertexOptions); + } + + return stream(model, context, { + ...base, + thinking: { + enabled: true, + budgetTokens: getGoogleBudget(geminiModel, effort, options.thinkingBudgets), + }, + } satisfies GoogleVertexOptions); +}; + +function createClient( + model: Model<"google-vertex">, + project: string, + location: string, + optionsHeaders?: Record, + env?: ProviderEnv, +): GoogleGenAI { + const googleAuthOptions = buildGoogleAuthOptions(env); + return new GoogleGenAI({ + vertexai: true, + project, + location, + apiVersion: API_VERSION, + ...(googleAuthOptions ? { googleAuthOptions } : {}), + httpOptions: buildHttpOptions(model, optionsHeaders), + }); +} + +function createClientWithApiKey( + model: Model<"google-vertex">, + apiKey: string, + optionsHeaders?: Record, +): GoogleGenAI { + return new GoogleGenAI({ + vertexai: true, + apiKey, + apiVersion: API_VERSION, + httpOptions: buildHttpOptions(model, optionsHeaders), + }); +} + +function buildHttpOptions( + model: Model<"google-vertex">, + optionsHeaders?: Record, +): HttpOptions | undefined { + const httpOptions: HttpOptions = {}; + const baseUrl = resolveCustomBaseUrl(model.baseUrl); + if (baseUrl) { + httpOptions.baseUrl = baseUrl; + httpOptions.baseUrlResourceScope = ResourceScope.COLLECTION; + if (baseUrlIncludesApiVersion(baseUrl)) { + httpOptions.apiVersion = ""; + } + } + + if (model.headers || optionsHeaders) { + httpOptions.headers = { ...model.headers, ...optionsHeaders }; + } + + return Object.keys(httpOptions).length > 0 ? httpOptions : undefined; +} + +function resolveCustomBaseUrl(baseUrl: string): string | undefined { + const trimmed = baseUrl.trim(); + if (!trimmed || trimmed.includes("{location}")) { + return undefined; + } + return trimmed; +} + +function baseUrlIncludesApiVersion(baseUrl: string): boolean { + try { + const url = new URL(baseUrl); + return url.pathname.split("/").some((part) => /^v\d+(?:beta\d*)?$/.test(part)); + } catch { + return /(?:^|\/)v\d+(?:beta\d*)?(?:\/|$)/.test(baseUrl); + } +} + +function buildGoogleAuthOptions(env?: ProviderEnv): { keyFilename: string } | undefined { + const keyFilename = getProviderEnvValue("GOOGLE_APPLICATION_CREDENTIALS", env); + return keyFilename ? { keyFilename } : undefined; +} + +function resolveApiKey(options?: GoogleVertexOptions): string | undefined { + const apiKey = options?.apiKey?.trim(); + if (!apiKey || apiKey === GCP_VERTEX_CREDENTIALS_MARKER || isPlaceholderApiKey(apiKey)) { + return undefined; + } + return apiKey; +} + +function isPlaceholderApiKey(apiKey: string): boolean { + return /^<[^>]+>$/.test(apiKey); +} + +function resolveProject(options?: GoogleVertexOptions): string { + const project = + options?.project || + getProviderEnvValue("GOOGLE_CLOUD_PROJECT", options?.env) || + getProviderEnvValue("GCLOUD_PROJECT", options?.env); + if (!project) { + throw new Error( + "Vertex AI requires a project ID. Set GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT or pass project in options.", + ); + } + return project; +} + +function resolveLocation(options?: GoogleVertexOptions): string { + const location = options?.location || getProviderEnvValue("GOOGLE_CLOUD_LOCATION", options?.env); + if (!location) { + throw new Error("Vertex AI requires a location. Set GOOGLE_CLOUD_LOCATION or pass location in options."); + } + return location; +} + +function buildParams( + model: Model<"google-vertex">, + context: Context, + options: GoogleVertexOptions = {}, +): GenerateContentParameters { + const contents = convertMessages(model, context); + + const generationConfig: GenerateContentConfig = {}; + if (options.temperature !== undefined) { + generationConfig.temperature = options.temperature; + } + if (options.maxTokens !== undefined) { + generationConfig.maxOutputTokens = options.maxTokens; + } + + const config: GenerateContentConfig = { + ...(Object.keys(generationConfig).length > 0 && generationConfig), + ...(context.systemPrompt && { systemInstruction: sanitizeSurrogates(context.systemPrompt) }), + ...(context.tools && context.tools.length > 0 && { tools: convertTools(context.tools) }), + }; + + if (context.tools && context.tools.length > 0 && options.toolChoice) { + config.toolConfig = { + functionCallingConfig: { + mode: mapToolChoice(options.toolChoice), + }, + }; + } else { + config.toolConfig = undefined; + } + + if (options.thinking?.enabled && model.reasoning) { + const thinkingConfig: ThinkingConfig = { includeThoughts: true }; + if (options.thinking.level !== undefined) { + thinkingConfig.thinkingLevel = THINKING_LEVEL_MAP[options.thinking.level]; + } else if (options.thinking.budgetTokens !== undefined) { + thinkingConfig.thinkingBudget = options.thinking.budgetTokens; + } + config.thinkingConfig = thinkingConfig; + } else if (model.reasoning && options.thinking && !options.thinking.enabled) { + config.thinkingConfig = getDisabledThinkingConfig(model); + } + + if (options.signal) { + if (options.signal.aborted) { + throw new Error("Request aborted"); + } + config.abortSignal = options.signal; + } + + const params: GenerateContentParameters = { + model: model.id, + contents, + config, + }; + + return params; +} + +type ClampedThinkingLevel = Exclude; + +function isGemini3ProModel(model: Model<"google-generative-ai">): boolean { + return /gemini-3(?:\.\d+)?-pro/.test(model.id.toLowerCase()); +} + +function isGemini3FlashModel(model: Model<"google-generative-ai">): boolean { + const id = model.id.toLowerCase(); + return /gemini-3(?:\.\d+)?-flash/.test(id) || id === "gemini-flash-latest" || id === "gemini-flash-lite-latest"; +} + +function getDisabledThinkingConfig(model: Model<"google-vertex">): ThinkingConfig { + // Google docs: Gemini 3.1 Pro cannot disable thinking, and Gemini 3 Flash / Flash-Lite + // do not support full thinking-off either. For Gemini 3 models, use the lowest supported + // thinkingLevel without includeThoughts so hidden thinking remains invisible to pi. + const geminiModel = model as unknown as Model<"google-generative-ai">; + if (isGemini3ProModel(geminiModel)) { + return { thinkingLevel: ThinkingLevel.LOW }; + } + if (isGemini3FlashModel(geminiModel)) { + return { thinkingLevel: ThinkingLevel.MINIMAL }; + } + + // Gemini 2.x supports disabling via thinkingBudget = 0. + return { thinkingBudget: 0 }; +} + +function getGemini3ThinkingLevel( + effort: ClampedThinkingLevel, + model: Model<"google-generative-ai">, +): GoogleThinkingLevel { + if (isGemini3ProModel(model)) { + switch (effort) { + case "minimal": + case "low": + return "LOW"; + case "medium": + case "high": + return "HIGH"; + } + } + switch (effort) { + case "minimal": + return "MINIMAL"; + case "low": + return "LOW"; + case "medium": + return "MEDIUM"; + case "high": + return "HIGH"; + } +} + +function getGoogleBudget( + model: Model<"google-generative-ai">, + effort: ClampedThinkingLevel, + customBudgets?: ThinkingBudgets, +): number { + if (customBudgets?.[effort] !== undefined) { + return customBudgets[effort]!; + } + + if (model.id.includes("2.5-pro")) { + const budgets: Record = { + minimal: 128, + low: 2048, + medium: 8192, + high: 32768, + }; + return budgets[effort]; + } + + if (model.id.includes("2.5-flash")) { + const budgets: Record = { + minimal: 128, + low: 2048, + medium: 8192, + high: 24576, + }; + return budgets[effort]; + } + + return -1; +} diff --git a/packages/ai/src/api/lazy.ts b/packages/ai/src/api/lazy.ts new file mode 100644 index 00000000..fe1836ae --- /dev/null +++ b/packages/ai/src/api/lazy.ts @@ -0,0 +1,70 @@ +import type { Api, AssistantMessage, AssistantMessageEvent, Model, ProviderStreams } 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; +} + +/** + * 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/api/mistral-conversations.ts b/packages/ai/src/api/mistral-conversations.ts new file mode 100644 index 00000000..66519e5e --- /dev/null +++ b/packages/ai/src/api/mistral-conversations.ts @@ -0,0 +1,664 @@ +import { Mistral } from "@mistralai/mistralai"; +import type { + ChatCompletionStreamRequest, + ChatCompletionStreamRequestMessage, + CompletionEvent, + ContentChunk, + FunctionTool, +} from "@mistralai/mistralai/models/components"; +import { calculateCost, clampThinkingLevel } from "../models.ts"; +import type { + AssistantMessage, + Context, + Message, + Model, + SimpleStreamOptions, + StopReason, + StreamFunction, + StreamOptions, + TextContent, + ThinkingContent, + Tool, + ToolCall, +} from "../types.ts"; +import { AssistantMessageEventStream } from "../utils/event-stream.ts"; +import { shortHash } from "../utils/hash.ts"; +import { parseStreamingJson } from "../utils/json-parse.ts"; +import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; +import { buildBaseOptions } from "./simple-options.ts"; +import { transformMessages } from "./transform-messages.ts"; + +const MISTRAL_TOOL_CALL_ID_LENGTH = 9; +const MAX_MISTRAL_ERROR_BODY_CHARS = 4000; + +/** + * Provider-specific options for the Mistral API. + */ +type MistralReasoningEffort = "none" | "high"; + +export interface MistralOptions extends StreamOptions { + toolChoice?: "auto" | "none" | "any" | "required" | { type: "function"; function: { name: string } }; + promptMode?: "reasoning"; + reasoningEffort?: MistralReasoningEffort; +} + +/** + * Stream responses from Mistral using `chat.stream`. + */ +export const stream: StreamFunction<"mistral-conversations", MistralOptions> = ( + model: Model<"mistral-conversations">, + context: Context, + options?: MistralOptions, +): AssistantMessageEventStream => { + const stream = new AssistantMessageEventStream(); + + (async () => { + const output = createOutput(model); + + try { + const apiKey = options?.apiKey; + if (!apiKey) { + throw new Error(`No API key for provider: ${model.provider}`); + } + + // Intentionally per-request: avoids shared SDK mutable state across concurrent consumers. + const mistral = new Mistral({ + apiKey, + serverURL: model.baseUrl, + }); + + const normalizeMistralToolCallId = createMistralToolCallIdNormalizer(); + const transformedMessages = transformMessages(context.messages, model, (id) => normalizeMistralToolCallId(id)); + + let payload = buildChatPayload(model, context, transformedMessages, options); + const nextPayload = await options?.onPayload?.(payload, model); + if (nextPayload !== undefined) { + payload = nextPayload as ChatCompletionStreamRequest; + } + const mistralStream = await mistral.chat.stream(payload, buildRequestOptions(model, options)); + stream.push({ type: "start", partial: output }); + await consumeChatStream(model, output, stream, mistralStream); + + if (options?.signal?.aborted) { + throw new Error("Request was aborted"); + } + + if (output.stopReason === "aborted" || output.stopReason === "error") { + throw new Error("An unknown error occurred"); + } + + stream.push({ type: "done", reason: output.stopReason, message: output }); + stream.end(); + } catch (error) { + for (const block of output.content) { + // partialArgs is only a streaming scratch buffer; never persist it. + delete (block as { partialArgs?: string }).partialArgs; + } + output.stopReason = options?.signal?.aborted ? "aborted" : "error"; + output.errorMessage = formatMistralError(error); + stream.push({ type: "error", reason: output.stopReason, error: output }); + stream.end(); + } + })(); + + return stream; +}; + +/** + * Maps provider-agnostic `SimpleStreamOptions` to Mistral options. + */ +export const streamSimple: StreamFunction<"mistral-conversations", SimpleStreamOptions> = ( + model: Model<"mistral-conversations">, + context: Context, + options?: SimpleStreamOptions, +): AssistantMessageEventStream => { + const apiKey = options?.apiKey; + if (!apiKey) { + throw new Error(`No API key for provider: ${model.provider}`); + } + + const base = buildBaseOptions(model, options, apiKey); + const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined; + const reasoning = clampedReasoning === "off" ? undefined : clampedReasoning; + const shouldUseReasoning = model.reasoning && reasoning !== undefined; + + return stream(model, context, { + ...base, + promptMode: shouldUseReasoning && usesPromptModeReasoning(model) ? "reasoning" : undefined, + reasoningEffort: + shouldUseReasoning && usesReasoningEffort(model) ? mapReasoningEffort(model, reasoning) : undefined, + } satisfies MistralOptions); +}; + +function createOutput(model: Model<"mistral-conversations">): 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(), + }; +} + +function createMistralToolCallIdNormalizer(): (id: string) => string { + const idMap = new Map(); + const reverseMap = new Map(); + + return (id: string): string => { + const existing = idMap.get(id); + if (existing) return existing; + + let attempt = 0; + while (true) { + const candidate = deriveMistralToolCallId(id, attempt); + const owner = reverseMap.get(candidate); + if (!owner || owner === id) { + idMap.set(id, candidate); + reverseMap.set(candidate, id); + return candidate; + } + attempt++; + } + }; +} + +function deriveMistralToolCallId(id: string, attempt: number): string { + const normalized = id.replace(/[^a-zA-Z0-9]/g, ""); + if (attempt === 0 && normalized.length === MISTRAL_TOOL_CALL_ID_LENGTH) return normalized; + const seedBase = normalized || id; + const seed = attempt === 0 ? seedBase : `${seedBase}:${attempt}`; + return shortHash(seed) + .replace(/[^a-zA-Z0-9]/g, "") + .slice(0, MISTRAL_TOOL_CALL_ID_LENGTH); +} + +function formatMistralError(error: unknown): string { + if (error instanceof Error) { + const sdkError = error as Error & { statusCode?: unknown; body?: unknown }; + const statusCode = typeof sdkError.statusCode === "number" ? sdkError.statusCode : undefined; + const bodyText = typeof sdkError.body === "string" ? sdkError.body.trim() : undefined; + if (statusCode !== undefined && bodyText) { + return `Mistral API error (${statusCode}): ${truncateErrorText(bodyText, MAX_MISTRAL_ERROR_BODY_CHARS)}`; + } + if (statusCode !== undefined) return `Mistral API error (${statusCode}): ${error.message}`; + return error.message; + } + return safeJsonStringify(error); +} + +function truncateErrorText(text: string, maxChars: number): string { + if (text.length <= maxChars) return text; + return `${text.slice(0, maxChars)}... [truncated ${text.length - maxChars} chars]`; +} + +function safeJsonStringify(value: unknown): string { + try { + const serialized = JSON.stringify(value); + return serialized === undefined ? String(value) : serialized; + } catch { + return String(value); + } +} + +function buildRequestOptions(model: Model<"mistral-conversations">, options?: MistralOptions) { + const requestOptions: { + signal?: AbortSignal; + retries: { strategy: "none" }; + headers?: Record; + } = { + retries: { strategy: "none" }, + }; + if (options?.signal) requestOptions.signal = options.signal; + + const headers: Record = {}; + if (model.headers) Object.assign(headers, model.headers); + if (options?.headers) Object.assign(headers, options.headers); + + // Mistral infrastructure uses `x-affinity` for KV-cache reuse (prefix caching). + // Respect explicit caller-provided header values. + if (shouldUsePromptCaching(options) && !headers["x-affinity"]) { + headers["x-affinity"] = options.sessionId; + } + + if (Object.keys(headers).length > 0) { + requestOptions.headers = headers; + } + + return requestOptions; +} + +function buildChatPayload( + model: Model<"mistral-conversations">, + context: Context, + messages: Message[], + options?: MistralOptions, +): ChatCompletionStreamRequest { + const payload: ChatCompletionStreamRequest = { + model: model.id, + stream: true, + messages: toChatMessages(messages, model.input.includes("image")), + }; + + if (context.tools?.length) payload.tools = toFunctionTools(context.tools); + if (options?.temperature !== undefined) payload.temperature = options.temperature; + if (options?.maxTokens !== undefined) payload.maxTokens = options.maxTokens; + 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({ + role: "system", + content: sanitizeSurrogates(context.systemPrompt), + }); + } + + 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, + stream: AssistantMessageEventStream, + mistralStream: AsyncIterable, +): Promise { + let currentBlock: TextContent | ThinkingContent | null = null; + const blocks = output.content; + const blockIndex = () => blocks.length - 1; + const toolBlocksByKey = new Map(); + + const finishCurrentBlock = (block?: typeof currentBlock) => { + if (!block) return; + if (block.type === "text") { + stream.push({ + type: "text_end", + contentIndex: blockIndex(), + content: block.text, + partial: output, + }); + return; + } + if (block.type === "thinking") { + stream.push({ + type: "thinking_end", + contentIndex: blockIndex(), + content: block.thinking, + partial: output, + }); + } + }; + + for await (const event of mistralStream) { + const chunk = event.data; + // Mistral's streamed CompletionChunk carries an id field. Keep the first non-empty one, + // mirroring how OpenAI-style streaming exposes a stable response identifier per stream. + output.responseId ||= chunk.id; + + if (chunk.usage) { + 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 = cachedPromptTokens; + output.usage.cacheWrite = 0; + output.usage.totalTokens = + chunk.usage.totalTokens || + output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite; + calculateCost(model, output.usage); + } + + const choice = chunk.choices[0]; + if (!choice) continue; + + if (choice.finishReason) { + output.stopReason = mapChatStopReason(choice.finishReason); + } + + const delta = choice.delta; + if (delta.content !== null && delta.content !== undefined) { + const contentItems = typeof delta.content === "string" ? [delta.content] : delta.content; + for (const item of contentItems) { + if (typeof item === "string") { + const textDelta = sanitizeSurrogates(item); + if (!currentBlock || currentBlock.type !== "text") { + finishCurrentBlock(currentBlock); + currentBlock = { type: "text", text: "" }; + output.content.push(currentBlock); + stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output }); + } + currentBlock.text += textDelta; + stream.push({ + type: "text_delta", + contentIndex: blockIndex(), + delta: textDelta, + partial: output, + }); + continue; + } + + if (item.type === "thinking") { + const deltaText = item.thinking + .map((part) => ("text" in part ? part.text : "")) + .filter((text) => text.length > 0) + .join(""); + const thinkingDelta = sanitizeSurrogates(deltaText); + if (!thinkingDelta) continue; + if (!currentBlock || currentBlock.type !== "thinking") { + finishCurrentBlock(currentBlock); + currentBlock = { type: "thinking", thinking: "" }; + output.content.push(currentBlock); + stream.push({ type: "thinking_start", contentIndex: blockIndex(), partial: output }); + } + currentBlock.thinking += thinkingDelta; + stream.push({ + type: "thinking_delta", + contentIndex: blockIndex(), + delta: thinkingDelta, + partial: output, + }); + continue; + } + + if (item.type === "text") { + const textDelta = sanitizeSurrogates(item.text); + if (!currentBlock || currentBlock.type !== "text") { + finishCurrentBlock(currentBlock); + currentBlock = { type: "text", text: "" }; + output.content.push(currentBlock); + stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output }); + } + currentBlock.text += textDelta; + stream.push({ + type: "text_delta", + contentIndex: blockIndex(), + delta: textDelta, + partial: output, + }); + } + } + } + + const toolCalls = delta.toolCalls || []; + for (const toolCall of toolCalls) { + if (currentBlock) { + finishCurrentBlock(currentBlock); + currentBlock = null; + } + const callId = + toolCall.id && toolCall.id !== "null" + ? toolCall.id + : deriveMistralToolCallId(`toolcall:${toolCall.index ?? 0}`, 0); + const key = `${callId}:${toolCall.index || 0}`; + const existingIndex = toolBlocksByKey.get(key); + let block: (ToolCall & { partialArgs?: string }) | undefined; + + if (existingIndex !== undefined) { + const existing = output.content[existingIndex]; + if (existing?.type === "toolCall") { + block = existing as ToolCall & { partialArgs?: string }; + } + } + + if (!block) { + block = { + type: "toolCall", + id: callId, + name: toolCall.function.name, + arguments: {}, + partialArgs: "", + }; + output.content.push(block); + toolBlocksByKey.set(key, output.content.length - 1); + stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output }); + } + + const argsDelta = + typeof toolCall.function.arguments === "string" + ? toolCall.function.arguments + : JSON.stringify(toolCall.function.arguments || {}); + block.partialArgs = (block.partialArgs || "") + argsDelta; + block.arguments = parseStreamingJson>(block.partialArgs); + stream.push({ + type: "toolcall_delta", + contentIndex: toolBlocksByKey.get(key)!, + delta: argsDelta, + partial: output, + }); + } + } + + finishCurrentBlock(currentBlock); + for (const index of toolBlocksByKey.values()) { + const block = output.content[index]; + if (block.type !== "toolCall") continue; + const toolBlock = block as ToolCall & { partialArgs?: string }; + toolBlock.arguments = parseStreamingJson>(toolBlock.partialArgs); + // Finalize in-place and strip the scratch buffer so replay only + // carries parsed arguments. + delete toolBlock.partialArgs; + stream.push({ + type: "toolcall_end", + contentIndex: index, + toolCall: toolBlock, + partial: output, + }); + } +} + +function toFunctionTools(tools: Tool[]): Array { + return tools.map((tool) => ({ + type: "function", + function: { + name: tool.name, + description: tool.description, + parameters: stripSymbolKeys(tool.parameters) as Record, + strict: false, + }, + })); +} + +function stripSymbolKeys(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map((item) => stripSymbolKeys(item)); + } + + if (value && typeof value === "object") { + const result: Record = {}; + for (const [key, entry] of Object.entries(value)) { + result[key] = stripSymbolKeys(entry); + } + return result; + } + + return value; +} + +function toChatMessages(messages: Message[], supportsImages: boolean): ChatCompletionStreamRequestMessage[] { + const result: ChatCompletionStreamRequestMessage[] = []; + + for (const msg of messages) { + if (msg.role === "user") { + if (typeof msg.content === "string") { + result.push({ role: "user", content: sanitizeSurrogates(msg.content) }); + continue; + } + const hadImages = msg.content.some((item) => item.type === "image"); + const content: ContentChunk[] = msg.content + .filter((item) => item.type === "text" || supportsImages) + .map((item) => { + if (item.type === "text") return { type: "text", text: sanitizeSurrogates(item.text) }; + return { type: "image_url", imageUrl: `data:${item.mimeType};base64,${item.data}` }; + }); + if (content.length > 0) { + result.push({ role: "user", content }); + continue; + } + if (hadImages && !supportsImages) { + result.push({ role: "user", content: "(image omitted: model does not support images)" }); + } + continue; + } + + if (msg.role === "assistant") { + const contentParts: ContentChunk[] = []; + const toolCalls: Array<{ id: string; type: "function"; function: { name: string; arguments: string } }> = []; + + for (const block of msg.content) { + if (block.type === "text") { + if (block.text.trim().length > 0) { + contentParts.push({ type: "text", text: sanitizeSurrogates(block.text) }); + } + continue; + } + if (block.type === "thinking") { + if (block.thinking.trim().length > 0) { + contentParts.push({ + type: "thinking", + thinking: [{ type: "text", text: sanitizeSurrogates(block.thinking) }], + }); + } + continue; + } + toolCalls.push({ + id: block.id, + type: "function", + function: { name: block.name, arguments: JSON.stringify(block.arguments || {}) }, + }); + } + + const assistantMessage: ChatCompletionStreamRequestMessage = { role: "assistant" }; + if (contentParts.length > 0) assistantMessage.content = contentParts; + if (toolCalls.length > 0) assistantMessage.toolCalls = toolCalls; + if (contentParts.length > 0 || toolCalls.length > 0) result.push(assistantMessage); + continue; + } + + const toolContent: ContentChunk[] = []; + const textResult = msg.content + .filter((part) => part.type === "text") + .map((part) => (part.type === "text" ? sanitizeSurrogates(part.text) : "")) + .join("\n"); + const hasImages = msg.content.some((part) => part.type === "image"); + const toolText = buildToolResultText(textResult, hasImages, supportsImages, msg.isError); + toolContent.push({ type: "text", text: toolText }); + for (const part of msg.content) { + if (!supportsImages) continue; + if (part.type !== "image") continue; + toolContent.push({ + type: "image_url", + imageUrl: `data:${part.mimeType};base64,${part.data}`, + }); + } + result.push({ + role: "tool", + toolCallId: msg.toolCallId, + name: msg.toolName, + content: toolContent, + }); + } + + return result; +} + +function buildToolResultText(text: string, hasImages: boolean, supportsImages: boolean, isError: boolean): string { + const trimmed = text.trim(); + const errorPrefix = isError ? "[tool error] " : ""; + + if (trimmed.length > 0) { + const imageSuffix = hasImages && !supportsImages ? "\n[tool image omitted: model does not support images]" : ""; + return `${errorPrefix}${trimmed}${imageSuffix}`; + } + + if (hasImages) { + if (supportsImages) { + return isError ? "[tool error] (see attached image)" : "(see attached image)"; + } + return isError + ? "[tool error] (image omitted: model does not support images)" + : "(image omitted: model does not support images)"; + } + + return isError ? "[tool error] (no tool output)" : "(no tool output)"; +} + +function usesReasoningEffort(model: Model<"mistral-conversations">): boolean { + return model.id === "mistral-small-2603" || model.id === "mistral-small-latest" || model.id === "mistral-medium-3.5"; +} + +function usesPromptModeReasoning(model: Model<"mistral-conversations">): boolean { + return model.reasoning && !usesReasoningEffort(model); +} + +function mapReasoningEffort( + model: Model<"mistral-conversations">, + level: Exclude, +): MistralReasoningEffort { + return (model.thinkingLevelMap?.[level] ?? "high") as MistralReasoningEffort; +} + +function mapToolChoice( + choice: MistralOptions["toolChoice"], +): "auto" | "none" | "any" | "required" | { type: "function"; function: { name: string } } | undefined { + if (!choice) return undefined; + if (choice === "auto" || choice === "none" || choice === "any" || choice === "required") { + return choice as any; + } + return { + type: "function", + function: { name: choice.function.name }, + }; +} + +function mapChatStopReason(reason: string | null): StopReason { + if (reason === null) return "stop"; + switch (reason) { + case "stop": + return "stop"; + case "length": + case "model_length": + return "length"; + case "tool_calls": + return "toolUse"; + case "error": + return "error"; + default: + return "stop"; + } +} 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 19a2f5d7..ee2503d5 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/api/openai-codex-responses.ts @@ -195,7 +195,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, @@ -408,7 +408,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, @@ -422,7 +422,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 2909678c..e891365e 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/api/openai-completions.ts @@ -133,7 +133,7 @@ function resolveCacheRetention(cacheRetention?: CacheRetention, env?: ProviderEn return "short"; } -export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenAICompletionsOptions> = ( +export const stream: StreamFunction<"openai-completions", OpenAICompletionsOptions> = ( model: Model<"openai-completions">, context: Context, options?: OpenAICompletionsOptions, @@ -463,7 +463,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, @@ -478,7 +478,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 014233d5..92756db3 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/api/openai-responses.ts @@ -80,7 +80,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, @@ -161,7 +161,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, @@ -175,7 +175,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/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/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/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/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/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 new file mode 100644 index 00000000..74bb45bf --- /dev/null +++ b/packages/ai/src/auth/types.ts @@ -0,0 +1,180 @@ +import type { Api, ImagesApi, ImagesModel, 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. Receives the chat or image-generation model + * the request is for (both carry `provider` and `baseUrl`). + */ + resolve(input: { + model: Model | ImagesModel; + 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/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/compat.ts b/packages/ai/src/compat.ts new file mode 100644 index 00000000..7ddbbaaf --- /dev/null +++ b/packages/ai/src/compat.ts @@ -0,0 +1,150 @@ +/** + * 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"; +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 { getBuiltinModel, getBuiltinModels, getBuiltinProviders } from "./providers/all.ts"; +import type { + Api, + AssistantMessage, + AssistantMessageEventStream, + Context, + Model, + ProviderStreamOptions, + ProviderStreams, + SimpleStreamOptions, + StreamOptions, +} from "./types.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()], + ["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()], +]; + +/** + * 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 }); + } +} + +export function resetApiProviders(): void { + clearApiProviders(); + registerBuiltInApiProviders(); +} + +registerBuiltInApiProviders(); + +function hasExplicitApiKey(apiKey: string | undefined): apiKey is string { + return typeof apiKey === "string" && apiKey.trim().length > 0; +} + +function withEnvApiKey( + model: Model, + options: TOptions | undefined, +): TOptions | undefined { + if (hasExplicitApiKey(options?.apiKey)) return options; + const apiKey = getEnvApiKey(model.provider); + if (!apiKey) return options; + return { ...options, apiKey } as TOptions; +} + +function resolveApiProvider(api: Api) { + const provider = getApiProvider(api); + if (!provider) { + throw new Error(`No API provider registered for api: ${api}`); + } + return provider; +} + +export function stream( + model: Model, + context: Context, + options?: ProviderStreamOptions, +): AssistantMessageEventStream { + const provider = resolveApiProvider(model.api); + return provider.stream(model, context, withEnvApiKey(model, options) as StreamOptions); +} + +export async function complete( + model: Model, + context: Context, + options?: ProviderStreamOptions, +): Promise { + const s = stream(model, context, options); + return s.result(); +} + +export function streamSimple( + model: Model, + context: Context, + options?: SimpleStreamOptions, +): AssistantMessageEventStream { + const provider = resolveApiProvider(model.api); + return provider.streamSimple(model, context, withEnvApiKey(model, options)); +} + +export async function completeSimple( + model: Model, + context: Context, + options?: SimpleStreamOptions, +): Promise { + const s = streamSimple(model, context, options); + return s.result(); +} 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 ed7aeaa8..6c3f9675 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -1,30 +1,30 @@ 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"; +// 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 type { AzureOpenAIResponsesOptions } from "./api/azure-openai-responses.ts"; +export type { BedrockOptions, BedrockThinkingDisplay } from "./api/bedrock-converse-stream.ts"; +export type { GoogleOptions } from "./api/google-generative-ai.ts"; +export type { GoogleThinkingLevel } from "./api/google-shared.ts"; +export type { GoogleVertexOptions } from "./api/google-vertex.ts"; +export * from "./api/lazy.ts"; +export type { MistralOptions } from "./api/mistral-conversations.ts"; +export type { OpenAICodexResponsesOptions, OpenAICodexWebSocketDebugStats } from "./api/openai-codex-responses.ts"; +export type { OpenAICompletionsOptions } from "./api/openai-completions.ts"; +export type { OpenAIResponsesOptions } from "./api/openai-responses.ts"; +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 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"; export * from "./utils/diagnostics.ts"; export * from "./utils/event-stream.ts"; diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 64325817..0129ddee 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -1,17180 +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.35, - output: 0.75, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 40960, - } 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: true, - input: ["text"], - cost: { - input: 2.25, - output: 2.75, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 40960, - } 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/moonshotai/kimi-k2.7-code": { - id: "@cf/moonshotai/kimi-k2.7-code", - name: "Kimi K2.7 Code", - 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.19, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } 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">, - "@cf/zai-org/glm-5.2": { - id: "@cf/zai-org/glm-5.2", - name: "Glm 5.2", - 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: 1.4, - output: 4.4, - cacheRead: 0.26, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } 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.028, - 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/glm-5p2": { - id: "accounts/fireworks/models/glm-5p2", - name: "GLM 5.2", - api: "openai-completions", - provider: "fireworks", - 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, - output: 4.4, - cacheRead: 0.26, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "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-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/kimi-k2p7-code": { - id: "accounts/fireworks/models/kimi-k2p7-code", - name: "Kimi K2.7 Code", - 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.19, - cacheWrite: 0, - }, - contextWindow: 262000, - maxTokens: 262000, - } 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/minimax-m3": { - id: "accounts/fireworks/models/minimax-m3", - name: "MiniMax-M3", - 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: 512000, - maxTokens: 512000, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/qwen3p7-plus": { - id: "accounts/fireworks/models/qwen3p7-plus", - name: "Qwen 3.7 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.4, - output: 1.6, - cacheRead: 0.08, - 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">, - "accounts/fireworks/routers/kimi-k2p7-code-fast": { - id: "accounts/fireworks/routers/kimi-k2p7-code-fast", - name: "Kimi K2.7 Code 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: 1.9, - output: 8, - cacheRead: 0.38, - cacheWrite: 0, - }, - contextWindow: 262000, - maxTokens: 262000, - } satisfies Model<"anthropic-messages">, - }, - "github-copilot": { - "claude-fable-5": { - id: "claude-fable-5", - name: "Claude Fable 5", - 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: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "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","minimal":"low"}, - 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","minimal":"low"}, - 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, - thinkingLevelMap: {"minimal":"low","xhigh":"max"}, - 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">, - }, - "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, - 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-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, - 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">, - "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-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - 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", - 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", - 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", - 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.1-flash-lite": { - id: "gemini-3.1-flash-lite", - name: "Gemini 3.1 Flash Lite", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - 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-vertex">, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - 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", - 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.5-flash": { - id: "gemini-3.5-flash", - name: "Gemini 3.5 Flash", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - 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-vertex">, - "gemini-flash-latest": { - id: "gemini-flash-latest", - name: "Gemini Flash Latest", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - 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-vertex">, - "gemini-flash-lite-latest": { - id: "gemini-flash-lite-latest", - name: "Gemini Flash-Lite Latest", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - 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-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": { - "k2p7": { - id: "k2p7", - name: "Kimi K2.7 Code", - 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-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.03, - 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.04, - 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.04, - 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.04, - 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.04, - 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.01, - 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.01, - 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.2, - 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.05, - 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.004, - 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.01, - 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.2, - 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.05, - 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.05, - 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.04, - 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.04, - 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.15, - 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.04, - 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.015, - 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.01, - 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.015, - 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.015, - 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.025, - 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.015, - 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.2, - 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.07, - 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.015, - 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.2, - 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">, - "kimi-k2.7-code": { - id: "kimi-k2.7-code", - name: "Kimi K2.7 Code", - 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, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.19, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2.7-code-highspeed": { - id: "kimi-k2.7-code-highspeed", - name: "Kimi K2.7 Code HighSpeed", - 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, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.9, - output: 8, - cacheRead: 0.38, - 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">, - "kimi-k2.7-code": { - id: "kimi-k2.7-code", - name: "Kimi K2.7 Code", - 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, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.19, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2.7-code-highspeed": { - id: "kimi-k2.7-code-highspeed", - name: "Kimi K2.7 Code HighSpeed", - 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, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.9, - output: 8, - cacheRead: 0.38, - 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.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-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","supportsLongCacheRetention":false,"requiresReasoningContentOnAssistantMessages":true}, - 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}, - 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","supportsLongCacheRetention":false,"requiresReasoningContentOnAssistantMessages":true}, - 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","supportsLongCacheRetention":false}, - 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","supportsLongCacheRetention":false}, - 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","supportsLongCacheRetention":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">, - "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.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">, - "glm-5.2": { - id: "glm-5.2", - name: "GLM-5.2", - api: "openai-completions", - provider: "opencode-go", - 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, - output: 4.4, - cacheRead: 0.26, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 131072, - } 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","supportsLongCacheRetention":false}, - 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">, - "kimi-k2.7-code": { - id: "kimi-k2.7-code", - name: "Kimi K2.7 Code", - 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.95, - output: 4, - cacheRead: 0.19, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } 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.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 (3x usage)", - api: "anthropic-messages", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.02, - 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.8, - output: 3.2, - 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-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.1, - 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.25, - output: 0.8, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 80000, - } 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.1, - output: 0.4, - 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">, - "cohere/north-mini-code:free": { - id: "cohere/north-mini-code:free", - name: "Cohere: North Mini Code (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: 64000, - } 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.2002, - output: 0.8001, - 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.2, - output: 0.77, - cacheRead: 0.135, - cacheWrite: 0, - }, - contextWindow: 163840, - 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.79, - 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.15, - 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.09, - output: 0.18, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } 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.083333, - }, - 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.1, - output: 0.4, - cacheRead: 0.01, - cacheWrite: 0.083333, - }, - 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.1, - output: 0.4, - cacheRead: 0.01, - cacheWrite: 0.083333, - }, - 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.05, - cacheWrite: 0.083333, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"openai-completions">, - "google/gemini-3-pro-image": { - id: "google/gemini-3-pro-image", - name: "Google: Nano Banana Pro (Gemini 3 Pro Image)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0.375, - }, - contextWindow: 65536, - maxTokens: 32768, - } 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.025, - cacheWrite: 0.083333, - }, - 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.025, - cacheWrite: 0.083333, - }, - 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.2, - 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.2, - 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.083333, - }, - 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.05, - 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.35, - cacheRead: 0.09, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } 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: 8192, - } 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.05, - output: 0.1, - cacheRead: 0.05, - 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.025, - 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">, - "liquid/lfm-2.5-1.2b-thinking:free": { - id: "liquid/lfm-2.5-1.2b-thinking:free", - name: "LiquidAI: LFM2.5-1.2B-Thinking (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: 32768, - maxTokens: 4096, - } 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.4, - output: 0.4, - 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.1, - 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.1, - 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.4, - 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.9, - cacheRead: 0.05, - 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.25, - output: 1, - cacheRead: 0.05, - 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.9, - 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.4, - 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.2, - output: 0.2, - 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.1, - output: 0.1, - 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.2, - 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.2, - 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.05, - 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.4, - 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.4, - 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.2, - 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.2, - 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.2, - 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.1, - 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.57, - 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.66, - output: 3.41, - cacheRead: 0.144, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2.7-code": { - id: "moonshotai/kimi-k2.7-code", - name: "MoonshotAI: Kimi K2.7 Code", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.612, - output: 3.069, - cacheRead: 0.1296, - 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", - reasoning: true, - input: ["text"], - cost: { - input: 0.4, - output: 0.4, - 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.05, - output: 0.2, - 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.45, - 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.2, - cacheRead: 0.1, - 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: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.4, - output: 1.6, - cacheRead: 0.1, - 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.1, - output: 0.4, - cacheRead: 0.025, - 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.025, - 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.05, - output: 0.4, - 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.025, - 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.2, - 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: 32768, - } 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.0375, - 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/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", - 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": { - 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)", - 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": { - 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)", - 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.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", - reasoning: false, - input: ["text"], - cost: { - input: 0.36, - output: 0.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "qwen/qwen-2.5-7b-instruct": { - id: "qwen/qwen-2.5-7b-instruct", - name: "Qwen: Qwen2.5 7B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.04, - output: 0.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } 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.052, - 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.1, - 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.455, - output: 1.82, - 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.1, - 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.1, - output: 0.1, - cacheRead: 0.1, - 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.4, - 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.05, - output: 0.4, - cacheRead: 0.05, - 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.8, - 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.8, - 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.2, - 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.104, - output: 0.416, - 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, - 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.385, - output: 2.45, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 4096, - } 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.1, - 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.8, - 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.2885, - output: 3.17, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262140, - } 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: 262144, - } 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.32, - output: 1.28, - cacheRead: 0.064, - cacheWrite: 0.4, - }, - 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.1, - output: 0.1, - 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.2, - 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.021, - 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.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", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.4, - output: 0.4, - 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.2, - 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.2, - 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.2, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 4096, - } 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.13, - output: 0.85, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 98304, - } 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.8, - 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.9, - 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.4, - 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.4, - 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.49, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 65535, - } satisfies Model<"openai-completions">, - "z-ai/glm-5.2": { - id: "z-ai/glm-5.2", - name: "Z.ai: GLM 5.2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text"], - cost: { - input: 1, - output: 4, - cacheRead: 0.18, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 32768, - } 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.1, - 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.083333, - }, - 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.2, - 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.66, - output: 3.41, - cacheRead: 0.144, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } 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.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">, - "MiniMaxAI/MiniMax-M3": { - id: "MiniMaxAI/MiniMax-M3", - name: "MiniMax-M3", - 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.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 524288, - maxTokens: 250000, - } satisfies Model<"openai-completions">, - "Qwen/Qwen2.5-7B-Instruct-Turbo": { - id: "Qwen/Qwen2.5-7B-Instruct-Turbo", - name: "Qwen 2.5 7B Instruct Turbo", - 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.3, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 32768, - } 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}, - reasoning: false, - input: ["text"], - cost: { - input: 0.2, - output: 0.6, - 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.5-9B": { - id: "Qwen/Qwen3.5-9B", - name: "Qwen3.5 9B", - 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.17, - output: 0.25, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } 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}, - reasoning: false, - input: ["text"], - cost: { - input: 1.25, - output: 3.75, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 500000, - } 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: 1.74, - output: 3.48, - 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.39, - output: 0.97, - 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.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">, - "moonshotai/Kimi-K2.7-Code": { - id: "moonshotai/Kimi-K2.7-Code", - name: "Kimi K2.7 Code", - 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.95, - output: 4, - cacheRead: 0.19, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } 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">, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - 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.05, - output: 0.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "zai-org/GLM-5": { - id: "zai-org/GLM-5", - name: "GLM-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: 1, - output: 3.2, - cacheRead: 0, - 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: "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.4, - 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.2, - 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.4, - 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.1, - output: 0.4, - 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.4, - 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.6, - 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.1, - 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.4, - output: 1.6, - 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.8, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } 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.1, - 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.9, - 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.05, - 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.1, - output: 0.4, - 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.05, - 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.2, - 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.2, - 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.4, - 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.025, - 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.97, - 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.17, - 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.9, - 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.4, - 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.1, - 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.1, - 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.1, - output: 0.1, - 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.4, - 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.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } 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.1, - 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.57, - 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.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.1, - 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">, - "moonshotai/kimi-k2.7-code": { - id: "moonshotai/kimi-k2.7-code", - name: "Kimi K2.7 Code", - 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.19, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2.7-code-highspeed": { - id: "moonshotai/kimi-k2.7-code-highspeed", - name: "Kimi K2.7 Code High Speed", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.9, - output: 8, - cacheRead: 0.38, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } 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.2, - 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.23, - 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.4, - output: 1.6, - cacheRead: 0.1, - 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.1, - output: 0.4, - cacheRead: 0.025, - 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.025, - 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.05, - output: 0.4, - 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.025, - 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.2, - 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.05, - output: 0.2, - 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">, - "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", - 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.02, - cacheWrite: 0, - }, - 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.2, - 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.2, - output: 0.5, - cacheRead: 0.05, - 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.2, - output: 0.5, - cacheRead: 0.05, - 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.2, - 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.2, - 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.2, - 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.2, - 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.2, - 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.2, - 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.2, - 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.2, - 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.1, - 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.2, - 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.2, - 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.8, - 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.9, - cacheRead: 0.05, - 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.4, - 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.4, - 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.2, - cacheRead: 0.2, - 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-5.2": { - id: "zai/glm-5.2", - name: "GLM 5.2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.5, - output: 4.5, - cacheRead: 0.3, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 128000, - } 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-5.2": { - id: "glm-5.2", - name: "GLM-5.2", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","supportsReasoningEffort":true,"zaiToolStream":true}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":"high","medium":"high","high":"high","xhigh":"max"}, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - 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-5.2": { - id: "glm-5.2", - name: "GLM-5.2", - 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}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":"high","medium":"high","high":"high","xhigh":"max"}, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - 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 9d9fa519..47ed7c8a 100644 --- a/packages/ai/src/models.ts +++ b/packages/ai/src/models.ts @@ -1,39 +1,374 @@ -import { MODELS } from "./models.generated.ts"; -import type { Api, KnownProvider, Model, ModelThinkingLevel, Usage } from "./types.ts"; +import { lazyStream } from "./api/lazy.ts"; +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 { + Api, + ApiStreamOptions, + AssistantMessage, + AssistantMessageEventStream, + Context, + Model, + ModelThinkingLevel, + ProviderStreams, + SimpleStreamOptions, + StreamOptions, + Usage, +} from "./types.ts"; -const modelRegistry: Map>> = new Map(); +export { type AuthModel, ModelsError, type ModelsErrorCode } from "./auth/resolve.ts"; -// 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); +/** + * 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; + + /** + * 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(): 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, + 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; + + /** + * 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 Model[]; + + /** + * 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): 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. + * 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); + } + + getModels(provider?: string): readonly Model[] { + if (provider !== undefined) { + const entry = this.providers.get(provider); + if (!entry) return []; + try { + return entry.getModels(); + } catch { + return []; + } + } + + const models: Model[] = []; + 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): 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 { + const provider = this.providers.get(model.provider); + if (!provider) return undefined; + return resolveProviderAuth(provider, model, this.credentials, this.authContext); + } + + 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(); } - 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 createModels(options?: CreateModelsOptions): MutableModels { + return new ModelsImpl(options); } -export function getProviders(): KnownProvider[] { - return Array.from(modelRegistry.keys()) as KnownProvider[]; +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; + /** 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>; } -export function getModels( - provider: TProvider, -): Model>[] { - const models = modelRegistry.get(provider); - return models ? (Array.from(models.values()) as Model>[]) : []; +/** + * 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 { + 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>); + + 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: () => 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)), + }; +} + +/** + * Runtime-checked narrowing for dynamically looked-up models: + * + * ```ts + * const model = 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; } export function calculateCost(model: Model, usage: Usage): Usage["cost"] { diff --git a/packages/ai/src/providers/all.ts b/packages/ai/src/providers/all.ts new file mode 100644 index 00000000..85ba0301 --- /dev/null +++ b/packages/ai/src/providers/all.ts @@ -0,0 +1,131 @@ +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"; +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 { openrouterImagesProvider } from "./openrouter-images.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"; + +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[] { + 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; +} + +/** 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/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 index 5d327f4b..d839ab6a 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -1,1061 +1,35 @@ -import type { Agent as HttpsAgent } from "node:https"; -import { - BedrockRuntimeClient, - type BedrockRuntimeClientConfig, - BedrockRuntimeServiceException, - StopReason as BedrockStopReason, - type Tool as BedrockTool, - CachePointType, - CacheTTL, - type ContentBlock, - type ContentBlockDeltaEvent, - type ContentBlockStartEvent, - type ContentBlockStopEvent, - ConversationRole, - ConverseStreamCommand, - type ConverseStreamMetadataEvent, - ImageFormat, - type Message, - type SystemContentBlock, - type ToolChoice, - type ToolConfiguration, - type ToolResultContentBlock, - ToolResultStatus, -} from "@aws-sdk/client-bedrock-runtime"; -import { NodeHttpHandler } from "@smithy/node-http-handler"; -import type { BuildMiddleware, DocumentType, MetadataBearer } from "@smithy/types"; -import { HttpProxyAgent } from "http-proxy-agent"; -import { HttpsProxyAgent } from "https-proxy-agent"; -import { calculateCost } from "../models.ts"; -import type { - Api, - AssistantMessage, - CacheRetention, - Context, - ImageContent, - Model, - ProviderEnv, - SimpleStreamOptions, - StopReason, - StreamFunction, - StreamOptions, - TextContent, - ThinkingBudgets, - ThinkingContent, - ThinkingLevel, - Tool, - ToolCall, - ToolResultMessage, -} from "../types.ts"; -import { AssistantMessageEventStream } from "../utils/event-stream.ts"; -import { parseStreamingJson } from "../utils/json-parse.ts"; -import { resolveHttpProxyUrlForTarget } from "../utils/node-http-proxy.ts"; -import { getProviderEnvValue } from "../utils/provider-env.ts"; -import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; -import { adjustMaxTokensForThinking, buildBaseOptions, clampReasoning } from "./simple-options.ts"; -import { transformMessages } from "./transform-messages.ts"; +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"; -export type BedrockThinkingDisplay = "summarized" | "omitted"; - -export interface BedrockOptions extends StreamOptions { - region?: string; - profile?: string; - toolChoice?: "auto" | "any" | "none" | { type: "tool"; name: string }; - /* See https://docs.aws.amazon.com/bedrock/latest/userguide/inference-reasoning.html for supported models. */ - reasoning?: ThinkingLevel; - /* Custom token budgets per thinking level. Overrides default budgets. */ - thinkingBudgets?: ThinkingBudgets; - /* Only supported by Claude 4.x models, see https://docs.aws.amazon.com/bedrock/latest/userguide/claude-messages-extended-thinking.html#claude-messages-extended-thinking-tool-use-interleaved */ - interleavedThinking?: boolean; - /** - * Controls how Claude's thinking content is returned in responses. - * - "summarized": Thinking blocks contain summarized thinking text (default here). - * - "omitted": Thinking content is redacted but the signature still travels back - * for multi-turn continuity, reducing time-to-first-text-token. - * - * Note: Anthropic's API default for Claude Opus 4.8 and Mythos Preview is - * "omitted". We default to "summarized" here to keep behavior consistent with - * older Claude 4 models. Only applies to Claude models on Bedrock. - */ - thinkingDisplay?: BedrockThinkingDisplay; - /** Key-value pairs attached to the inference request for cost allocation tagging. - * Keys: max 64 chars, no `aws:` prefix. Values: max 256 chars. Max 50 pairs. - * Tags appear in AWS Cost Explorer split cost allocation data. - * @see https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ConverseStream.html */ - requestMetadata?: Record; - /** Bearer token for Bedrock API key authentication. - * When set, bypasses SigV4 signing and sends Authorization: Bearer instead. - * Requires `bedrock:CallWithBearerToken` IAM permission on the token's identity. - * Set via AWS_BEARER_TOKEN_BEDROCK env var or pass directly. - * @see https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonbedrock.html */ - bearerToken?: string; -} - -type Block = (TextContent | ThinkingContent | ToolCall) & { index?: number; partialJson?: string }; - -const EMPTY_TEXT_PLACEHOLDER = ""; - -export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOptions> = ( - model: Model<"bedrock-converse-stream">, - context: Context, - options: BedrockOptions = {}, -): AssistantMessageEventStream => { - const stream = new AssistantMessageEventStream(); - - (async () => { - const output: AssistantMessage = { - role: "assistant", - content: [], - api: "bedrock-converse-stream" as 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(), - }; - - const blocks = output.content as Block[]; - - const config: BedrockRuntimeClientConfig = { - profile: options.profile || getProviderEnvValue("AWS_PROFILE", options.env), - }; - const configuredRegion = getConfiguredBedrockRegion(options); - const hasAmbientConfiguredProfile = Boolean(getProviderEnvValue("AWS_PROFILE")); - const endpointRegion = getStandardBedrockEndpointRegion(model.baseUrl); - const useExplicitEndpoint = shouldUseExplicitBedrockEndpoint( - model.baseUrl, - configuredRegion, - hasAmbientConfiguredProfile, - ); - - // Only pin standard AWS Bedrock runtime endpoints when no region or ambient AWS_PROFILE is configured. - // This preserves custom endpoints (VPC/proxy) from #3402 without forcing built-in - // catalog defaults such as us-east-1 to override AWS_REGION/AWS_PROFILE. - if (useExplicitEndpoint) { - config.endpoint = model.baseUrl; +/** + * 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" }; } - - // Resolve bearer token for Bedrock API key auth. - const skipAuth = getProviderEnvValue("AWS_BEDROCK_SKIP_AUTH", options.env) === "1"; - const bearerToken = - options.bearerToken || getProviderEnvValue("AWS_BEARER_TOKEN_BEDROCK", options.env) || undefined; - const useBearerToken = bearerToken !== undefined && !skipAuth; - - // in Node.js/Bun environment only - if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) { - // Region resolution: ARN-embedded > explicit option > env vars > SDK default chain. - // When the model ID is an inference profile ARN, extract the region from it. - // This avoids conflicts with AWS_REGION set for other services. - const arnRegionMatch = model.id.match(/^arn:aws(?:-[a-z0-9-]+)?:bedrock:([a-z0-9-]+):/); - if (arnRegionMatch) { - config.region = arnRegionMatch[1]; - } else if (configuredRegion) { - config.region = configuredRegion; - } else if (endpointRegion && useExplicitEndpoint) { - config.region = endpointRegion; - } else if (!hasAmbientConfiguredProfile) { - config.region = "us-east-1"; - } - - // Support proxies that don't need authentication - if (skipAuth) { - config.credentials = { - accessKeyId: "dummy-access-key", - secretAccessKey: "dummy-secret-key", - }; - } - - const credentials = getConfiguredBedrockCredentials(options.env); - if (!skipAuth && credentials) { - config.credentials = credentials; - } - - const proxyUrl = resolveHttpProxyUrlForTarget(model.baseUrl, options.env); - if (proxyUrl) { - // Bedrock runtime uses NodeHttp2Handler by default since v3.798.0, which is based - // on `http2` module and has no support for http agent. - // Use NodeHttpHandler to support HTTP(S) proxy agents. - config.requestHandler = new NodeHttpHandler({ - httpAgent: new HttpProxyAgent(proxyUrl), - httpsAgent: new HttpsProxyAgent(proxyUrl) as unknown as HttpsAgent, - }); - } else if (getProviderEnvValue("AWS_BEDROCK_FORCE_HTTP1", options.env) === "1") { - // Some custom endpoints require HTTP/1.1 instead of HTTP/2 - config.requestHandler = new NodeHttpHandler(); - } - } else { - // Non-Node environment (browser): fall back to us-east-1 since - // there's no config file resolution available. - config.region = - configuredRegion || (endpointRegion && useExplicitEndpoint ? endpointRegion : undefined) || "us-east-1"; - } - - if (useBearerToken) { - config.token = { token: bearerToken }; - config.authSchemePreference = ["httpBearerAuth"]; - } - - try { - const client = new BedrockRuntimeClient(config); - if (options.headers && Object.keys(options.headers).length > 0) { - addCustomHeadersMiddleware(client, options.headers); - } - const cacheRetention = resolveCacheRetention(options.cacheRetention, options.env); - const inferenceMaxTokens = options.maxTokens ?? (isAnthropicClaudeModel(model) ? model.maxTokens : undefined); - let commandInput = { - modelId: model.id, - messages: convertMessages(context, model, cacheRetention, options.env), - system: buildSystemPrompt(context.systemPrompt, model, cacheRetention, options.env), - inferenceConfig: { - ...(inferenceMaxTokens !== undefined && { maxTokens: inferenceMaxTokens }), - ...(options.temperature !== undefined && { temperature: options.temperature }), - }, - toolConfig: convertToolConfig(context.tools, options.toolChoice), - additionalModelRequestFields: buildAdditionalModelRequestFields(model, options), - ...(options.requestMetadata !== undefined && { requestMetadata: options.requestMetadata }), - }; - const nextCommandInput = await options?.onPayload?.(commandInput, model); - if (nextCommandInput !== undefined) { - commandInput = nextCommandInput as typeof commandInput; - } - const command = new ConverseStreamCommand(commandInput); - - const response = await client.send(command, { abortSignal: options.signal }); - if (response.$metadata.httpStatusCode !== undefined) { - const responseHeaders: Record = {}; - if (response.$metadata.requestId) { - responseHeaders["x-amzn-requestid"] = response.$metadata.requestId; - } - await options?.onResponse?.({ status: response.$metadata.httpStatusCode, headers: responseHeaders }, model); - } - - for await (const item of response.stream!) { - if (item.messageStart) { - if (item.messageStart.role !== ConversationRole.ASSISTANT) { - throw new Error("Unexpected assistant message start but got user message start instead"); - } - stream.push({ type: "start", partial: output }); - } else if (item.contentBlockStart) { - handleContentBlockStart(item.contentBlockStart, blocks, output, stream); - } else if (item.contentBlockDelta) { - handleContentBlockDelta(item.contentBlockDelta, blocks, output, stream); - } else if (item.contentBlockStop) { - handleContentBlockStop(item.contentBlockStop, blocks, output, stream); - } else if (item.messageStop) { - output.stopReason = mapStopReason(item.messageStop.stopReason); - } else if (item.metadata) { - handleMetadata(item.metadata, model, output); - } else if (item.internalServerException) { - throw item.internalServerException; - } else if (item.modelStreamErrorException) { - throw item.modelStreamErrorException; - } else if (item.validationException) { - throw item.validationException; - } else if (item.throttlingException) { - throw item.throttlingException; - } else if (item.serviceUnavailableException) { - throw item.serviceUnavailableException; - } - } - - if (options.signal?.aborted) { - throw new Error("Request was aborted"); - } - - if (output.stopReason === "error" || output.stopReason === "aborted") { - throw new Error("An unknown error occurred"); - } - - stream.push({ type: "done", reason: output.stopReason, message: output }); - stream.end(); - } catch (error) { - for (const block of output.content) { - delete (block as Block).index; - // partialJson is only a streaming scratch buffer; never persist it. - delete (block as Block).partialJson; - } - output.stopReason = options.signal?.aborted ? "aborted" : "error"; - output.errorMessage = formatBedrockError(error); - stream.push({ type: "error", reason: output.stopReason, error: output }); - stream.end(); - } - })(); - - return stream; + 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; + }, }; -/** - * Human-readable prefixes for Bedrock SDK exception names. - * The downstream retry logic in agent-session matches patterns like - * `server.?error` and `service.?unavailable`, so we preserve the legacy - * prefix format rather than using the raw SDK exception name. - */ -const BEDROCK_ERROR_PREFIXES: Record = { - InternalServerException: "Internal server error", - ModelStreamErrorException: "Model stream error", - ValidationException: "Validation error", - ThrottlingException: "Throttling error", - ServiceUnavailableException: "Service unavailable", -}; - -/** - * Some models reject the account/profile's configured Bedrock data retention mode - * (e.g. "data retention mode 'default' is not available for this model"). Point - * users at the AWS docs explaining how to configure a supported mode. - */ -const BEDROCK_DATA_RETENTION_DOCS_URL = "https://docs.aws.amazon.com/bedrock/latest/userguide/data-retention.html"; - -/** - * Format a Bedrock error with a human-readable prefix. - * AWS SDK exceptions (both from `client.send()` and from stream event items) - * extend BedrockRuntimeServiceException. We map the `.name` to a stable - * human-readable prefix so downstream consumers (retry logic, context-overflow - * detection) can distinguish error categories via simple string matching. - */ -function formatBedrockError(error: unknown): string { - const message = error instanceof Error ? error.message : JSON.stringify(error); - const dataRetentionHint = /data retention mode/i.test(message) - ? ` See ${BEDROCK_DATA_RETENTION_DOCS_URL} for supported data retention modes.` - : ""; - if (error instanceof BedrockRuntimeServiceException) { - const prefix = BEDROCK_ERROR_PREFIXES[error.name] ?? error.name; - return `${prefix}: ${message}${dataRetentionHint}`; - } - return `${message}${dataRetentionHint}`; -} - -/** - * Header keys that must never be overwritten by caller-supplied headers. - * `host` and `x-amz-*` participate in the SigV4 canonical request; `authorization` - * is owned by SigV4 or the bearer-token path (config.token + authSchemePreference). - * Compared case-insensitively (caller key is lower-cased before lookup). - */ -const RESERVED_HEADER_EXACT = new Set(["authorization", "host"]); - -function isReservedHeader(key: string): boolean { - const lower = key.toLowerCase(); - return lower.startsWith("x-amz-") || RESERVED_HEADER_EXACT.has(lower); -} - -/** - * Attach caller-supplied headers to the outgoing Bedrock request via a Smithy - * `build`-step middleware. The `build` step runs after request serialisation but - * before SigV4 signing, so injected headers are covered by the signature. Reserved - * SigV4 / auth headers (`x-amz-*`, `authorization`, `host`) are silently skipped; - * all other caller headers override any existing same-named header on the request. - */ -function addCustomHeadersMiddleware(client: BedrockRuntimeClient, headers: Record): void { - const middleware: BuildMiddleware = (next) => async (args) => { - const request = args.request; - if (request && typeof request === "object" && "headers" in request) { - const requestHeaders = (request as { headers: Record }).headers; - for (const [key, value] of Object.entries(headers)) { - if (!isReservedHeader(key)) { - requestHeaders[key] = value; - } - } - } - return next(args); - }; - client.middlewareStack.add(middleware, { step: "build", name: "pi-ai-custom-headers", priority: "low" }); -} - -export const streamSimpleBedrock: 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); - } - - if (isAnthropicClaudeModel(model)) { - if (supportsAdaptiveThinking(model.id, model.name)) { - return streamBedrock(model, context, { - ...base, - reasoning: options.reasoning, - thinkingBudgets: options.thinkingBudgets, - } satisfies BedrockOptions); - } - - // Undefined means the caller did not request an output cap; let the helper use the model cap. - // Do not coerce to 0 here, or the thinking budget would become the entire maxTokens value. - const adjusted = adjustMaxTokensForThinking( - base.maxTokens, - model.maxTokens, - options.reasoning, - options.thinkingBudgets, - ); - - return streamBedrock(model, context, { - ...base, - maxTokens: adjusted.maxTokens, - reasoning: options.reasoning, - thinkingBudgets: { - ...(options.thinkingBudgets || {}), - [clampReasoning(options.reasoning)!]: adjusted.thinkingBudget, - }, - } satisfies BedrockOptions); - } - - return streamBedrock(model, context, { - ...base, - reasoning: options.reasoning, - thinkingBudgets: options.thinkingBudgets, - } satisfies BedrockOptions); -}; - -function handleContentBlockStart( - event: ContentBlockStartEvent, - blocks: Block[], - output: AssistantMessage, - stream: AssistantMessageEventStream, -): void { - const index = event.contentBlockIndex!; - const start = event.start; - - if (start?.toolUse) { - const block: Block = { - type: "toolCall", - id: start.toolUse.toolUseId || "", - name: start.toolUse.name || "", - arguments: {}, - partialJson: "", - index, - }; - output.content.push(block); - stream.push({ type: "toolcall_start", contentIndex: blocks.length - 1, partial: output }); - } -} - -function handleContentBlockDelta( - event: ContentBlockDeltaEvent, - blocks: Block[], - output: AssistantMessage, - stream: AssistantMessageEventStream, -): void { - const contentBlockIndex = event.contentBlockIndex!; - const delta = event.delta; - let index = blocks.findIndex((b) => b.index === contentBlockIndex); - let block = blocks[index]; - - if (delta?.text !== undefined) { - // If no text block exists yet, create one, as `handleContentBlockStart` is not sent for text blocks - if (!block) { - const newBlock: Block = { type: "text", text: "", index: contentBlockIndex }; - output.content.push(newBlock); - index = blocks.length - 1; - block = blocks[index]; - stream.push({ type: "text_start", contentIndex: index, partial: output }); - } - if (block.type === "text") { - block.text += delta.text; - stream.push({ type: "text_delta", contentIndex: index, delta: delta.text, partial: output }); - } - } else if (delta?.toolUse && block?.type === "toolCall") { - block.partialJson = (block.partialJson || "") + (delta.toolUse.input || ""); - block.arguments = parseStreamingJson(block.partialJson); - stream.push({ type: "toolcall_delta", contentIndex: index, delta: delta.toolUse.input || "", partial: output }); - } else if (delta?.reasoningContent) { - let thinkingBlock = block; - let thinkingIndex = index; - - if (!thinkingBlock) { - const newBlock: Block = { type: "thinking", thinking: "", thinkingSignature: "", index: contentBlockIndex }; - output.content.push(newBlock); - thinkingIndex = blocks.length - 1; - thinkingBlock = blocks[thinkingIndex]; - stream.push({ type: "thinking_start", contentIndex: thinkingIndex, partial: output }); - } - - if (thinkingBlock?.type === "thinking") { - if (delta.reasoningContent.text) { - thinkingBlock.thinking += delta.reasoningContent.text; - stream.push({ - type: "thinking_delta", - contentIndex: thinkingIndex, - delta: delta.reasoningContent.text, - partial: output, - }); - } - if (delta.reasoningContent.signature) { - thinkingBlock.thinkingSignature = - (thinkingBlock.thinkingSignature || "") + delta.reasoningContent.signature; - } - } - } -} - -function handleMetadata( - event: ConverseStreamMetadataEvent, - model: Model<"bedrock-converse-stream">, - output: AssistantMessage, -): void { - if (event.usage) { - output.usage.input = event.usage.inputTokens || 0; - output.usage.output = event.usage.outputTokens || 0; - output.usage.cacheRead = event.usage.cacheReadInputTokens || 0; - output.usage.cacheWrite = event.usage.cacheWriteInputTokens || 0; - output.usage.totalTokens = event.usage.totalTokens || output.usage.input + output.usage.output; - calculateCost(model, output.usage); - } -} - -function handleContentBlockStop( - event: ContentBlockStopEvent, - blocks: Block[], - output: AssistantMessage, - stream: AssistantMessageEventStream, -): void { - const index = blocks.findIndex((b) => b.index === event.contentBlockIndex); - const block = blocks[index]; - if (!block) return; - delete (block as Block).index; - - switch (block.type) { - case "text": - stream.push({ type: "text_end", contentIndex: index, content: block.text, partial: output }); - break; - case "thinking": - stream.push({ type: "thinking_end", contentIndex: index, content: block.thinking, partial: output }); - break; - case "toolCall": - block.arguments = parseStreamingJson(block.partialJson); - // Finalize in-place and strip the scratch buffer so replay only - // carries parsed arguments. - delete (block as Block).partialJson; - stream.push({ type: "toolcall_end", contentIndex: index, toolCall: block, partial: output }); - break; - } -} - -/** - * Check if the model supports adaptive thinking (Opus 4.6+, Sonnet 4.6). - * Checks both model ID and model name to support application inference profiles - * whose ARNs don't contain the model name. - */ -function getModelMatchCandidates(modelId: string, modelName?: string): string[] { - const values = modelName ? [modelId, modelName] : [modelId]; - return values.flatMap((value) => { - const lower = value.toLowerCase(); - return [lower, lower.replace(/[\s_.:]+/g, "-")]; +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(), }); } - -function supportsAdaptiveThinking(modelId: string, modelName?: string): boolean { - const candidates = getModelMatchCandidates(modelId, modelName); - return candidates.some( - (s) => - s.includes("opus-4-6") || - s.includes("opus-4-7") || - s.includes("opus-4-8") || - s.includes("sonnet-4-6") || - s.includes("fable-5"), - ); -} - -function supportsNativeXhighEffort(model: Model<"bedrock-converse-stream">): boolean { - const candidates = getModelMatchCandidates(model.id, model.name); - return candidates.some((s) => s.includes("opus-4-7") || s.includes("opus-4-8") || s.includes("fable-5")); -} - -function mapThinkingLevelToEffort( - model: Model<"bedrock-converse-stream">, - level: SimpleStreamOptions["reasoning"], -): "low" | "medium" | "high" | "xhigh" | "max" { - if (level === "xhigh" && supportsNativeXhighEffort(model)) return "xhigh"; - - const mapped = level ? model.thinkingLevelMap?.[level] : undefined; - if (typeof mapped === "string") return mapped as "low" | "medium" | "high" | "xhigh" | "max"; - - switch (level) { - case "minimal": - case "low": - return "low"; - case "medium": - return "medium"; - case "high": - return "high"; - default: - return "high"; - } -} - -/** - * Resolve cache retention preference. - * Defaults to "short" and uses PI_CACHE_RETENTION for backward compatibility. - */ -function resolveCacheRetention(cacheRetention?: CacheRetention, env?: ProviderEnv): CacheRetention { - if (cacheRetention) { - return cacheRetention; - } - if (getProviderEnvValue("PI_CACHE_RETENTION", env) === "long") { - return "long"; - } - return "short"; -} - -/** - * Check if the model is an Anthropic Claude model on Bedrock. - * Checks both model ID and model name to support application inference profiles - * whose ARNs don't contain the model name. - */ -function isAnthropicClaudeModel(model: Model<"bedrock-converse-stream">): boolean { - const id = model.id.toLowerCase(); - const name = model.name?.toLowerCase() ?? ""; - return ( - id.includes("anthropic.claude") || - id.includes("anthropic/claude") || - name.includes("anthropic.claude") || - name.includes("anthropic/claude") || - name.includes("claude") - ); -} - -/** - * Check if the model supports prompt caching. - * Supported: Claude 3.5 Haiku, Claude 3.7 Sonnet, Claude 4.x models - * - * For base models and system-defined inference profiles the model ID / ARN - * contains the model name, so we can decide locally. - * - * For application inference profiles (whose ARNs don't contain the model name), - * also checks model.name which is user-controlled via models.json or registerProvider. - * As a last resort, set AWS_BEDROCK_FORCE_CACHE=1 to enable cache points. - * Amazon Nova models have automatic caching and don't need explicit cache points. - */ -function supportsPromptCaching(model: Model<"bedrock-converse-stream">, env?: ProviderEnv): boolean { - const candidates = getModelMatchCandidates(model.id, model.name); - - const hasClaudeRef = candidates.some((s) => s.includes("claude")); - if (!hasClaudeRef) { - // Application inference profiles don't contain the model name in the ARN. - // Allow users to force cache points via environment variable. - if (getProviderEnvValue("AWS_BEDROCK_FORCE_CACHE", env) === "1") return true; - return false; - } - // Claude 4.x models (opus-4, sonnet-4, haiku-4) - if (candidates.some((s) => s.includes("-4-"))) return true; - // Claude 3.7 Sonnet - if (candidates.some((s) => s.includes("claude-3-7-sonnet"))) return true; - // Claude 3.5 Haiku - if (candidates.some((s) => s.includes("claude-3-5-haiku"))) return true; - return false; -} - -/** - * Check if the model supports thinking signatures in reasoningContent. - * Only Anthropic Claude models support the signature field. - * Other models (OpenAI, Qwen, Minimax, Moonshot, etc.) reject it with: - * "This model doesn't support the reasoningContent.reasoningText.signature field" - * - * Checks both model ID and model name to support application inference profiles. - */ -function supportsThinkingSignature(model: Model<"bedrock-converse-stream">): boolean { - return isAnthropicClaudeModel(model); -} - -function buildSystemPrompt( - systemPrompt: string | undefined, - model: Model<"bedrock-converse-stream">, - cacheRetention: CacheRetention, - env?: ProviderEnv, -): SystemContentBlock[] | undefined { - if (!systemPrompt) return undefined; - - const blocks: SystemContentBlock[] = [{ text: sanitizeSurrogates(systemPrompt) }]; - - // Add cache point for supported Claude models when caching is enabled - if (cacheRetention !== "none" && supportsPromptCaching(model, env)) { - blocks.push({ - cachePoint: { type: CachePointType.DEFAULT, ...(cacheRetention === "long" ? { ttl: CacheTTL.ONE_HOUR } : {}) }, - }); - } - - return blocks; -} - -function normalizeToolCallId(id: string): string { - const sanitized = id.replace(/[^a-zA-Z0-9_-]/g, "_"); - return sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized; -} - -function createNonBlankTextBlock(text: string): ContentBlock.TextMember | undefined { - const sanitized = sanitizeSurrogates(text); - return sanitized.trim().length === 0 ? undefined : { text: sanitized }; -} - -function createRequiredTextBlock(text: string): ContentBlock.TextMember { - return createNonBlankTextBlock(text) ?? { text: EMPTY_TEXT_PLACEHOLDER }; -} - -function convertToolResultContent(content: (TextContent | ImageContent)[]): ToolResultContentBlock[] { - const result: ToolResultContentBlock[] = []; - for (const c of content) { - if (c.type === "image") { - result.push({ image: createImageBlock(c.mimeType, c.data) }); - } else { - const textBlock = createNonBlankTextBlock(c.text); - if (textBlock) result.push(textBlock); - } - } - if (result.length === 0) result.push({ text: EMPTY_TEXT_PLACEHOLDER }); - return result; -} - -function convertMessages( - context: Context, - model: Model<"bedrock-converse-stream">, - cacheRetention: CacheRetention, - env?: ProviderEnv, -): Message[] { - const result: Message[] = []; - const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId); - - for (let i = 0; i < transformedMessages.length; i++) { - const m = transformedMessages[i]; - - switch (m.role) { - case "user": { - const content: ContentBlock[] = []; - if (typeof m.content === "string") { - content.push(createRequiredTextBlock(m.content)); - } else { - for (const c of m.content) { - switch (c.type) { - case "text": { - const textBlock = createNonBlankTextBlock(c.text); - if (textBlock) content.push(textBlock); - break; - } - case "image": - content.push({ image: createImageBlock(c.mimeType, c.data) }); - break; - default: - continue; - } - } - if (content.length === 0) content.push({ text: EMPTY_TEXT_PLACEHOLDER }); - } - result.push({ - role: ConversationRole.USER, - content, - }); - break; - } - case "assistant": { - // Skip assistant messages with empty content (e.g., from aborted requests) - // Bedrock rejects messages with empty content arrays - if (m.content.length === 0) { - continue; - } - const contentBlocks: ContentBlock[] = []; - for (const c of m.content) { - switch (c.type) { - case "text": { - // Skip empty text blocks - const textBlock = createNonBlankTextBlock(c.text); - if (!textBlock) continue; - contentBlocks.push(textBlock); - break; - } - case "toolCall": - contentBlocks.push({ - toolUse: { toolUseId: c.id, name: c.name, input: c.arguments }, - }); - break; - case "thinking": { - // Skip empty thinking blocks - const thinking = sanitizeSurrogates(c.thinking); - if (thinking.trim().length === 0) continue; - // Only Anthropic models support the signature field in reasoningText. - // For other models, we omit the signature to avoid errors like: - // "This model doesn't support the reasoningContent.reasoningText.signature field" - if (supportsThinkingSignature(model)) { - // Signatures arrive after thinking deltas. If a partial or externally - // persisted message lacks a signature, Bedrock rejects the replayed - // reasoning block. Fall back to plain text, matching Anthropic. - if (!c.thinkingSignature || c.thinkingSignature.trim().length === 0) { - contentBlocks.push({ text: thinking }); - } else { - contentBlocks.push({ - reasoningContent: { - reasoningText: { - text: thinking, - signature: c.thinkingSignature, - }, - }, - }); - } - } else { - contentBlocks.push({ - reasoningContent: { - reasoningText: { text: thinking }, - }, - }); - } - break; - } - default: - continue; - } - } - // Skip if all content blocks were filtered out - if (contentBlocks.length === 0) { - continue; - } - result.push({ - role: ConversationRole.ASSISTANT, - content: contentBlocks, - }); - break; - } - case "toolResult": { - // Collect all consecutive toolResult messages into a single user message - // Bedrock requires all tool results to be in one message - const toolResults: ContentBlock.ToolResultMember[] = []; - - // Add current tool result with all content blocks combined - toolResults.push({ - toolResult: { - toolUseId: m.toolCallId, - content: convertToolResultContent(m.content), - status: m.isError ? ToolResultStatus.ERROR : ToolResultStatus.SUCCESS, - }, - }); - - // Look ahead for consecutive toolResult messages - let j = i + 1; - while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") { - const nextMsg = transformedMessages[j] as ToolResultMessage; - toolResults.push({ - toolResult: { - toolUseId: nextMsg.toolCallId, - content: convertToolResultContent(nextMsg.content), - status: nextMsg.isError ? ToolResultStatus.ERROR : ToolResultStatus.SUCCESS, - }, - }); - j++; - } - - // Skip the messages we've already processed - i = j - 1; - - result.push({ - role: ConversationRole.USER, - content: toolResults, - }); - break; - } - default: - continue; - } - } - - // Add cache point to the last user message for supported Claude models when caching is enabled - if (cacheRetention !== "none" && supportsPromptCaching(model, env) && result.length > 0) { - const lastMessage = result[result.length - 1]; - if (lastMessage.role === ConversationRole.USER && lastMessage.content) { - (lastMessage.content as ContentBlock[]).push({ - cachePoint: { - type: CachePointType.DEFAULT, - ...(cacheRetention === "long" ? { ttl: CacheTTL.ONE_HOUR } : {}), - }, - }); - } - } - - return result; -} - -function convertToolConfig( - tools: Tool[] | undefined, - toolChoice: BedrockOptions["toolChoice"], -): ToolConfiguration | undefined { - if (!tools?.length || toolChoice === "none") return undefined; - - const bedrockTools: BedrockTool[] = tools.map((tool) => ({ - toolSpec: { - name: tool.name, - description: tool.description, - inputSchema: { json: tool.parameters as unknown as DocumentType }, - }, - })); - - let bedrockToolChoice: ToolChoice | undefined; - switch (toolChoice) { - case "auto": - bedrockToolChoice = { auto: {} }; - break; - case "any": - bedrockToolChoice = { any: {} }; - break; - default: - if (toolChoice?.type === "tool") { - bedrockToolChoice = { tool: { name: toolChoice.name } }; - } - } - - return { tools: bedrockTools, toolChoice: bedrockToolChoice }; -} - -function mapStopReason(reason: string | undefined): StopReason { - switch (reason) { - case BedrockStopReason.END_TURN: - case BedrockStopReason.STOP_SEQUENCE: - return "stop"; - case BedrockStopReason.MAX_TOKENS: - case BedrockStopReason.MODEL_CONTEXT_WINDOW_EXCEEDED: - return "length"; - case BedrockStopReason.TOOL_USE: - return "toolUse"; - default: - return "error"; - } -} - -function getConfiguredBedrockRegion(options: BedrockOptions): string | undefined { - return ( - options.region || - getProviderEnvValue("AWS_REGION", options.env) || - getProviderEnvValue("AWS_DEFAULT_REGION", options.env) || - undefined - ); -} - -function getConfiguredBedrockCredentials(env?: ProviderEnv): BedrockRuntimeClientConfig["credentials"] | undefined { - const accessKeyId = getProviderEnvValue("AWS_ACCESS_KEY_ID", env); - const secretAccessKey = getProviderEnvValue("AWS_SECRET_ACCESS_KEY", env); - if (!accessKeyId || !secretAccessKey) { - return undefined; - } - const sessionToken = getProviderEnvValue("AWS_SESSION_TOKEN", env); - return { - accessKeyId, - secretAccessKey, - ...(sessionToken ? { sessionToken } : {}), - }; -} - -function getStandardBedrockEndpointRegion(baseUrl: string | undefined): string | undefined { - if (!baseUrl) { - return undefined; - } - - try { - const { hostname } = new URL(baseUrl); - const match = hostname.toLowerCase().match(/^bedrock-runtime(?:-fips)?\.([a-z0-9-]+)\.amazonaws\.com(?:\.cn)?$/); - return match?.[1]; - } catch { - return undefined; - } -} - -function shouldUseExplicitBedrockEndpoint( - baseUrl: string, - configuredRegion: string | undefined, - hasAmbientConfiguredProfile: boolean, -): boolean { - const endpointRegion = getStandardBedrockEndpointRegion(baseUrl); - if (!endpointRegion) { - return true; - } - - return !configuredRegion && !hasAmbientConfiguredProfile; -} - -function isGovCloudBedrockTarget(model: Model<"bedrock-converse-stream">, options: BedrockOptions): boolean { - const region = getConfiguredBedrockRegion(options); - if (region?.toLowerCase().startsWith("us-gov-")) { - return true; - } - - const modelId = model.id.toLowerCase(); - return modelId.startsWith("us-gov.") || modelId.startsWith("arn:aws-us-gov:"); -} - -function buildAdditionalModelRequestFields( - model: Model<"bedrock-converse-stream">, - options: BedrockOptions, -): Record | undefined { - if (!options.reasoning || !model.reasoning) { - return undefined; - } - - if (isAnthropicClaudeModel(model)) { - // GovCloud Bedrock currently rejects the Claude thinking.display field. - // Omit it there until the GovCloud Converse schema catches up. - const display = isGovCloudBedrockTarget(model, options) ? undefined : (options.thinkingDisplay ?? "summarized"); - const result: Record = supportsAdaptiveThinking(model.id, model.name) - ? { - thinking: { type: "adaptive", ...(display !== undefined ? { display } : {}) }, - output_config: { effort: mapThinkingLevelToEffort(model, options.reasoning) }, - } - : (() => { - const defaultBudgets: Record = { - minimal: 1024, - low: 2048, - medium: 8192, - high: 16384, - xhigh: 16384, // Claude doesn't support xhigh, clamp to high - }; - - // Custom budgets override defaults (xhigh not in ThinkingBudgets, use high) - const level = options.reasoning === "xhigh" ? "high" : options.reasoning; - const budget = options.thinkingBudgets?.[level] ?? defaultBudgets[options.reasoning]; - - return { - thinking: { - type: "enabled", - budget_tokens: budget, - ...(display !== undefined ? { display } : {}), - }, - }; - })(); - - if (!supportsAdaptiveThinking(model.id, model.name) && (options.interleavedThinking ?? true)) { - result.anthropic_beta = ["interleaved-thinking-2025-05-14"]; - } - - return result; - } - - return undefined; -} - -function createImageBlock(mimeType: string, data: string) { - let format: ImageFormat; - switch (mimeType) { - case "image/jpeg": - case "image/jpg": - format = ImageFormat.JPEG; - break; - case "image/png": - format = ImageFormat.PNG; - break; - case "image/gif": - format = ImageFormat.GIF; - break; - case "image/webp": - format = ImageFormat.WEBP; - break; - default: - throw new Error(`Unknown image type: ${mimeType}`); - } - - const binaryString = atob(data); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - - return { source: { bytes }, format }; -} 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 index 6ffab7cb..6570fc38 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -1,1242 +1,20 @@ -import Anthropic from "@anthropic-ai/sdk"; -import type { - CacheControlEphemeral, - ContentBlockParam, - MessageCreateParamsStreaming, - MessageParam, - RawMessageStreamEvent, - RefusalStopDetails, -} from "@anthropic-ai/sdk/resources/messages.js"; -import { calculateCost } from "../models.ts"; -import type { - AnthropicMessagesCompat, - Api, - AssistantMessage, - CacheRetention, - Context, - ImageContent, - Message, - Model, - ProviderEnv, - SimpleStreamOptions, - StopReason, - StreamFunction, - StreamOptions, - TextContent, - ThinkingContent, - Tool, - ToolCall, - ToolResultMessage, -} from "../types.ts"; -import { AssistantMessageEventStream } from "../utils/event-stream.ts"; -import { headersToRecord } from "../utils/headers.ts"; -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"; - -/** - * Resolve cache retention preference. - * Defaults to "short" and uses PI_CACHE_RETENTION for backward compatibility. - */ -function resolveCacheRetention(cacheRetention?: CacheRetention, env?: ProviderEnv): CacheRetention { - if (cacheRetention) { - return cacheRetention; - } - if (getProviderEnvValue("PI_CACHE_RETENTION", env) === "long") { - return "long"; - } - return "short"; -} - -function getCacheControl( - model: Model<"anthropic-messages">, - cacheRetention?: CacheRetention, - env?: ProviderEnv, -): { retention: CacheRetention; cacheControl?: CacheControlEphemeral } { - const retention = resolveCacheRetention(cacheRetention, env); - if (retention === "none") { - return { retention }; - } - const ttl = retention === "long" && getAnthropicCompat(model).supportsLongCacheRetention ? "1h" : undefined; - return { - retention, - cacheControl: { type: "ephemeral", ...(ttl && { ttl }) }, - }; -} - -// Stealth mode: Mimic Claude Code's tool naming exactly -const claudeCodeVersion = "2.1.75"; - -// Claude Code 2.x tool names (canonical casing) -// Source: https://cchistory.mariozechner.at/data/prompts-2.1.11.md -// To update: https://github.com/badlogic/cchistory -const claudeCodeTools = [ - "Read", - "Write", - "Edit", - "Bash", - "Grep", - "Glob", - "AskUserQuestion", - "EnterPlanMode", - "ExitPlanMode", - "KillShell", - "NotebookEdit", - "Skill", - "Task", - "TaskOutput", - "TodoWrite", - "WebFetch", - "WebSearch", -]; - -const ccToolLookup = new Map(claudeCodeTools.map((t) => [t.toLowerCase(), t])); - -// Convert tool name to CC canonical casing if it matches (case-insensitive) -const toClaudeCodeName = (name: string) => ccToolLookup.get(name.toLowerCase()) ?? name; -const fromClaudeCodeName = (name: string, tools?: Tool[]) => { - if (tools && tools.length > 0) { - const lowerName = name.toLowerCase(); - const matchedTool = tools.find((tool) => tool.name.toLowerCase() === lowerName); - if (matchedTool) return matchedTool.name; - } - return name; -}; - -/** - * Convert content blocks to Anthropic API format - */ -function convertContentBlocks(content: (TextContent | ImageContent)[]): - | string - | Array< - | { type: "text"; text: string } - | { - type: "image"; - source: { - type: "base64"; - media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; - data: string; - }; - } - > { - // If only text blocks, return as concatenated string for simplicity - const hasImages = content.some((c) => c.type === "image"); - if (!hasImages) { - return sanitizeSurrogates(content.map((c) => (c as TextContent).text).join("\n")); - } - - // If we have images, convert to content block array - const blocks = content.map((block) => { - if (block.type === "text") { - return { - type: "text" as const, - text: sanitizeSurrogates(block.text), - }; - } - return { - type: "image" as const, - source: { - type: "base64" as const, - media_type: block.mimeType as "image/jpeg" | "image/png" | "image/gif" | "image/webp", - data: block.data, - }, - }; - }); - - // If only images (no text), add placeholder text block - const hasText = blocks.some((b) => b.type === "text"); - if (!hasText) { - blocks.unshift({ - type: "text" as const, - text: "(see attached image)", - }); - } - - return blocks; -} - -export type AnthropicEffort = "low" | "medium" | "high" | "xhigh" | "max"; - -export type AnthropicThinkingDisplay = "summarized" | "omitted"; - -const FINE_GRAINED_TOOL_STREAMING_BETA = "fine-grained-tool-streaming-2025-05-14"; -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, - supportsTemperature: model.compat?.supportsTemperature ?? true, - allowEmptySignature: model.compat?.allowEmptySignature ?? false, - }; -} - -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 - * a simple reasoning level to this option, or callers set it explicitly). - */ - thinkingEnabled?: boolean; - /** - * Token budget for extended thinking (older models only). - * Ignored for adaptive thinking models. - * Default: 1024 when `thinkingEnabled` is true and no budget is provided. - */ - thinkingBudgetTokens?: number; - /** - * Effort level for adaptive thinking models. - * Controls how much thinking Claude allocates: - * - "max": Always thinks with no constraints (Opus 4.6 only) - * - "xhigh": Highest reasoning level (Opus 4.7+, Fable 5) - * - "high": Always thinks, deep reasoning - * - "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 - * level to this option. - */ - effort?: AnthropicEffort; - /** - * Controls how thinking content is returned in API responses. - * - "summarized": Thinking blocks contain summarized thinking text. - * - "omitted": Thinking blocks return an empty thinking field; the encrypted - * signature still travels back for multi-turn continuity. Use for faster - * time-to-first-text-token when your UI does not surface thinking. - * - * Note: Anthropic's API default for Claude Opus 4.7 and Claude Mythos Preview - * is "omitted". We default to "summarized" here to keep behavior consistent - * with older Claude 4 models. Set this explicitly to "omitted" to opt in. - * Default: "summarized" when thinking is enabled. - */ - thinkingDisplay?: AnthropicThinkingDisplay; - /** - * Whether to request the interleaved thinking beta header for non-adaptive - * thinking models. Adaptive thinking models have interleaved thinking built in, - * so the header is skipped for them regardless of this setting. - * Default: true. - */ - interleavedThinking?: boolean; - /** - * Anthropic tool choice behavior. String values map to Anthropic's built-in - * choices; `{ type: "tool", name }` forces a specific tool. - * Default: omitted (Anthropic default behavior, currently equivalent to auto). - */ - toolChoice?: "auto" | "any" | "none" | { type: "tool"; name: string }; - /** - * Pre-built Anthropic client instance. When provided, skips internal client - * construction entirely. Use this to inject alternative SDK clients such as - * `AnthropicVertex` that shares the same messaging API. - */ - client?: Anthropic; -} - -function mergeHeaders(...headerSources: (Record | undefined)[]): Record { - const merged: Record = {}; - for (const headers of headerSources) { - if (headers) { - Object.assign(merged, headers); - } - } - return merged; -} - -interface ServerSentEvent { - event: string | null; - data: string; - raw: string[]; -} - -interface SseDecoderState { - event: string | null; - data: string[]; - raw: string[]; -} - -const ANTHROPIC_MESSAGE_EVENTS: ReadonlySet = new Set([ - "message_start", - "message_delta", - "message_stop", - "content_block_start", - "content_block_delta", - "content_block_stop", -]); - -function flushSseEvent(state: SseDecoderState): ServerSentEvent | null { - if (!state.event && state.data.length === 0) { - return null; - } - - const event: ServerSentEvent = { - event: state.event, - data: state.data.join("\n"), - raw: [...state.raw], - }; - state.event = null; - state.data = []; - state.raw = []; - return event; -} - -function decodeSseLine(line: string, state: SseDecoderState): ServerSentEvent | null { - if (line === "") { - return flushSseEvent(state); - } - - state.raw.push(line); - if (line.startsWith(":")) { - return null; - } - - const delimiterIndex = line.indexOf(":"); - const fieldName = delimiterIndex === -1 ? line : line.slice(0, delimiterIndex); - let value = delimiterIndex === -1 ? "" : line.slice(delimiterIndex + 1); - if (value.startsWith(" ")) { - value = value.slice(1); - } - - if (fieldName === "event") { - state.event = value; - } else if (fieldName === "data") { - state.data.push(value); - } - - return null; -} - -function nextLineBreakIndex(text: string): number { - const carriageReturnIndex = text.indexOf("\r"); - const newlineIndex = text.indexOf("\n"); - if (carriageReturnIndex === -1) { - return newlineIndex; - } - if (newlineIndex === -1) { - return carriageReturnIndex; - } - return Math.min(carriageReturnIndex, newlineIndex); -} - -function consumeLine(text: string): { line: string; rest: string } | null { - const lineBreakIndex = nextLineBreakIndex(text); - if (lineBreakIndex === -1) { - return null; - } - - let nextIndex = lineBreakIndex + 1; - if (text[lineBreakIndex] === "\r" && text[nextIndex] === "\n") { - nextIndex += 1; - } - - return { - line: text.slice(0, lineBreakIndex), - rest: text.slice(nextIndex), - }; -} - -async function* iterateSseMessages( - body: ReadableStream, - signal?: AbortSignal, -): AsyncGenerator { - const reader = body.getReader(); - const decoder = new TextDecoder(); - const state: SseDecoderState = { event: null, data: [], raw: [] }; - let buffer = ""; - - try { - while (true) { - if (signal?.aborted) { - throw new Error("Request was aborted"); - } - - const { value, done } = await reader.read(); - if (done) { - break; - } - - buffer += decoder.decode(value, { stream: true }); - let consumed = consumeLine(buffer); - while (consumed) { - buffer = consumed.rest; - const event = decodeSseLine(consumed.line, state); - if (event) { - yield event; - } - consumed = consumeLine(buffer); - } - } - - buffer += decoder.decode(); - let consumed = consumeLine(buffer); - while (consumed) { - buffer = consumed.rest; - const event = decodeSseLine(consumed.line, state); - if (event) { - yield event; - } - consumed = consumeLine(buffer); - } - - if (buffer.length > 0) { - const event = decodeSseLine(buffer, state); - if (event) { - yield event; - } - } - - const trailingEvent = flushSseEvent(state); - if (trailingEvent) { - yield trailingEvent; - } - } finally { - reader.releaseLock(); - } -} - -async function* iterateAnthropicEvents( - response: Response, - signal?: AbortSignal, -): AsyncGenerator { - if (!response.body) { - throw new Error("Attempted to iterate over an Anthropic response with no body"); - } - - let sawMessageStart = false; - let sawMessageEnd = false; - - for await (const sse of iterateSseMessages(response.body, signal)) { - if (sse.event === "error") { - throw new Error(sse.data); - } - - if (!ANTHROPIC_MESSAGE_EVENTS.has(sse.event ?? "")) { - continue; - } - - try { - const event = parseJsonWithRepair(sse.data); - if (event.type === "message_start") { - sawMessageStart = true; - } else if (event.type === "message_stop") { - sawMessageEnd = true; - } - yield event; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - throw new Error( - `Could not parse Anthropic SSE event ${sse.event}: ${message}; data=${sse.data}; raw=${sse.raw.join("\\n")}`, - ); - } - } - - if (sawMessageStart && !sawMessageEnd) { - throw new Error("Anthropic stream ended before message_stop"); - } -} - -export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOptions> = ( - model: Model<"anthropic-messages">, - context: Context, - options?: AnthropicOptions, -): AssistantMessageEventStream => { - const stream = new AssistantMessageEventStream(); - - (async () => { - const output: AssistantMessage = { - role: "assistant", - content: [], - api: model.api as 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(), - }; - - try { - let client: Anthropic; - let isOAuth: boolean; - - if (options?.client) { - client = options.client; - isOAuth = false; - } else { - const apiKey = options?.apiKey; - if (!apiKey) { - throw new Error(`No API key for provider: ${model.provider}`); - } - - let copilotDynamicHeaders: Record | undefined; - if (model.provider === "github-copilot") { - const hasImages = hasCopilotVisionInput(context.messages); - copilotDynamicHeaders = buildCopilotDynamicHeaders({ - messages: context.messages, - hasImages, - }); - } - - const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env); - const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId; - - const created = createClient( - model, - apiKey, - options?.interleavedThinking ?? true, - shouldUseFineGrainedToolStreamingBeta(model, context), - options?.headers, - copilotDynamicHeaders, - cacheSessionId, - options?.env, - ); - client = created.client; - isOAuth = created.isOAuthToken; - } - let params = buildParams(model, context, isOAuth, options); - const nextParams = await options?.onPayload?.(params, model); - if (nextParams !== undefined) { - params = nextParams as MessageCreateParamsStreaming; - } - const requestOptions = { - ...(options?.signal ? { signal: options.signal } : {}), - ...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}), - maxRetries: options?.maxRetries ?? 0, - }; - const response = await client.messages.create({ ...params, stream: true }, requestOptions).asResponse(); - await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model); - stream.push({ type: "start", partial: output }); - - type Block = (ThinkingContent | TextContent | (ToolCall & { partialJson: string })) & { index: number }; - const blocks = output.content as Block[]; - - for await (const event of iterateAnthropicEvents(response, options?.signal)) { - if (event.type === "message_start") { - output.responseId = event.message.id; - // Capture initial token usage from message_start event - // This ensures we have input token counts even if the stream is aborted early - output.usage.input = event.message.usage.input_tokens || 0; - output.usage.output = event.message.usage.output_tokens || 0; - output.usage.cacheRead = event.message.usage.cache_read_input_tokens || 0; - output.usage.cacheWrite = event.message.usage.cache_creation_input_tokens || 0; - output.usage.cacheWrite1h = event.message.usage.cache_creation?.ephemeral_1h_input_tokens || 0; - // Anthropic doesn't provide total_tokens, compute from components - output.usage.totalTokens = - output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite; - calculateCost(model, output.usage); - } else if (event.type === "content_block_start") { - if (event.content_block.type === "text") { - const block: Block = { - type: "text", - text: "", - index: event.index, - }; - output.content.push(block); - stream.push({ type: "text_start", contentIndex: output.content.length - 1, partial: output }); - } else if (event.content_block.type === "thinking") { - const block: Block = { - type: "thinking", - thinking: "", - thinkingSignature: "", - index: event.index, - }; - output.content.push(block); - stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output }); - } else if (event.content_block.type === "redacted_thinking") { - const block: Block = { - type: "thinking", - thinking: "[Reasoning redacted]", - thinkingSignature: event.content_block.data, - redacted: true, - index: event.index, - }; - output.content.push(block); - stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output }); - } else if (event.content_block.type === "tool_use") { - const block: Block = { - type: "toolCall", - id: event.content_block.id, - name: isOAuth - ? fromClaudeCodeName(event.content_block.name, context.tools) - : event.content_block.name, - arguments: (event.content_block.input as Record) ?? {}, - partialJson: "", - index: event.index, - }; - output.content.push(block); - stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output }); - } - } else if (event.type === "content_block_delta") { - if (event.delta.type === "text_delta") { - const index = blocks.findIndex((b) => b.index === event.index); - const block = blocks[index]; - if (block && block.type === "text") { - block.text += event.delta.text; - stream.push({ - type: "text_delta", - contentIndex: index, - delta: event.delta.text, - partial: output, - }); - } - } else if (event.delta.type === "thinking_delta") { - const index = blocks.findIndex((b) => b.index === event.index); - const block = blocks[index]; - if (block && block.type === "thinking") { - block.thinking += event.delta.thinking; - stream.push({ - type: "thinking_delta", - contentIndex: index, - delta: event.delta.thinking, - partial: output, - }); - } - } else if (event.delta.type === "input_json_delta") { - const index = blocks.findIndex((b) => b.index === event.index); - const block = blocks[index]; - if (block && block.type === "toolCall") { - block.partialJson += event.delta.partial_json; - block.arguments = parseStreamingJson(block.partialJson); - stream.push({ - type: "toolcall_delta", - contentIndex: index, - delta: event.delta.partial_json, - partial: output, - }); - } - } else if (event.delta.type === "signature_delta") { - const index = blocks.findIndex((b) => b.index === event.index); - const block = blocks[index]; - if (block && block.type === "thinking") { - block.thinkingSignature = block.thinkingSignature || ""; - block.thinkingSignature += event.delta.signature; - } - } - } else if (event.type === "content_block_stop") { - const index = blocks.findIndex((b) => b.index === event.index); - const block = blocks[index]; - if (block) { - delete (block as any).index; - if (block.type === "text") { - stream.push({ - type: "text_end", - contentIndex: index, - content: block.text, - partial: output, - }); - } else if (block.type === "thinking") { - stream.push({ - type: "thinking_end", - contentIndex: index, - content: block.thinking, - partial: output, - }); - } else if (block.type === "toolCall") { - block.arguments = parseStreamingJson(block.partialJson); - // Finalize in-place and strip the scratch buffer so replay only - // carries parsed arguments. - delete (block as { partialJson?: string }).partialJson; - stream.push({ - type: "toolcall_end", - contentIndex: index, - toolCall: block, - partial: output, - }); - } - } - } else if (event.type === "message_delta") { - if (event.delta.stop_reason) { - const stopReasonResult = mapStopReason(event.delta.stop_reason, event.delta.stop_details); - output.stopReason = stopReasonResult.stopReason; - if (stopReasonResult.errorMessage) { - output.errorMessage = stopReasonResult.errorMessage; - } - } - // Only update usage fields if present (not null). - // Preserves input_tokens from message_start when proxies omit it in message_delta. - if (event.usage.input_tokens != null) { - output.usage.input = event.usage.input_tokens; - } - if (event.usage.output_tokens != null) { - output.usage.output = event.usage.output_tokens; - } - if (event.usage.cache_read_input_tokens != null) { - output.usage.cacheRead = event.usage.cache_read_input_tokens; - } - if (event.usage.cache_creation_input_tokens != null) { - output.usage.cacheWrite = event.usage.cache_creation_input_tokens; - } - // Anthropic doesn't provide total_tokens, compute from components - output.usage.totalTokens = - output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite; - calculateCost(model, output.usage); - } - } - - if (options?.signal?.aborted) { - throw new Error("Request was aborted"); - } - - if (output.stopReason === "aborted" || output.stopReason === "error") { - throw new Error(output.errorMessage || "An unknown error occurred"); - } - - stream.push({ type: "done", reason: output.stopReason, message: output }); - stream.end(); - } catch (error) { - for (const block of output.content) { - delete (block as { index?: number }).index; - // partialJson is only a streaming scratch buffer; never persist it. - delete (block as { partialJson?: string }).partialJson; - } - output.stopReason = options?.signal?.aborted ? "aborted" : "error"; - output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error); - stream.push({ type: "error", reason: output.stopReason, error: output }); - stream.end(); - } - })(); - - return stream; -}; - -/** - * Map ThinkingLevel to Anthropic effort levels for adaptive thinking. - * Note: effort "max" is only valid on Opus 4.6, while Opus 4.7+ and Fable 5 support "xhigh". - */ -function mapThinkingLevelToEffort( - model: Model<"anthropic-messages">, - level: SimpleStreamOptions["reasoning"], -): AnthropicEffort { - const mapped = level ? model.thinkingLevelMap?.[level] : undefined; - if (typeof mapped === "string") return mapped as AnthropicEffort; - - switch (level) { - case "minimal": - case "low": - return "low"; - case "medium": - return "medium"; - case "high": - return "high"; - default: - return "high"; - } -} - -export const streamSimpleAnthropic: StreamFunction<"anthropic-messages", SimpleStreamOptions> = ( - model: Model<"anthropic-messages">, - context: Context, - options?: SimpleStreamOptions, -): AssistantMessageEventStream => { - const apiKey = options?.apiKey; - if (!apiKey) { - throw new Error(`No API key for provider: ${model.provider}`); - } - - const base = buildBaseOptions(model, options, apiKey); - if (!options?.reasoning) { - return streamAnthropic(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, { - ...base, - thinkingEnabled: true, - effort, - } satisfies AnthropicOptions); - } - - // Undefined means the caller did not request an output cap; let the helper use the model cap. - // Do not coerce to 0 here, or the thinking budget would become the entire max_tokens value. - const adjusted = adjustMaxTokensForThinking( - base.maxTokens, - model.maxTokens, - options.reasoning, - options.thinkingBudgets, - ); - - return streamAnthropic(model, context, { - ...base, - maxTokens: adjusted.maxTokens, - thinkingEnabled: true, - thinkingBudgetTokens: adjusted.thinkingBudget, - } satisfies AnthropicOptions); -}; - -function isOAuthToken(apiKey: string): boolean { - return apiKey.includes("sk-ant-oat"); -} - -function createClient( - model: Model<"anthropic-messages">, - apiKey: string, - interleavedThinking: boolean, - useFineGrainedToolStreamingBeta: boolean, - optionsHeaders?: Record, - 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; - const betaFeatures: string[] = []; - if (useFineGrainedToolStreamingBeta) { - betaFeatures.push(FINE_GRAINED_TOOL_STREAMING_BETA); - } - if (needsInterleavedBeta) { - 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, - baseURL: model.baseUrl, - dangerouslyAllowBrowser: true, - defaultHeaders: mergeHeaders( - { - accept: "application/json", - "anthropic-dangerous-direct-browser-access": "true", - ...(betaFeatures.length > 0 ? { "anthropic-beta": betaFeatures.join(",") } : {}), - }, - model.headers, - dynamicHeaders, - optionsHeaders, - ), - }); - - return { client, isOAuthToken: false }; - } - - // OAuth: Bearer auth, Claude Code identity headers - if (isOAuthToken(apiKey)) { - const client = new Anthropic({ - apiKey: null, - authToken: apiKey, - baseURL: model.baseUrl, - dangerouslyAllowBrowser: true, - defaultHeaders: mergeHeaders( - { - accept: "application/json", - "anthropic-dangerous-direct-browser-access": "true", - "anthropic-beta": ["claude-code-20250219", "oauth-2025-04-20", ...betaFeatures].join(","), - "user-agent": `claude-cli/${claudeCodeVersion}`, - "x-app": "cli", - }, - model.headers, - optionsHeaders, - ), - }); - - return { client, isOAuthToken: true }; - } - - // API key auth - const sessionAffinityHeaders: Record = - sessionId && getAnthropicCompat(model).sendSessionAffinityHeaders ? { "x-session-affinity": sessionId } : {}; - const client = new Anthropic({ - apiKey, - 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, - ), - }); - - return { client, isOAuthToken: false }; -} - -function buildParams( - model: Model<"anthropic-messages">, - context: Context, - isOAuthToken: boolean, - options?: AnthropicOptions, -): MessageCreateParamsStreaming { - const { cacheControl } = getCacheControl(model, options?.cacheRetention, options?.env); - const compat = getAnthropicCompat(model); - const params: MessageCreateParamsStreaming = { - model: model.id, - messages: convertMessages(context.messages, model, isOAuthToken, cacheControl, compat.allowEmptySignature), - max_tokens: options?.maxTokens ?? model.maxTokens, - stream: true, - }; - - // For OAuth tokens, we MUST include Claude Code identity - if (isOAuthToken) { - params.system = [ - { - type: "text", - text: "You are Claude Code, Anthropic's official CLI for Claude.", - ...(cacheControl ? { cache_control: cacheControl } : {}), - }, - ]; - if (context.systemPrompt) { - params.system.push({ - type: "text", - text: sanitizeSurrogates(context.systemPrompt), - ...(cacheControl ? { cache_control: cacheControl } : {}), - }); - } - } else if (context.systemPrompt) { - // Add cache control to system prompt for non-OAuth tokens - params.system = [ - { - type: "text", - text: sanitizeSurrogates(context.systemPrompt), - ...(cacheControl ? { cache_control: cacheControl } : {}), - }, - ]; - } - - // Temperature is incompatible with extended thinking and unsupported on Claude Opus 4.7+. - if (options?.temperature !== undefined && !options?.thinkingEnabled && compat.supportsTemperature) { - params.temperature = options.temperature; - } - - if (context.tools && context.tools.length > 0) { - params.tools = convertTools( - context.tools, - isOAuthToken, - compat.supportsEagerToolInputStreaming, - compat.supportsCacheControlOnTools ? cacheControl : undefined, - ); - } - - // Configure thinking mode: adaptive, budget-based, or explicitly disabled. - if (model.reasoning) { - if (options?.thinkingEnabled) { - // Default to "summarized" so Opus 4.7 and Mythos Preview behave like - // older Claude 4 models (whose API default is also "summarized"). - const display: AnthropicThinkingDisplay = options.thinkingDisplay ?? "summarized"; - if (model.compat?.forceAdaptiveThinking === true) { - // Adaptive thinking: Claude decides when and how much to think. - params.thinking = { type: "adaptive", display }; - if (options.effort) { - // The Anthropic SDK types can lag newly supported effort values such as "xhigh". - params.output_config = - options.effort === "xhigh" - ? ({ effort: options.effort } as unknown as NonNullable< - MessageCreateParamsStreaming["output_config"] - >) - : { effort: options.effort }; - } - } else { - // Budget-based thinking for older models - params.thinking = { - type: "enabled", - budget_tokens: options.thinkingBudgetTokens || 1024, - display, - }; - } - } else if (options?.thinkingEnabled === false && model.thinkingLevelMap?.off !== null) { - params.thinking = { type: "disabled" }; - } - } - - if (options?.metadata) { - const userId = options.metadata.user_id; - if (typeof userId === "string") { - params.metadata = { user_id: userId }; - } - } - - if (options?.toolChoice) { - if (typeof options.toolChoice === "string") { - params.tool_choice = { type: options.toolChoice }; - } else { - params.tool_choice = options.toolChoice; - } - } - - return params; -} - -// Normalize tool call IDs to match Anthropic's required pattern and length -function normalizeToolCallId(id: string): string { - return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64); -} - -function convertMessages( - messages: Message[], - model: Model<"anthropic-messages">, - isOAuthToken: boolean, - cacheControl?: CacheControlEphemeral, - allowEmptySignature = false, -): MessageParam[] { - const params: MessageParam[] = []; - - // Transform messages for cross-provider compatibility - const transformedMessages = transformMessages(messages, model, normalizeToolCallId); - - for (let i = 0; i < transformedMessages.length; i++) { - const msg = transformedMessages[i]; - - if (msg.role === "user") { - if (typeof msg.content === "string") { - if (msg.content.trim().length > 0) { - params.push({ - role: "user", - content: sanitizeSurrogates(msg.content), - }); - } - } else { - const blocks: ContentBlockParam[] = msg.content.map((item) => { - if (item.type === "text") { - return { - type: "text", - text: sanitizeSurrogates(item.text), - }; - } else { - return { - type: "image", - source: { - type: "base64", - media_type: item.mimeType as "image/jpeg" | "image/png" | "image/gif" | "image/webp", - data: item.data, - }, - }; - } - }); - const filteredBlocks = blocks.filter((b) => { - if (b.type === "text") { - return b.text.trim().length > 0; - } - return true; - }); - if (filteredBlocks.length === 0) continue; - params.push({ - role: "user", - content: filteredBlocks, - }); - } - } else if (msg.role === "assistant") { - const blocks: ContentBlockParam[] = []; - - for (const block of msg.content) { - if (block.type === "text") { - if (block.text.trim().length === 0) continue; - blocks.push({ - type: "text", - text: sanitizeSurrogates(block.text), - }); - } else if (block.type === "thinking") { - // Redacted thinking: pass the opaque payload back as redacted_thinking - if (block.redacted) { - blocks.push({ - type: "redacted_thinking", - data: block.thinkingSignature!, - }); - continue; - } - if (block.thinking.trim().length === 0) continue; - // If thinking signature is missing/empty (e.g., from aborted stream), - // convert to plain text for Anthropic. Some compatible providers emit - // and accept empty signatures, so let marked models preserve the block. - if (!block.thinkingSignature || block.thinkingSignature.trim().length === 0) { - blocks.push( - allowEmptySignature - ? { - type: "thinking", - thinking: sanitizeSurrogates(block.thinking), - signature: "", - } - : { - type: "text", - text: sanitizeSurrogates(block.thinking), - }, - ); - } else { - blocks.push({ - type: "thinking", - thinking: sanitizeSurrogates(block.thinking), - signature: block.thinkingSignature, - }); - } - } else if (block.type === "toolCall") { - blocks.push({ - type: "tool_use", - id: block.id, - name: isOAuthToken ? toClaudeCodeName(block.name) : block.name, - input: block.arguments ?? {}, - }); - } - } - if (blocks.length === 0) continue; - params.push({ - role: "assistant", - content: blocks, - }); - } else if (msg.role === "toolResult") { - // Collect all consecutive toolResult messages, needed for z.ai Anthropic endpoint - const toolResults: ContentBlockParam[] = []; - - // Add the current tool result - toolResults.push({ - type: "tool_result", - tool_use_id: msg.toolCallId, - content: convertContentBlocks(msg.content), - is_error: msg.isError, - }); - - // Look ahead for consecutive toolResult messages - let j = i + 1; - while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") { - const nextMsg = transformedMessages[j] as ToolResultMessage; // We know it's a toolResult - toolResults.push({ - type: "tool_result", - tool_use_id: nextMsg.toolCallId, - content: convertContentBlocks(nextMsg.content), - is_error: nextMsg.isError, - }); - j++; - } - - // Skip the messages we've already processed - i = j - 1; - - // Add a single user message with all tool results - params.push({ - role: "user", - content: toolResults, - }); - } - } - - // Add cache_control to the last user message to cache conversation history - if (cacheControl && params.length > 0) { - const lastMessage = params[params.length - 1]; - if (lastMessage.role === "user") { - if (Array.isArray(lastMessage.content)) { - const lastBlock = lastMessage.content[lastMessage.content.length - 1]; - if ( - lastBlock && - (lastBlock.type === "text" || lastBlock.type === "image" || lastBlock.type === "tool_result") - ) { - (lastBlock as any).cache_control = cacheControl; - } - } else if (typeof lastMessage.content === "string") { - lastMessage.content = [ - { - type: "text", - text: lastMessage.content, - cache_control: cacheControl, - }, - ] as any; - } - } - } - - return params; -} - -function shouldUseFineGrainedToolStreamingBeta(model: Model<"anthropic-messages">, context: Context): boolean { - return !!context.tools?.length && !getAnthropicCompat(model).supportsEagerToolInputStreaming; -} - -function convertTools( - tools: Tool[], - isOAuthToken: boolean, - supportsEagerToolInputStreaming: boolean, - cacheControl?: CacheControlEphemeral, -): Anthropic.Messages.Tool[] { - if (!tools) return []; - - return tools.map((tool, index) => { - const schema = tool.parameters as { properties?: unknown; required?: string[] }; - - return { - name: isOAuthToken ? toClaudeCodeName(tool.name) : tool.name, - description: tool.description, - ...(supportsEagerToolInputStreaming ? { eager_input_streaming: true } : {}), - input_schema: { - type: "object", - properties: schema.properties ?? {}, - required: schema.required ?? [], - }, - ...(cacheControl && index === tools.length - 1 ? { cache_control: cacheControl } : {}), - }; +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(), }); } - -function mapStopReason( - reason: Anthropic.Messages.StopReason | string, - stopDetails?: RefusalStopDetails | null, -): { stopReason: StopReason; errorMessage?: string } { - switch (reason) { - case "end_turn": - return { stopReason: "stop" }; - case "max_tokens": - return { stopReason: "length" }; - case "tool_use": - return { stopReason: "toolUse" }; - case "refusal": - return { - stopReason: "error", - errorMessage: stopDetails?.explanation || `The model refused to complete the request`, - }; - case "pause_turn": // Stop is good enough -> resubmit - return { stopReason: "stop" }; - case "stop_sequence": - return { stopReason: "stop" }; // We don't supply stop sequences, so this should never happen - case "sensitive": // Content flagged by safety filters (not yet in SDK types) - return { stopReason: "error" }; - default: - // Handle unknown stop reasons gracefully (API may add new values) - throw new Error(`Unhandled stop reason: ${reason}`); - } -} 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 index db1d3fb7..78351dea 100644 --- a/packages/ai/src/providers/azure-openai-responses.ts +++ b/packages/ai/src/providers/azure-openai-responses.ts @@ -1,299 +1,14 @@ -import { AzureOpenAI } from "openai"; -import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js"; -import { clampThinkingLevel } from "../models.ts"; -import type { - Api, - AssistantMessage, - Context, - Model, - SimpleStreamOptions, - StreamFunction, - StreamOptions, -} from "../types.ts"; -import { AssistantMessageEventStream } from "../utils/event-stream.ts"; -import { headersToRecord } from "../utils/headers.ts"; -import { getProviderEnvValue } from "../utils/provider-env.ts"; -import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts"; -import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.ts"; -import { buildBaseOptions } from "./simple-options.ts"; +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"; -const DEFAULT_AZURE_API_VERSION = "v1"; -const AZURE_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode", "azure-openai-responses"]); - -function parseDeploymentNameMap(value: string | undefined): Map { - const map = new Map(); - if (!value) return map; - for (const entry of value.split(",")) { - const trimmed = entry.trim(); - if (!trimmed) continue; - const [modelId, deploymentName] = trimmed.split("=", 2); - if (!modelId || !deploymentName) continue; - map.set(modelId.trim(), deploymentName.trim()); - } - return map; -} - -function resolveDeploymentName(model: Model<"azure-openai-responses">, options?: AzureOpenAIResponsesOptions): string { - if (options?.azureDeploymentName) { - return options.azureDeploymentName; - } - const mappedDeployment = parseDeploymentNameMap( - getProviderEnvValue("AZURE_OPENAI_DEPLOYMENT_NAME_MAP", options?.env), - ).get(model.id); - return mappedDeployment || model.id; -} - -function formatAzureOpenAIError(error: unknown): string { - if (error instanceof Error) { - const status = (error as Error & { status?: unknown }).status; - const statusCode = typeof status === "number" ? status : undefined; - if (statusCode !== undefined) { - return `Azure OpenAI API error (${statusCode}): ${error.message}`; - } - return error.message; - } - try { - return JSON.stringify(error); - } catch { - return String(error); - } -} - -// Azure OpenAI Responses-specific options -export interface AzureOpenAIResponsesOptions extends StreamOptions { - reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh"; - reasoningSummary?: "auto" | "detailed" | "concise" | null; - azureApiVersion?: string; - azureResourceName?: string; - azureBaseUrl?: string; - azureDeploymentName?: string; -} - -/** - * Generate function for Azure OpenAI Responses API - */ -export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses", AzureOpenAIResponsesOptions> = ( - model: Model<"azure-openai-responses">, - context: Context, - options?: AzureOpenAIResponsesOptions, -): AssistantMessageEventStream => { - const stream = new AssistantMessageEventStream(); - - // Start async processing - (async () => { - const deploymentName = resolveDeploymentName(model, options); - - const output: AssistantMessage = { - role: "assistant", - content: [], - api: "azure-openai-responses" as 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(), - }; - - try { - // Create Azure OpenAI client - const apiKey = options?.apiKey; - if (!apiKey) { - throw new Error(`No API key for provider: ${model.provider}`); - } - const client = createClient(model, apiKey, options); - let params = buildParams(model, context, options, deploymentName); - const nextParams = await options?.onPayload?.(params, model); - if (nextParams !== undefined) { - params = nextParams as ResponseCreateParamsStreaming; - } - const requestOptions = { - ...(options?.signal ? { signal: options.signal } : {}), - ...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}), - maxRetries: options?.maxRetries ?? 0, - }; - const { data: openaiStream, response } = await client.responses.create(params, requestOptions).withResponse(); - await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model); - stream.push({ type: "start", partial: output }); - - await processResponsesStream(openaiStream, output, stream, model); - - if (options?.signal?.aborted) { - throw new Error("Request was aborted"); - } - - if (output.stopReason === "aborted" || output.stopReason === "error") { - throw new Error("An unknown error occurred"); - } - - stream.push({ type: "done", reason: output.stopReason, message: output }); - stream.end(); - } catch (error) { - for (const block of output.content) { - delete (block as { index?: number }).index; - // partialJson is only a streaming scratch buffer; never persist it. - delete (block as { partialJson?: string }).partialJson; - } - output.stopReason = options?.signal?.aborted ? "aborted" : "error"; - output.errorMessage = formatAzureOpenAIError(error); - stream.push({ type: "error", reason: output.stopReason, error: output }); - stream.end(); - } - })(); - - return stream; -}; - -export const streamSimpleAzureOpenAIResponses: StreamFunction<"azure-openai-responses", SimpleStreamOptions> = ( - model: Model<"azure-openai-responses">, - context: Context, - options?: SimpleStreamOptions, -): AssistantMessageEventStream => { - const apiKey = options?.apiKey; - if (!apiKey) { - throw new Error(`No API key for provider: ${model.provider}`); - } - - const base = buildBaseOptions(model, options, apiKey); - const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined; - const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning; - - return streamAzureOpenAIResponses(model, context, { - ...base, - reasoningEffort, - } satisfies AzureOpenAIResponsesOptions); -}; - -function normalizeAzureBaseUrl(baseUrl: string): string { - const trimmed = baseUrl.trim().replace(/\/+$/, ""); - let url: URL; - try { - url = new URL(trimmed); - } catch { - throw new Error(`Invalid Azure OpenAI base URL: ${baseUrl}`); - } - - const isAzureHost = - url.hostname.endsWith(".openai.azure.com") || url.hostname.endsWith(".cognitiveservices.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")) { - url.pathname = "/openai/v1"; - url.search = ""; - } - - return url.toString().replace(/\/+$/, ""); -} - -function buildDefaultBaseUrl(resourceName: string): string { - return `https://${resourceName}.openai.azure.com/openai/v1`; -} - -function resolveAzureConfig( - model: Model<"azure-openai-responses">, - options?: AzureOpenAIResponsesOptions, -): { baseUrl: string; apiVersion: string } { - const apiVersion = - options?.azureApiVersion || - getProviderEnvValue("AZURE_OPENAI_API_VERSION", options?.env) || - DEFAULT_AZURE_API_VERSION; - - const baseUrl = - options?.azureBaseUrl?.trim() || getProviderEnvValue("AZURE_OPENAI_BASE_URL", options?.env)?.trim() || undefined; - const resourceName = options?.azureResourceName || getProviderEnvValue("AZURE_OPENAI_RESOURCE_NAME", options?.env); - - let resolvedBaseUrl = baseUrl; - - if (!resolvedBaseUrl && resourceName) { - resolvedBaseUrl = buildDefaultBaseUrl(resourceName); - } - - if (!resolvedBaseUrl && model.baseUrl) { - resolvedBaseUrl = model.baseUrl; - } - - if (!resolvedBaseUrl) { - throw new Error( - "Azure OpenAI base URL is required. Set AZURE_OPENAI_BASE_URL or AZURE_OPENAI_RESOURCE_NAME, or pass azureBaseUrl, azureResourceName, or model.baseUrl.", - ); - } - - return { - baseUrl: normalizeAzureBaseUrl(resolvedBaseUrl), - apiVersion, - }; -} - -function createClient(model: Model<"azure-openai-responses">, apiKey: string, options?: AzureOpenAIResponsesOptions) { - const headers = { ...model.headers }; - - if (options?.headers) { - Object.assign(headers, options.headers); - } - - const { baseUrl, apiVersion } = resolveAzureConfig(model, options); - - return new AzureOpenAI({ - apiKey, - apiVersion, - dangerouslyAllowBrowser: true, - defaultHeaders: headers, - baseURL: baseUrl, +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(), }); } - -function buildParams( - model: Model<"azure-openai-responses">, - context: Context, - options: AzureOpenAIResponsesOptions | undefined, - deploymentName: string, -) { - const messages = convertResponsesMessages(model, context, AZURE_TOOL_CALL_PROVIDERS); - - const params: ResponseCreateParamsStreaming = { - model: deploymentName, - input: messages, - stream: true, - prompt_cache_key: clampOpenAIPromptCacheKey(options?.sessionId), - store: false, - }; - - if (options?.maxTokens) { - params.max_output_tokens = options?.maxTokens; - } - - if (options?.temperature !== undefined) { - params.temperature = options?.temperature; - } - - if (context.tools && context.tools.length > 0) { - params.tools = convertResponsesTools(context.tools); - } - - if (model.reasoning) { - if (options?.reasoningEffort || options?.reasoningSummary) { - const effort = options?.reasoningEffort - ? (model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort) - : "medium"; - params.reasoning = { - effort: effort as NonNullable["effort"], - summary: options?.reasoningSummary || "auto", - }; - params.include = ["reasoning.encrypted_content"]; - } else if (model.thinkingLevelMap?.off !== null) { - params.reasoning = { - effort: (model.thinkingLevelMap?.off ?? "none") as NonNullable["effort"], - }; - } - } - - return params; -} diff --git a/packages/ai/src/providers/cerebras.models.ts b/packages/ai/src/providers/cerebras.models.ts new file mode 100644 index 00000000..e93c8bc3 --- /dev/null +++ b/packages/ai/src/providers/cerebras.models.ts @@ -0,0 +1,41 @@ +// 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.35, + output: 0.75, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 40960, + } 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: true, + input: ["text"], + cost: { + input: 2.25, + output: 2.75, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 40960, + } 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..e3c2ccc6 --- /dev/null +++ b/packages/ai/src/providers/cloudflare-workers-ai.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 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/moonshotai/kimi-k2.7-code": { + id: "@cf/moonshotai/kimi-k2.7-code", + name: "Kimi K2.7 Code", + 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.19, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } 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">, + "@cf/zai-org/glm-5.2": { + id: "@cf/zai-org/glm-5.2", + name: "Glm 5.2", + 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: 1.4, + output: 4.4, + cacheRead: 0.26, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } 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..cb93d846 --- /dev/null +++ b/packages/ai/src/providers/fireworks.models.ts @@ -0,0 +1,278 @@ +// 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.028, + 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/glm-5p2": { + id: "accounts/fireworks/models/glm-5p2", + name: "GLM 5.2", + api: "openai-completions", + provider: "fireworks", + 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, + output: 4.4, + cacheRead: 0.26, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "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-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/kimi-k2p7-code": { + id: "accounts/fireworks/models/kimi-k2p7-code", + name: "Kimi K2.7 Code", + 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.19, + cacheWrite: 0, + }, + contextWindow: 262000, + maxTokens: 262000, + } 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/minimax-m3": { + id: "accounts/fireworks/models/minimax-m3", + name: "MiniMax-M3", + 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: 512000, + maxTokens: 512000, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/qwen3p7-plus": { + id: "accounts/fireworks/models/qwen3p7-plus", + name: "Qwen 3.7 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.4, + output: 1.6, + cacheRead: 0.08, + 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">, + "accounts/fireworks/routers/kimi-k2p7-code-fast": { + id: "accounts/fireworks/routers/kimi-k2p7-code-fast", + name: "Kimi K2.7 Code 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: 1.9, + output: 8, + cacheRead: 0.38, + 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..518fb259 --- /dev/null +++ b/packages/ai/src/providers/fireworks.ts @@ -0,0 +1,19 @@ +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 { FIREWORKS_MODELS } from "./fireworks.models.ts"; + +export function fireworksProvider(): Provider<"anthropic-messages" | "openai-completions"> { + 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: { + "anthropic-messages": anthropicMessagesApi(), + "openai-completions": openAICompletionsApi(), + }, + }); +} 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..cf866ec2 --- /dev/null +++ b/packages/ai/src/providers/github-copilot.models.ts @@ -0,0 +1,428 @@ +// 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-fable-5": { + id: "claude-fable-5", + name: "Claude Fable 5", + 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: 10, + output: 50, + cacheRead: 1, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "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","minimal":"low"}, + 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","minimal":"low"}, + 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, + thinkingLevelMap: {"minimal":"low","xhigh":"max"}, + 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">, +} 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..8dfa2414 --- /dev/null +++ b/packages/ai/src/providers/google-vertex.models.ts @@ -0,0 +1,184 @@ +// 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-2.5-flash": { + id: "gemini-2.5-flash", + name: "Gemini 2.5 Flash", + 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", + 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", + 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", + 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.1-flash-lite": { + id: "gemini-3.1-flash-lite", + name: "Gemini 3.1 Flash Lite", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + 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-vertex">, + "gemini-3.1-pro-preview": { + id: "gemini-3.1-pro-preview", + name: "Gemini 3.1 Pro Preview", + 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", + 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.5-flash": { + id: "gemini-3.5-flash", + name: "Gemini 3.5 Flash", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + 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-vertex">, + "gemini-flash-latest": { + id: "gemini-flash-latest", + name: "Gemini Flash Latest", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + 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-vertex">, + "gemini-flash-lite-latest": { + id: "gemini-flash-lite-latest", + name: "Gemini Flash-Lite Latest", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + 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-vertex">, +} as const; diff --git a/packages/ai/src/providers/google-vertex.ts b/packages/ai/src/providers/google-vertex.ts index aa971959..af84fc70 100644 --- a/packages/ai/src/providers/google-vertex.ts +++ b/packages/ai/src/providers/google-vertex.ts @@ -1,582 +1,38 @@ -import { - type GenerateContentConfig, - type GenerateContentParameters, - GoogleGenAI, - type HttpOptions, - ResourceScope, - type ThinkingConfig, - ThinkingLevel, -} from "@google/genai"; -import { calculateCost, clampThinkingLevel } from "../models.ts"; -import type { - Api, - AssistantMessage, - Context, - Model, - ThinkingLevel as PiThinkingLevel, - ProviderEnv, - SimpleStreamOptions, - StreamFunction, - StreamOptions, - TextContent, - ThinkingBudgets, - ThinkingContent, - ToolCall, -} from "../types.ts"; -import { AssistantMessageEventStream } from "../utils/event-stream.ts"; -import { getProviderEnvValue } from "../utils/provider-env.ts"; -import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; -import type { GoogleThinkingLevel } from "./google-shared.ts"; -import { - convertMessages, - convertTools, - isThinkingPart, - mapStopReason, - mapToolChoice, - retainThoughtSignature, -} from "./google-shared.ts"; -import { buildBaseOptions } from "./simple-options.ts"; +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"; -export interface GoogleVertexOptions extends StreamOptions { - toolChoice?: "auto" | "none" | "any"; - thinking?: { - enabled: boolean; - budgetTokens?: number; // -1 for dynamic, 0 to disable - level?: GoogleThinkingLevel; - }; - project?: string; - location?: string; -} +const VERTEX_ADC_PATH = "~/.config/gcloud/application_default_credentials.json"; -const API_VERSION = "v1"; -const GCP_VERTEX_CREDENTIALS_MARKER = "gcp-vertex-credentials"; +/** + * 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 THINKING_LEVEL_MAP: Record = { - THINKING_LEVEL_UNSPECIFIED: ThinkingLevel.THINKING_LEVEL_UNSPECIFIED, - MINIMAL: ThinkingLevel.MINIMAL, - LOW: ThinkingLevel.LOW, - MEDIUM: ThinkingLevel.MEDIUM, - HIGH: ThinkingLevel.HIGH, -}; - -// Counter for generating unique tool call IDs -let toolCallCounter = 0; - -export const streamGoogleVertex: StreamFunction<"google-vertex", GoogleVertexOptions> = ( - model: Model<"google-vertex">, - context: Context, - options?: GoogleVertexOptions, -): AssistantMessageEventStream => { - const stream = new AssistantMessageEventStream(); - - (async () => { - const output: AssistantMessage = { - role: "assistant", - content: [], - api: "google-vertex" as 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(), - }; - - try { - const apiKey = resolveApiKey(options); - // Create the client using either a Vertex API key, if provided, or ADC with project and location - const client = apiKey - ? createClientWithApiKey(model, apiKey, options?.headers) - : createClient(model, resolveProject(options), resolveLocation(options), options?.headers, options?.env); - let params = buildParams(model, context, options); - const nextParams = await options?.onPayload?.(params, model); - if (nextParams !== undefined) { - params = nextParams as GenerateContentParameters; - } - const googleStream = await client.models.generateContentStream(params); - - stream.push({ type: "start", partial: output }); - let currentBlock: TextContent | ThinkingContent | null = null; - const blocks = output.content; - const blockIndex = () => blocks.length - 1; - for await (const chunk of googleStream) { - // Vertex uses the same @google/genai GenerateContentResponse type as Gemini. - // responseId is documented there as an output-only identifier for each response. - output.responseId ||= chunk.responseId; - const candidate = chunk.candidates?.[0]; - if (candidate?.content?.parts) { - for (const part of candidate.content.parts) { - if (part.text !== undefined) { - const isThinking = isThinkingPart(part); - if ( - !currentBlock || - (isThinking && currentBlock.type !== "thinking") || - (!isThinking && currentBlock.type !== "text") - ) { - if (currentBlock) { - if (currentBlock.type === "text") { - stream.push({ - type: "text_end", - contentIndex: blocks.length - 1, - content: currentBlock.text, - partial: output, - }); - } else { - stream.push({ - type: "thinking_end", - contentIndex: blockIndex(), - content: currentBlock.thinking, - partial: output, - }); - } - } - if (isThinking) { - currentBlock = { type: "thinking", thinking: "", thinkingSignature: undefined }; - output.content.push(currentBlock); - stream.push({ type: "thinking_start", contentIndex: blockIndex(), partial: output }); - } else { - currentBlock = { type: "text", text: "" }; - output.content.push(currentBlock); - stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output }); - } - } - if (currentBlock.type === "thinking") { - currentBlock.thinking += part.text; - currentBlock.thinkingSignature = retainThoughtSignature( - currentBlock.thinkingSignature, - part.thoughtSignature, - ); - stream.push({ - type: "thinking_delta", - contentIndex: blockIndex(), - delta: part.text, - partial: output, - }); - } else { - currentBlock.text += part.text; - currentBlock.textSignature = retainThoughtSignature( - currentBlock.textSignature, - part.thoughtSignature, - ); - stream.push({ - type: "text_delta", - contentIndex: blockIndex(), - delta: part.text, - partial: output, - }); - } - } - - if (part.functionCall) { - if (currentBlock) { - if (currentBlock.type === "text") { - stream.push({ - type: "text_end", - contentIndex: blockIndex(), - content: currentBlock.text, - partial: output, - }); - } else { - stream.push({ - type: "thinking_end", - contentIndex: blockIndex(), - content: currentBlock.thinking, - partial: output, - }); - } - currentBlock = null; - } - - const providedId = part.functionCall.id; - const needsNewId = - !providedId || output.content.some((b) => b.type === "toolCall" && b.id === providedId); - const toolCallId = needsNewId - ? `${part.functionCall.name}_${Date.now()}_${++toolCallCounter}` - : providedId; - - const toolCall: ToolCall = { - type: "toolCall", - id: toolCallId, - name: part.functionCall.name || "", - arguments: (part.functionCall.args as Record) ?? {}, - ...(part.thoughtSignature && { thoughtSignature: part.thoughtSignature }), - }; - - output.content.push(toolCall); - stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output }); - stream.push({ - type: "toolcall_delta", - contentIndex: blockIndex(), - delta: JSON.stringify(toolCall.arguments), - partial: output, - }); - stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output }); - } - } - } - - if (candidate?.finishReason) { - output.stopReason = mapStopReason(candidate.finishReason); - if (output.content.some((b) => b.type === "toolCall")) { - output.stopReason = "toolUse"; - } - } - - if (chunk.usageMetadata) { - output.usage = { - input: - (chunk.usageMetadata.promptTokenCount || 0) - (chunk.usageMetadata.cachedContentTokenCount || 0), - output: - (chunk.usageMetadata.candidatesTokenCount || 0) + (chunk.usageMetadata.thoughtsTokenCount || 0), - cacheRead: chunk.usageMetadata.cachedContentTokenCount || 0, - cacheWrite: 0, - totalTokens: chunk.usageMetadata.totalTokenCount || 0, - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - total: 0, - }, - }; - calculateCost(model, output.usage); - } - } - - if (currentBlock) { - if (currentBlock.type === "text") { - stream.push({ - type: "text_end", - contentIndex: blockIndex(), - content: currentBlock.text, - partial: output, - }); - } else { - stream.push({ - type: "thinking_end", - contentIndex: blockIndex(), - content: currentBlock.thinking, - partial: output, - }); - } - } - - if (options?.signal?.aborted) { - throw new Error("Request was aborted"); - } - - if (output.stopReason === "aborted" || output.stopReason === "error") { - throw new Error("An unknown error occurred"); - } - - stream.push({ type: "done", reason: output.stopReason, message: output }); - stream.end(); - } catch (error) { - // Remove internal index property used during streaming - for (const block of output.content) { - if ("index" in block) { - delete (block as { index?: number }).index; - } - } - output.stopReason = options?.signal?.aborted ? "aborted" : "error"; - output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error); - stream.push({ type: "error", reason: output.stopReason, error: output }); - stream.end(); + 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 stream; + return undefined; + }, }; -export const streamSimpleGoogleVertex: 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, { - ...base, - thinking: { enabled: false }, - } satisfies GoogleVertexOptions); - } - - const clampedReasoning = clampThinkingLevel(model, options.reasoning); - const effort = (clampedReasoning === "off" ? "high" : clampedReasoning) as ClampedThinkingLevel; - const geminiModel = model as unknown as Model<"google-generative-ai">; - - if (isGemini3ProModel(geminiModel) || isGemini3FlashModel(geminiModel)) { - return streamGoogleVertex(model, context, { - ...base, - thinking: { - enabled: true, - level: getGemini3ThinkingLevel(effort, geminiModel), - }, - } satisfies GoogleVertexOptions); - } - - return streamGoogleVertex(model, context, { - ...base, - thinking: { - enabled: true, - budgetTokens: getGoogleBudget(geminiModel, effort, options.thinkingBudgets), - }, - } satisfies GoogleVertexOptions); -}; - -function createClient( - model: Model<"google-vertex">, - project: string, - location: string, - optionsHeaders?: Record, - env?: ProviderEnv, -): GoogleGenAI { - const googleAuthOptions = buildGoogleAuthOptions(env); - return new GoogleGenAI({ - vertexai: true, - project, - location, - apiVersion: API_VERSION, - ...(googleAuthOptions ? { googleAuthOptions } : {}), - httpOptions: buildHttpOptions(model, optionsHeaders), +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(), }); } - -function createClientWithApiKey( - model: Model<"google-vertex">, - apiKey: string, - optionsHeaders?: Record, -): GoogleGenAI { - return new GoogleGenAI({ - vertexai: true, - apiKey, - apiVersion: API_VERSION, - httpOptions: buildHttpOptions(model, optionsHeaders), - }); -} - -function buildHttpOptions( - model: Model<"google-vertex">, - optionsHeaders?: Record, -): HttpOptions | undefined { - const httpOptions: HttpOptions = {}; - const baseUrl = resolveCustomBaseUrl(model.baseUrl); - if (baseUrl) { - httpOptions.baseUrl = baseUrl; - httpOptions.baseUrlResourceScope = ResourceScope.COLLECTION; - if (baseUrlIncludesApiVersion(baseUrl)) { - httpOptions.apiVersion = ""; - } - } - - if (model.headers || optionsHeaders) { - httpOptions.headers = { ...model.headers, ...optionsHeaders }; - } - - return Object.keys(httpOptions).length > 0 ? httpOptions : undefined; -} - -function resolveCustomBaseUrl(baseUrl: string): string | undefined { - const trimmed = baseUrl.trim(); - if (!trimmed || trimmed.includes("{location}")) { - return undefined; - } - return trimmed; -} - -function baseUrlIncludesApiVersion(baseUrl: string): boolean { - try { - const url = new URL(baseUrl); - return url.pathname.split("/").some((part) => /^v\d+(?:beta\d*)?$/.test(part)); - } catch { - return /(?:^|\/)v\d+(?:beta\d*)?(?:\/|$)/.test(baseUrl); - } -} - -function buildGoogleAuthOptions(env?: ProviderEnv): { keyFilename: string } | undefined { - const keyFilename = getProviderEnvValue("GOOGLE_APPLICATION_CREDENTIALS", env); - return keyFilename ? { keyFilename } : undefined; -} - -function resolveApiKey(options?: GoogleVertexOptions): string | undefined { - const apiKey = options?.apiKey?.trim(); - if (!apiKey || apiKey === GCP_VERTEX_CREDENTIALS_MARKER || isPlaceholderApiKey(apiKey)) { - return undefined; - } - return apiKey; -} - -function isPlaceholderApiKey(apiKey: string): boolean { - return /^<[^>]+>$/.test(apiKey); -} - -function resolveProject(options?: GoogleVertexOptions): string { - const project = - options?.project || - getProviderEnvValue("GOOGLE_CLOUD_PROJECT", options?.env) || - getProviderEnvValue("GCLOUD_PROJECT", options?.env); - if (!project) { - throw new Error( - "Vertex AI requires a project ID. Set GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT or pass project in options.", - ); - } - return project; -} - -function resolveLocation(options?: GoogleVertexOptions): string { - const location = options?.location || getProviderEnvValue("GOOGLE_CLOUD_LOCATION", options?.env); - if (!location) { - throw new Error("Vertex AI requires a location. Set GOOGLE_CLOUD_LOCATION or pass location in options."); - } - return location; -} - -function buildParams( - model: Model<"google-vertex">, - context: Context, - options: GoogleVertexOptions = {}, -): GenerateContentParameters { - const contents = convertMessages(model, context); - - const generationConfig: GenerateContentConfig = {}; - if (options.temperature !== undefined) { - generationConfig.temperature = options.temperature; - } - if (options.maxTokens !== undefined) { - generationConfig.maxOutputTokens = options.maxTokens; - } - - const config: GenerateContentConfig = { - ...(Object.keys(generationConfig).length > 0 && generationConfig), - ...(context.systemPrompt && { systemInstruction: sanitizeSurrogates(context.systemPrompt) }), - ...(context.tools && context.tools.length > 0 && { tools: convertTools(context.tools) }), - }; - - if (context.tools && context.tools.length > 0 && options.toolChoice) { - config.toolConfig = { - functionCallingConfig: { - mode: mapToolChoice(options.toolChoice), - }, - }; - } else { - config.toolConfig = undefined; - } - - if (options.thinking?.enabled && model.reasoning) { - const thinkingConfig: ThinkingConfig = { includeThoughts: true }; - if (options.thinking.level !== undefined) { - thinkingConfig.thinkingLevel = THINKING_LEVEL_MAP[options.thinking.level]; - } else if (options.thinking.budgetTokens !== undefined) { - thinkingConfig.thinkingBudget = options.thinking.budgetTokens; - } - config.thinkingConfig = thinkingConfig; - } else if (model.reasoning && options.thinking && !options.thinking.enabled) { - config.thinkingConfig = getDisabledThinkingConfig(model); - } - - if (options.signal) { - if (options.signal.aborted) { - throw new Error("Request aborted"); - } - config.abortSignal = options.signal; - } - - const params: GenerateContentParameters = { - model: model.id, - contents, - config, - }; - - return params; -} - -type ClampedThinkingLevel = Exclude; - -function isGemini3ProModel(model: Model<"google-generative-ai">): boolean { - return /gemini-3(?:\.\d+)?-pro/.test(model.id.toLowerCase()); -} - -function isGemini3FlashModel(model: Model<"google-generative-ai">): boolean { - const id = model.id.toLowerCase(); - return /gemini-3(?:\.\d+)?-flash/.test(id) || id === "gemini-flash-latest" || id === "gemini-flash-lite-latest"; -} - -function getDisabledThinkingConfig(model: Model<"google-vertex">): ThinkingConfig { - // Google docs: Gemini 3.1 Pro cannot disable thinking, and Gemini 3 Flash / Flash-Lite - // do not support full thinking-off either. For Gemini 3 models, use the lowest supported - // thinkingLevel without includeThoughts so hidden thinking remains invisible to pi. - const geminiModel = model as unknown as Model<"google-generative-ai">; - if (isGemini3ProModel(geminiModel)) { - return { thinkingLevel: ThinkingLevel.LOW }; - } - if (isGemini3FlashModel(geminiModel)) { - return { thinkingLevel: ThinkingLevel.MINIMAL }; - } - - // Gemini 2.x supports disabling via thinkingBudget = 0. - return { thinkingBudget: 0 }; -} - -function getGemini3ThinkingLevel( - effort: ClampedThinkingLevel, - model: Model<"google-generative-ai">, -): GoogleThinkingLevel { - if (isGemini3ProModel(model)) { - switch (effort) { - case "minimal": - case "low": - return "LOW"; - case "medium": - case "high": - return "HIGH"; - } - } - switch (effort) { - case "minimal": - return "MINIMAL"; - case "low": - return "LOW"; - case "medium": - return "MEDIUM"; - case "high": - return "HIGH"; - } -} - -function getGoogleBudget( - model: Model<"google-generative-ai">, - effort: ClampedThinkingLevel, - customBudgets?: ThinkingBudgets, -): number { - if (customBudgets?.[effort] !== undefined) { - return customBudgets[effort]!; - } - - if (model.id.includes("2.5-pro")) { - const budgets: Record = { - minimal: 128, - low: 2048, - medium: 8192, - high: 32768, - }; - return budgets[effort]; - } - - if (model.id.includes("2.5-flash")) { - const budgets: Record = { - minimal: 128, - low: 2048, - medium: 8192, - high: 24576, - }; - return budgets[effort]; - } - - return -1; -} diff --git a/packages/ai/src/providers/google.models.ts b/packages/ai/src/providers/google.models.ts new file mode 100644 index 00000000..334e3b43 --- /dev/null +++ b/packages/ai/src/providers/google.models.ts @@ -0,0 +1,290 @@ +// 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, + 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-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, + 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">, + "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 index a270792a..0bd45237 100644 --- a/packages/ai/src/providers/google.ts +++ b/packages/ai/src/providers/google.ts @@ -1,504 +1,15 @@ -import { - type GenerateContentConfig, - type GenerateContentParameters, - GoogleGenAI, - type ThinkingConfig, -} from "@google/genai"; -import { calculateCost, clampThinkingLevel } from "../models.ts"; -import type { - Api, - AssistantMessage, - Context, - Model, - SimpleStreamOptions, - StreamFunction, - StreamOptions, - TextContent, - ThinkingBudgets, - ThinkingContent, - ThinkingLevel, - ToolCall, -} from "../types.ts"; -import { AssistantMessageEventStream } from "../utils/event-stream.ts"; -import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; -import type { GoogleThinkingLevel } from "./google-shared.ts"; -import { - convertMessages, - convertTools, - isThinkingPart, - mapStopReason, - mapToolChoice, - retainThoughtSignature, -} from "./google-shared.ts"; -import { buildBaseOptions } from "./simple-options.ts"; +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 interface GoogleOptions extends StreamOptions { - toolChoice?: "auto" | "none" | "any"; - thinking?: { - enabled: boolean; - budgetTokens?: number; // -1 for dynamic, 0 to disable - level?: GoogleThinkingLevel; - }; -} - -// Counter for generating unique tool call IDs -let toolCallCounter = 0; - -export const streamGoogle: StreamFunction<"google-generative-ai", GoogleOptions> = ( - model: Model<"google-generative-ai">, - context: Context, - options?: GoogleOptions, -): AssistantMessageEventStream => { - const stream = new AssistantMessageEventStream(); - - (async () => { - const output: AssistantMessage = { - role: "assistant", - content: [], - api: "google-generative-ai" as 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(), - }; - - try { - const apiKey = options?.apiKey; - if (!apiKey) { - throw new Error(`No API key for provider: ${model.provider}`); - } - const client = createClient(model, apiKey, options?.headers); - let params = buildParams(model, context, options); - const nextParams = await options?.onPayload?.(params, model); - if (nextParams !== undefined) { - params = nextParams as GenerateContentParameters; - } - const googleStream = await client.models.generateContentStream(params); - - stream.push({ type: "start", partial: output }); - let currentBlock: TextContent | ThinkingContent | null = null; - const blocks = output.content; - const blockIndex = () => blocks.length - 1; - for await (const chunk of googleStream) { - // @google/genai documents GenerateContentResponse.responseId as an output-only field - // used to identify each response. Keep the first non-empty one from the stream. - output.responseId ||= chunk.responseId; - const candidate = chunk.candidates?.[0]; - if (candidate?.content?.parts) { - for (const part of candidate.content.parts) { - if (part.text !== undefined) { - const isThinking = isThinkingPart(part); - if ( - !currentBlock || - (isThinking && currentBlock.type !== "thinking") || - (!isThinking && currentBlock.type !== "text") - ) { - if (currentBlock) { - if (currentBlock.type === "text") { - stream.push({ - type: "text_end", - contentIndex: blocks.length - 1, - content: currentBlock.text, - partial: output, - }); - } else { - stream.push({ - type: "thinking_end", - contentIndex: blockIndex(), - content: currentBlock.thinking, - partial: output, - }); - } - } - if (isThinking) { - currentBlock = { type: "thinking", thinking: "", thinkingSignature: undefined }; - output.content.push(currentBlock); - stream.push({ type: "thinking_start", contentIndex: blockIndex(), partial: output }); - } else { - currentBlock = { type: "text", text: "" }; - output.content.push(currentBlock); - stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output }); - } - } - if (currentBlock.type === "thinking") { - currentBlock.thinking += part.text; - currentBlock.thinkingSignature = retainThoughtSignature( - currentBlock.thinkingSignature, - part.thoughtSignature, - ); - stream.push({ - type: "thinking_delta", - contentIndex: blockIndex(), - delta: part.text, - partial: output, - }); - } else { - currentBlock.text += part.text; - currentBlock.textSignature = retainThoughtSignature( - currentBlock.textSignature, - part.thoughtSignature, - ); - stream.push({ - type: "text_delta", - contentIndex: blockIndex(), - delta: part.text, - partial: output, - }); - } - } - - if (part.functionCall) { - if (currentBlock) { - if (currentBlock.type === "text") { - stream.push({ - type: "text_end", - contentIndex: blockIndex(), - content: currentBlock.text, - partial: output, - }); - } else { - stream.push({ - type: "thinking_end", - contentIndex: blockIndex(), - content: currentBlock.thinking, - partial: output, - }); - } - currentBlock = null; - } - - // Generate unique ID if not provided or if it's a duplicate - const providedId = part.functionCall.id; - const needsNewId = - !providedId || output.content.some((b) => b.type === "toolCall" && b.id === providedId); - const toolCallId = needsNewId - ? `${part.functionCall.name}_${Date.now()}_${++toolCallCounter}` - : providedId; - - const toolCall: ToolCall = { - type: "toolCall", - id: toolCallId, - name: part.functionCall.name || "", - arguments: (part.functionCall.args as Record) ?? {}, - ...(part.thoughtSignature && { thoughtSignature: part.thoughtSignature }), - }; - - output.content.push(toolCall); - stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output }); - stream.push({ - type: "toolcall_delta", - contentIndex: blockIndex(), - delta: JSON.stringify(toolCall.arguments), - partial: output, - }); - stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output }); - } - } - } - - if (candidate?.finishReason) { - output.stopReason = mapStopReason(candidate.finishReason); - if (output.content.some((b) => b.type === "toolCall")) { - output.stopReason = "toolUse"; - } - } - - if (chunk.usageMetadata) { - output.usage = { - input: - (chunk.usageMetadata.promptTokenCount || 0) - (chunk.usageMetadata.cachedContentTokenCount || 0), - output: - (chunk.usageMetadata.candidatesTokenCount || 0) + (chunk.usageMetadata.thoughtsTokenCount || 0), - cacheRead: chunk.usageMetadata.cachedContentTokenCount || 0, - cacheWrite: 0, - totalTokens: chunk.usageMetadata.totalTokenCount || 0, - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - total: 0, - }, - }; - calculateCost(model, output.usage); - } - } - - if (currentBlock) { - if (currentBlock.type === "text") { - stream.push({ - type: "text_end", - contentIndex: blockIndex(), - content: currentBlock.text, - partial: output, - }); - } else { - stream.push({ - type: "thinking_end", - contentIndex: blockIndex(), - content: currentBlock.thinking, - partial: output, - }); - } - } - - if (options?.signal?.aborted) { - throw new Error("Request was aborted"); - } - - if (output.stopReason === "aborted" || output.stopReason === "error") { - throw new Error("An unknown error occurred"); - } - - stream.push({ type: "done", reason: output.stopReason, message: output }); - stream.end(); - } catch (error) { - // Remove internal index property used during streaming - for (const block of output.content) { - if ("index" in block) { - delete (block as { index?: number }).index; - } - } - output.stopReason = options?.signal?.aborted ? "aborted" : "error"; - output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error); - stream.push({ type: "error", reason: output.stopReason, error: output }); - stream.end(); - } - })(); - - return stream; -}; - -export const streamSimpleGoogle: StreamFunction<"google-generative-ai", SimpleStreamOptions> = ( - model: Model<"google-generative-ai">, - context: Context, - options?: SimpleStreamOptions, -): AssistantMessageEventStream => { - const apiKey = options?.apiKey; - if (!apiKey) { - throw new Error(`No API key for provider: ${model.provider}`); - } - - const base = buildBaseOptions(model, options, apiKey); - if (!options?.reasoning) { - return streamGoogle(model, context, { ...base, thinking: { enabled: false } } satisfies GoogleOptions); - } - - const clampedReasoning = clampThinkingLevel(model, options.reasoning); - const effort = (clampedReasoning === "off" ? "high" : clampedReasoning) as ClampedThinkingLevel; - const googleModel = model as Model<"google-generative-ai">; - - if (isGemini3ProModel(googleModel) || isGemini3FlashModel(googleModel) || isGemma4Model(googleModel)) { - return streamGoogle(model, context, { - ...base, - thinking: { - enabled: true, - level: getThinkingLevel(effort, googleModel), - }, - } satisfies GoogleOptions); - } - - return streamGoogle(model, context, { - ...base, - thinking: { - enabled: true, - budgetTokens: getGoogleBudget(googleModel, effort, options.thinkingBudgets), - }, - } satisfies GoogleOptions); -}; - -function createClient( - model: Model<"google-generative-ai">, - apiKey?: string, - optionsHeaders?: Record, -): 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 }; - } - - return new GoogleGenAI({ - apiKey, - httpOptions: Object.keys(httpOptions).length > 0 ? httpOptions : undefined, +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(), }); } - -function buildParams( - model: Model<"google-generative-ai">, - context: Context, - options: GoogleOptions = {}, -): GenerateContentParameters { - const contents = convertMessages(model, context); - - const generationConfig: GenerateContentConfig = {}; - if (options.temperature !== undefined) { - generationConfig.temperature = options.temperature; - } - if (options.maxTokens !== undefined) { - generationConfig.maxOutputTokens = options.maxTokens; - } - - const config: GenerateContentConfig = { - ...(Object.keys(generationConfig).length > 0 && generationConfig), - ...(context.systemPrompt && { systemInstruction: sanitizeSurrogates(context.systemPrompt) }), - ...(context.tools && context.tools.length > 0 && { tools: convertTools(context.tools) }), - }; - - if (context.tools && context.tools.length > 0 && options.toolChoice) { - config.toolConfig = { - functionCallingConfig: { - mode: mapToolChoice(options.toolChoice), - }, - }; - } else { - config.toolConfig = undefined; - } - - if (options.thinking?.enabled && model.reasoning) { - const thinkingConfig: ThinkingConfig = { includeThoughts: true }; - if (options.thinking.level !== undefined) { - // Cast to any since our GoogleThinkingLevel mirrors Google's ThinkingLevel enum values - thinkingConfig.thinkingLevel = options.thinking.level as any; - } else if (options.thinking.budgetTokens !== undefined) { - thinkingConfig.thinkingBudget = options.thinking.budgetTokens; - } - config.thinkingConfig = thinkingConfig; - } else if (model.reasoning && options.thinking && !options.thinking.enabled) { - config.thinkingConfig = getDisabledThinkingConfig(model); - } - - if (options.signal) { - if (options.signal.aborted) { - throw new Error("Request aborted"); - } - config.abortSignal = options.signal; - } - - const params: GenerateContentParameters = { - model: model.id, - contents, - config, - }; - - return params; -} - -type ClampedThinkingLevel = Exclude; - -function isGemma4Model(model: Model<"google-generative-ai">): boolean { - return /gemma-?4/.test(model.id.toLowerCase()); -} - -function isGemini3ProModel(model: Model<"google-generative-ai">): boolean { - return /gemini-3(?:\.\d+)?-pro/.test(model.id.toLowerCase()); -} - -function isGemini3FlashModel(model: Model<"google-generative-ai">): boolean { - const id = model.id.toLowerCase(); - return /gemini-3(?:\.\d+)?-flash/.test(id) || id === "gemini-flash-latest" || id === "gemini-flash-lite-latest"; -} - -function getDisabledThinkingConfig(model: Model<"google-generative-ai">): ThinkingConfig { - // Google docs: Gemini 3.1 Pro cannot disable thinking, and Gemini 3 Flash / Flash-Lite - // do not support full thinking-off either. For Gemini 3 models, use the lowest supported - // thinkingLevel without includeThoughts so hidden thinking remains invisible to pi. - if (isGemini3ProModel(model)) { - return { thinkingLevel: "LOW" as any }; - } - if (isGemini3FlashModel(model)) { - return { thinkingLevel: "MINIMAL" as any }; - } - if (isGemma4Model(model)) { - return { thinkingLevel: "MINIMAL" as any }; - } - - // Gemini 2.x supports disabling via thinkingBudget = 0. - return { thinkingBudget: 0 }; -} - -function getThinkingLevel(effort: ClampedThinkingLevel, model: Model<"google-generative-ai">): GoogleThinkingLevel { - if (isGemini3ProModel(model)) { - switch (effort) { - case "minimal": - case "low": - return "LOW"; - case "medium": - case "high": - return "HIGH"; - } - } - if (isGemma4Model(model)) { - switch (effort) { - case "minimal": - case "low": - return "MINIMAL"; - case "medium": - case "high": - return "HIGH"; - } - } - switch (effort) { - case "minimal": - return "MINIMAL"; - case "low": - return "LOW"; - case "medium": - return "MEDIUM"; - case "high": - return "HIGH"; - } -} - -function getGoogleBudget( - model: Model<"google-generative-ai">, - effort: ClampedThinkingLevel, - customBudgets?: ThinkingBudgets, -): number { - if (customBudgets?.[effort] !== undefined) { - return customBudgets[effort]!; - } - - if (model.id.includes("2.5-pro")) { - const budgets: Record = { - minimal: 128, - low: 2048, - medium: 8192, - high: 32768, - }; - return budgets[effort]; - } - - if (model.id.includes("2.5-flash-lite")) { - const budgets: Record = { - minimal: 512, - low: 2048, - medium: 8192, - high: 24576, - }; - return budgets[effort]; - } - - if (model.id.includes("2.5-flash")) { - const budgets: Record = { - minimal: 128, - low: 2048, - medium: 8192, - high: 24576, - }; - return budgets[effort]; - } - - return -1; -} 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/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/kimi-coding.models.ts b/packages/ai/src/providers/kimi-coding.models.ts new file mode 100644 index 00000000..a3b1f266 --- /dev/null +++ b/packages/ai/src/providers/kimi-coding.models.ts @@ -0,0 +1,61 @@ +// 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 = { + "k2p7": { + id: "k2p7", + name: "Kimi K2.7 Code", + 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-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..7060772b --- /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.03, + 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.04, + 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.04, + 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.04, + 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.04, + 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.01, + 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.01, + 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.2, + 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.05, + 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.004, + 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.01, + 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.2, + 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.05, + 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.05, + 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.04, + 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.04, + 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.15, + 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.04, + 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.015, + 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.01, + 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.015, + 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.015, + 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.025, + 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.015, + 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.2, + 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.07, + 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.015, + 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.2, + 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 index 6a132236..9b84a71f 100644 --- a/packages/ai/src/providers/mistral.ts +++ b/packages/ai/src/providers/mistral.ts @@ -1,664 +1,15 @@ -import { Mistral } from "@mistralai/mistralai"; -import type { - ChatCompletionStreamRequest, - ChatCompletionStreamRequestMessage, - CompletionEvent, - ContentChunk, - FunctionTool, -} from "@mistralai/mistralai/models/components"; -import { calculateCost, clampThinkingLevel } from "../models.ts"; -import type { - AssistantMessage, - Context, - Message, - Model, - SimpleStreamOptions, - StopReason, - StreamFunction, - StreamOptions, - TextContent, - ThinkingContent, - Tool, - ToolCall, -} from "../types.ts"; -import { AssistantMessageEventStream } from "../utils/event-stream.ts"; -import { shortHash } from "../utils/hash.ts"; -import { parseStreamingJson } from "../utils/json-parse.ts"; -import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; -import { buildBaseOptions } from "./simple-options.ts"; -import { transformMessages } from "./transform-messages.ts"; +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"; -const MISTRAL_TOOL_CALL_ID_LENGTH = 9; -const MAX_MISTRAL_ERROR_BODY_CHARS = 4000; - -/** - * Provider-specific options for the Mistral API. - */ -type MistralReasoningEffort = "none" | "high"; - -export interface MistralOptions extends StreamOptions { - toolChoice?: "auto" | "none" | "any" | "required" | { type: "function"; function: { name: string } }; - promptMode?: "reasoning"; - reasoningEffort?: MistralReasoningEffort; -} - -/** - * Stream responses from Mistral using `chat.stream`. - */ -export const streamMistral: StreamFunction<"mistral-conversations", MistralOptions> = ( - model: Model<"mistral-conversations">, - context: Context, - options?: MistralOptions, -): AssistantMessageEventStream => { - const stream = new AssistantMessageEventStream(); - - (async () => { - const output = createOutput(model); - - try { - const apiKey = options?.apiKey; - if (!apiKey) { - throw new Error(`No API key for provider: ${model.provider}`); - } - - // Intentionally per-request: avoids shared SDK mutable state across concurrent consumers. - const mistral = new Mistral({ - apiKey, - serverURL: model.baseUrl, - }); - - const normalizeMistralToolCallId = createMistralToolCallIdNormalizer(); - const transformedMessages = transformMessages(context.messages, model, (id) => normalizeMistralToolCallId(id)); - - let payload = buildChatPayload(model, context, transformedMessages, options); - const nextPayload = await options?.onPayload?.(payload, model); - if (nextPayload !== undefined) { - payload = nextPayload as ChatCompletionStreamRequest; - } - const mistralStream = await mistral.chat.stream(payload, buildRequestOptions(model, options)); - stream.push({ type: "start", partial: output }); - await consumeChatStream(model, output, stream, mistralStream); - - if (options?.signal?.aborted) { - throw new Error("Request was aborted"); - } - - if (output.stopReason === "aborted" || output.stopReason === "error") { - throw new Error("An unknown error occurred"); - } - - stream.push({ type: "done", reason: output.stopReason, message: output }); - stream.end(); - } catch (error) { - for (const block of output.content) { - // partialArgs is only a streaming scratch buffer; never persist it. - delete (block as { partialArgs?: string }).partialArgs; - } - output.stopReason = options?.signal?.aborted ? "aborted" : "error"; - output.errorMessage = formatMistralError(error); - stream.push({ type: "error", reason: output.stopReason, error: output }); - stream.end(); - } - })(); - - return stream; -}; - -/** - * Maps provider-agnostic `SimpleStreamOptions` to Mistral options. - */ -export const streamSimpleMistral: StreamFunction<"mistral-conversations", SimpleStreamOptions> = ( - model: Model<"mistral-conversations">, - context: Context, - options?: SimpleStreamOptions, -): AssistantMessageEventStream => { - const apiKey = options?.apiKey; - if (!apiKey) { - throw new Error(`No API key for provider: ${model.provider}`); - } - - const base = buildBaseOptions(model, options, apiKey); - const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined; - const reasoning = clampedReasoning === "off" ? undefined : clampedReasoning; - const shouldUseReasoning = model.reasoning && reasoning !== undefined; - - return streamMistral(model, context, { - ...base, - promptMode: shouldUseReasoning && usesPromptModeReasoning(model) ? "reasoning" : undefined, - reasoningEffort: - shouldUseReasoning && usesReasoningEffort(model) ? mapReasoningEffort(model, reasoning) : undefined, - } satisfies MistralOptions); -}; - -function createOutput(model: Model<"mistral-conversations">): 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(), - }; -} - -function createMistralToolCallIdNormalizer(): (id: string) => string { - const idMap = new Map(); - const reverseMap = new Map(); - - return (id: string): string => { - const existing = idMap.get(id); - if (existing) return existing; - - let attempt = 0; - while (true) { - const candidate = deriveMistralToolCallId(id, attempt); - const owner = reverseMap.get(candidate); - if (!owner || owner === id) { - idMap.set(id, candidate); - reverseMap.set(candidate, id); - return candidate; - } - attempt++; - } - }; -} - -function deriveMistralToolCallId(id: string, attempt: number): string { - const normalized = id.replace(/[^a-zA-Z0-9]/g, ""); - if (attempt === 0 && normalized.length === MISTRAL_TOOL_CALL_ID_LENGTH) return normalized; - const seedBase = normalized || id; - const seed = attempt === 0 ? seedBase : `${seedBase}:${attempt}`; - return shortHash(seed) - .replace(/[^a-zA-Z0-9]/g, "") - .slice(0, MISTRAL_TOOL_CALL_ID_LENGTH); -} - -function formatMistralError(error: unknown): string { - if (error instanceof Error) { - const sdkError = error as Error & { statusCode?: unknown; body?: unknown }; - const statusCode = typeof sdkError.statusCode === "number" ? sdkError.statusCode : undefined; - const bodyText = typeof sdkError.body === "string" ? sdkError.body.trim() : undefined; - if (statusCode !== undefined && bodyText) { - return `Mistral API error (${statusCode}): ${truncateErrorText(bodyText, MAX_MISTRAL_ERROR_BODY_CHARS)}`; - } - if (statusCode !== undefined) return `Mistral API error (${statusCode}): ${error.message}`; - return error.message; - } - return safeJsonStringify(error); -} - -function truncateErrorText(text: string, maxChars: number): string { - if (text.length <= maxChars) return text; - return `${text.slice(0, maxChars)}... [truncated ${text.length - maxChars} chars]`; -} - -function safeJsonStringify(value: unknown): string { - try { - const serialized = JSON.stringify(value); - return serialized === undefined ? String(value) : serialized; - } catch { - return String(value); - } -} - -function buildRequestOptions(model: Model<"mistral-conversations">, options?: MistralOptions) { - const requestOptions: { - signal?: AbortSignal; - retries: { strategy: "none" }; - headers?: Record; - } = { - retries: { strategy: "none" }, - }; - if (options?.signal) requestOptions.signal = options.signal; - - const headers: Record = {}; - if (model.headers) Object.assign(headers, model.headers); - if (options?.headers) Object.assign(headers, options.headers); - - // Mistral infrastructure uses `x-affinity` for KV-cache reuse (prefix caching). - // Respect explicit caller-provided header values. - if (shouldUsePromptCaching(options) && !headers["x-affinity"]) { - headers["x-affinity"] = options.sessionId; - } - - if (Object.keys(headers).length > 0) { - requestOptions.headers = headers; - } - - return requestOptions; -} - -function buildChatPayload( - model: Model<"mistral-conversations">, - context: Context, - messages: Message[], - options?: MistralOptions, -): ChatCompletionStreamRequest { - const payload: ChatCompletionStreamRequest = { - model: model.id, - stream: true, - messages: toChatMessages(messages, model.input.includes("image")), - }; - - if (context.tools?.length) payload.tools = toFunctionTools(context.tools); - if (options?.temperature !== undefined) payload.temperature = options.temperature; - if (options?.maxTokens !== undefined) payload.maxTokens = options.maxTokens; - 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({ - role: "system", - content: sanitizeSurrogates(context.systemPrompt), - }); - } - - 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, - stream: AssistantMessageEventStream, - mistralStream: AsyncIterable, -): Promise { - let currentBlock: TextContent | ThinkingContent | null = null; - const blocks = output.content; - const blockIndex = () => blocks.length - 1; - const toolBlocksByKey = new Map(); - - const finishCurrentBlock = (block?: typeof currentBlock) => { - if (!block) return; - if (block.type === "text") { - stream.push({ - type: "text_end", - contentIndex: blockIndex(), - content: block.text, - partial: output, - }); - return; - } - if (block.type === "thinking") { - stream.push({ - type: "thinking_end", - contentIndex: blockIndex(), - content: block.thinking, - partial: output, - }); - } - }; - - for await (const event of mistralStream) { - const chunk = event.data; - // Mistral's streamed CompletionChunk carries an id field. Keep the first non-empty one, - // mirroring how OpenAI-style streaming exposes a stable response identifier per stream. - output.responseId ||= chunk.id; - - if (chunk.usage) { - 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 = cachedPromptTokens; - output.usage.cacheWrite = 0; - output.usage.totalTokens = - chunk.usage.totalTokens || - output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite; - calculateCost(model, output.usage); - } - - const choice = chunk.choices[0]; - if (!choice) continue; - - if (choice.finishReason) { - output.stopReason = mapChatStopReason(choice.finishReason); - } - - const delta = choice.delta; - if (delta.content !== null && delta.content !== undefined) { - const contentItems = typeof delta.content === "string" ? [delta.content] : delta.content; - for (const item of contentItems) { - if (typeof item === "string") { - const textDelta = sanitizeSurrogates(item); - if (!currentBlock || currentBlock.type !== "text") { - finishCurrentBlock(currentBlock); - currentBlock = { type: "text", text: "" }; - output.content.push(currentBlock); - stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output }); - } - currentBlock.text += textDelta; - stream.push({ - type: "text_delta", - contentIndex: blockIndex(), - delta: textDelta, - partial: output, - }); - continue; - } - - if (item.type === "thinking") { - const deltaText = item.thinking - .map((part) => ("text" in part ? part.text : "")) - .filter((text) => text.length > 0) - .join(""); - const thinkingDelta = sanitizeSurrogates(deltaText); - if (!thinkingDelta) continue; - if (!currentBlock || currentBlock.type !== "thinking") { - finishCurrentBlock(currentBlock); - currentBlock = { type: "thinking", thinking: "" }; - output.content.push(currentBlock); - stream.push({ type: "thinking_start", contentIndex: blockIndex(), partial: output }); - } - currentBlock.thinking += thinkingDelta; - stream.push({ - type: "thinking_delta", - contentIndex: blockIndex(), - delta: thinkingDelta, - partial: output, - }); - continue; - } - - if (item.type === "text") { - const textDelta = sanitizeSurrogates(item.text); - if (!currentBlock || currentBlock.type !== "text") { - finishCurrentBlock(currentBlock); - currentBlock = { type: "text", text: "" }; - output.content.push(currentBlock); - stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output }); - } - currentBlock.text += textDelta; - stream.push({ - type: "text_delta", - contentIndex: blockIndex(), - delta: textDelta, - partial: output, - }); - } - } - } - - const toolCalls = delta.toolCalls || []; - for (const toolCall of toolCalls) { - if (currentBlock) { - finishCurrentBlock(currentBlock); - currentBlock = null; - } - const callId = - toolCall.id && toolCall.id !== "null" - ? toolCall.id - : deriveMistralToolCallId(`toolcall:${toolCall.index ?? 0}`, 0); - const key = `${callId}:${toolCall.index || 0}`; - const existingIndex = toolBlocksByKey.get(key); - let block: (ToolCall & { partialArgs?: string }) | undefined; - - if (existingIndex !== undefined) { - const existing = output.content[existingIndex]; - if (existing?.type === "toolCall") { - block = existing as ToolCall & { partialArgs?: string }; - } - } - - if (!block) { - block = { - type: "toolCall", - id: callId, - name: toolCall.function.name, - arguments: {}, - partialArgs: "", - }; - output.content.push(block); - toolBlocksByKey.set(key, output.content.length - 1); - stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output }); - } - - const argsDelta = - typeof toolCall.function.arguments === "string" - ? toolCall.function.arguments - : JSON.stringify(toolCall.function.arguments || {}); - block.partialArgs = (block.partialArgs || "") + argsDelta; - block.arguments = parseStreamingJson>(block.partialArgs); - stream.push({ - type: "toolcall_delta", - contentIndex: toolBlocksByKey.get(key)!, - delta: argsDelta, - partial: output, - }); - } - } - - finishCurrentBlock(currentBlock); - for (const index of toolBlocksByKey.values()) { - const block = output.content[index]; - if (block.type !== "toolCall") continue; - const toolBlock = block as ToolCall & { partialArgs?: string }; - toolBlock.arguments = parseStreamingJson>(toolBlock.partialArgs); - // Finalize in-place and strip the scratch buffer so replay only - // carries parsed arguments. - delete toolBlock.partialArgs; - stream.push({ - type: "toolcall_end", - contentIndex: index, - toolCall: toolBlock, - partial: output, - }); - } -} - -function toFunctionTools(tools: Tool[]): Array { - return tools.map((tool) => ({ - type: "function", - function: { - name: tool.name, - description: tool.description, - parameters: stripSymbolKeys(tool.parameters) as Record, - strict: false, - }, - })); -} - -function stripSymbolKeys(value: unknown): unknown { - if (Array.isArray(value)) { - return value.map((item) => stripSymbolKeys(item)); - } - - if (value && typeof value === "object") { - const result: Record = {}; - for (const [key, entry] of Object.entries(value)) { - result[key] = stripSymbolKeys(entry); - } - return result; - } - - return value; -} - -function toChatMessages(messages: Message[], supportsImages: boolean): ChatCompletionStreamRequestMessage[] { - const result: ChatCompletionStreamRequestMessage[] = []; - - for (const msg of messages) { - if (msg.role === "user") { - if (typeof msg.content === "string") { - result.push({ role: "user", content: sanitizeSurrogates(msg.content) }); - continue; - } - const hadImages = msg.content.some((item) => item.type === "image"); - const content: ContentChunk[] = msg.content - .filter((item) => item.type === "text" || supportsImages) - .map((item) => { - if (item.type === "text") return { type: "text", text: sanitizeSurrogates(item.text) }; - return { type: "image_url", imageUrl: `data:${item.mimeType};base64,${item.data}` }; - }); - if (content.length > 0) { - result.push({ role: "user", content }); - continue; - } - if (hadImages && !supportsImages) { - result.push({ role: "user", content: "(image omitted: model does not support images)" }); - } - continue; - } - - if (msg.role === "assistant") { - const contentParts: ContentChunk[] = []; - const toolCalls: Array<{ id: string; type: "function"; function: { name: string; arguments: string } }> = []; - - for (const block of msg.content) { - if (block.type === "text") { - if (block.text.trim().length > 0) { - contentParts.push({ type: "text", text: sanitizeSurrogates(block.text) }); - } - continue; - } - if (block.type === "thinking") { - if (block.thinking.trim().length > 0) { - contentParts.push({ - type: "thinking", - thinking: [{ type: "text", text: sanitizeSurrogates(block.thinking) }], - }); - } - continue; - } - toolCalls.push({ - id: block.id, - type: "function", - function: { name: block.name, arguments: JSON.stringify(block.arguments || {}) }, - }); - } - - const assistantMessage: ChatCompletionStreamRequestMessage = { role: "assistant" }; - if (contentParts.length > 0) assistantMessage.content = contentParts; - if (toolCalls.length > 0) assistantMessage.toolCalls = toolCalls; - if (contentParts.length > 0 || toolCalls.length > 0) result.push(assistantMessage); - continue; - } - - const toolContent: ContentChunk[] = []; - const textResult = msg.content - .filter((part) => part.type === "text") - .map((part) => (part.type === "text" ? sanitizeSurrogates(part.text) : "")) - .join("\n"); - const hasImages = msg.content.some((part) => part.type === "image"); - const toolText = buildToolResultText(textResult, hasImages, supportsImages, msg.isError); - toolContent.push({ type: "text", text: toolText }); - for (const part of msg.content) { - if (!supportsImages) continue; - if (part.type !== "image") continue; - toolContent.push({ - type: "image_url", - imageUrl: `data:${part.mimeType};base64,${part.data}`, - }); - } - result.push({ - role: "tool", - toolCallId: msg.toolCallId, - name: msg.toolName, - content: toolContent, - }); - } - - return result; -} - -function buildToolResultText(text: string, hasImages: boolean, supportsImages: boolean, isError: boolean): string { - const trimmed = text.trim(); - const errorPrefix = isError ? "[tool error] " : ""; - - if (trimmed.length > 0) { - const imageSuffix = hasImages && !supportsImages ? "\n[tool image omitted: model does not support images]" : ""; - return `${errorPrefix}${trimmed}${imageSuffix}`; - } - - if (hasImages) { - if (supportsImages) { - return isError ? "[tool error] (see attached image)" : "(see attached image)"; - } - return isError - ? "[tool error] (image omitted: model does not support images)" - : "(image omitted: model does not support images)"; - } - - return isError ? "[tool error] (no tool output)" : "(no tool output)"; -} - -function usesReasoningEffort(model: Model<"mistral-conversations">): boolean { - return model.id === "mistral-small-2603" || model.id === "mistral-small-latest" || model.id === "mistral-medium-3.5"; -} - -function usesPromptModeReasoning(model: Model<"mistral-conversations">): boolean { - return model.reasoning && !usesReasoningEffort(model); -} - -function mapReasoningEffort( - model: Model<"mistral-conversations">, - level: Exclude, -): MistralReasoningEffort { - return (model.thinkingLevelMap?.[level] ?? "high") as MistralReasoningEffort; -} - -function mapToolChoice( - choice: MistralOptions["toolChoice"], -): "auto" | "none" | "any" | "required" | { type: "function"; function: { name: string } } | undefined { - if (!choice) return undefined; - if (choice === "auto" || choice === "none" || choice === "any" || choice === "required") { - return choice as any; - } - return { - type: "function", - function: { name: choice.function.name }, - }; -} - -function mapChatStopReason(reason: string | null): StopReason { - if (reason === null) return "stop"; - switch (reason) { - case "stop": - return "stop"; - case "length": - case "model_length": - return "length"; - case "tool_calls": - return "toolUse"; - case "error": - return "error"; - default: - return "stop"; - } +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..899f9b11 --- /dev/null +++ b/packages/ai/src/providers/moonshotai-cn.models.ts @@ -0,0 +1,171 @@ +// 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">, + "kimi-k2.7-code": { + id: "kimi-k2.7-code", + name: "Kimi K2.7 Code", + 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, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.19, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2.7-code-highspeed": { + id: "kimi-k2.7-code-highspeed", + name: "Kimi K2.7 Code HighSpeed", + 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, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.9, + output: 8, + cacheRead: 0.38, + 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..2ec685e6 --- /dev/null +++ b/packages/ai/src/providers/moonshotai.models.ts @@ -0,0 +1,171 @@ +// 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">, + "kimi-k2.7-code": { + id: "kimi-k2.7-code", + name: "Kimi K2.7 Code", + 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, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.19, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2.7-code-highspeed": { + id: "kimi-k2.7-code-highspeed", + name: "Kimi K2.7 Code HighSpeed", + 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, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.9, + output: 8, + cacheRead: 0.38, + 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..d0a0c713 --- /dev/null +++ b/packages/ai/src/providers/nvidia.models.ts @@ -0,0 +1,368 @@ +// 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.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..aac18d80 --- /dev/null +++ b/packages/ai/src/providers/opencode-go.models.ts @@ -0,0 +1,242 @@ +// 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.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">, + "glm-5.2": { + id: "glm-5.2", + name: "GLM-5.2", + api: "openai-completions", + provider: "opencode-go", + 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, + output: 4.4, + cacheRead: 0.26, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 131072, + } 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","supportsLongCacheRetention":false}, + 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">, + "kimi-k2.7-code": { + id: "kimi-k2.7-code", + name: "Kimi K2.7 Code", + 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.95, + output: 4, + cacheRead: 0.19, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } 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.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 (3x usage)", + api: "anthropic-messages", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.4, + cacheRead: 0.02, + 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..1b050cf8 --- /dev/null +++ b/packages/ai/src/providers/opencode.models.ts @@ -0,0 +1,799 @@ +// 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-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","supportsLongCacheRetention":false,"requiresReasoningContentOnAssistantMessages":true}, + 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}, + 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","supportsLongCacheRetention":false,"requiresReasoningContentOnAssistantMessages":true}, + 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","supportsLongCacheRetention":false}, + 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","supportsLongCacheRetention":false}, + 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","supportsLongCacheRetention":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">, + "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-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/providers/openrouter.models.ts b/packages/ai/src/providers/openrouter.models.ts new file mode 100644 index 00000000..02a5d3b9 --- /dev/null +++ b/packages/ai/src/providers/openrouter.models.ts @@ -0,0 +1,4435 @@ +// 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.8, + output: 3.2, + 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-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.1, + 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.25, + output: 0.8, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 80000, + } 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.1, + output: 0.4, + 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">, + "cohere/north-mini-code:free": { + id: "cohere/north-mini-code:free", + name: "Cohere: North Mini Code (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: 64000, + } 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.2002, + output: 0.8001, + 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.2, + output: 0.77, + cacheRead: 0.135, + cacheWrite: 0, + }, + contextWindow: 163840, + 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.79, + 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.15, + 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.09, + output: 0.18, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } 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.083333, + }, + 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.1, + output: 0.4, + cacheRead: 0.01, + cacheWrite: 0.083333, + }, + 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.1, + output: 0.4, + cacheRead: 0.01, + cacheWrite: 0.083333, + }, + 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.05, + cacheWrite: 0.083333, + }, + contextWindow: 1048576, + maxTokens: 65535, + } satisfies Model<"openai-completions">, + "google/gemini-3-pro-image": { + id: "google/gemini-3-pro-image", + name: "Google: Nano Banana Pro (Gemini 3 Pro Image)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0.375, + }, + contextWindow: 65536, + maxTokens: 32768, + } 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.025, + cacheWrite: 0.083333, + }, + 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.025, + cacheWrite: 0.083333, + }, + 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.2, + 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.2, + 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.083333, + }, + 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.05, + 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.35, + cacheRead: 0.09, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } 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: 8192, + } 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.05, + output: 0.1, + cacheRead: 0.05, + 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.025, + 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">, + "liquid/lfm-2.5-1.2b-thinking:free": { + id: "liquid/lfm-2.5-1.2b-thinking:free", + name: "LiquidAI: LFM2.5-1.2B-Thinking (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: 32768, + maxTokens: 4096, + } 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.4, + output: 0.4, + 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.1, + 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.1, + 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.4, + 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.9, + cacheRead: 0.05, + 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.25, + output: 1, + cacheRead: 0.05, + 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.9, + 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.4, + 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.2, + output: 0.2, + 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.1, + output: 0.1, + 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.2, + 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.2, + 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.05, + 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.4, + 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.4, + 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.2, + 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.2, + 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.2, + 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.1, + 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.57, + 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.66, + output: 3.41, + cacheRead: 0.144, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "moonshotai/kimi-k2.7-code": { + id: "moonshotai/kimi-k2.7-code", + name: "MoonshotAI: Kimi K2.7 Code", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.612, + output: 3.069, + cacheRead: 0.1296, + 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", + reasoning: true, + input: ["text"], + cost: { + input: 0.4, + output: 0.4, + 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.05, + output: 0.2, + 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.45, + 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.2, + cacheRead: 0.1, + 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: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.4, + output: 1.6, + cacheRead: 0.1, + 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.1, + output: 0.4, + cacheRead: 0.025, + 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.025, + 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.05, + output: 0.4, + 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.025, + 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.2, + 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: 32768, + } 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.0375, + 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/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", + 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": { + 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)", + 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": { + 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)", + 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.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", + reasoning: false, + input: ["text"], + cost: { + input: 0.36, + output: 0.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "qwen/qwen-2.5-7b-instruct": { + id: "qwen/qwen-2.5-7b-instruct", + name: "Qwen: Qwen2.5 7B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.04, + output: 0.1, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } 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.052, + 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.1, + 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.455, + output: 1.82, + 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.1, + 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.1, + output: 0.1, + cacheRead: 0.1, + 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.4, + 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.05, + output: 0.4, + cacheRead: 0.05, + 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.8, + 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.8, + 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.2, + 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.104, + output: 0.416, + 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, + 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.385, + output: 2.45, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 4096, + } 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.1, + 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.8, + 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.2885, + output: 3.17, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262140, + } 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: 262144, + } 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.32, + output: 1.28, + cacheRead: 0.064, + cacheWrite: 0.4, + }, + 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.1, + output: 0.1, + 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.2, + 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.021, + 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.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", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.4, + output: 0.4, + 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.2, + 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.2, + 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.2, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 4096, + } 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.13, + output: 0.85, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 98304, + } 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.8, + 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.9, + 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.4, + 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.4, + 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.49, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 65535, + } satisfies Model<"openai-completions">, + "z-ai/glm-5.2": { + id: "z-ai/glm-5.2", + name: "Z.ai: GLM 5.2", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text"], + cost: { + input: 1, + output: 4, + cacheRead: 0.18, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 32768, + } 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.1, + 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.083333, + }, + 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.2, + 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.66, + output: 3.41, + cacheRead: 0.144, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } 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/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/providers/together.models.ts b/packages/ai/src/providers/together.models.ts new file mode 100644 index 00000000..5ba1500c --- /dev/null +++ b/packages/ai/src/providers/together.models.ts @@ -0,0 +1,363 @@ +// 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.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">, + "MiniMaxAI/MiniMax-M3": { + id: "MiniMaxAI/MiniMax-M3", + name: "MiniMax-M3", + 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.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 524288, + maxTokens: 250000, + } satisfies Model<"openai-completions">, + "Qwen/Qwen2.5-7B-Instruct-Turbo": { + id: "Qwen/Qwen2.5-7B-Instruct-Turbo", + name: "Qwen 2.5 7B Instruct Turbo", + 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.3, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 32768, + maxTokens: 32768, + } 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}, + reasoning: false, + input: ["text"], + cost: { + input: 0.2, + output: 0.6, + 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.5-9B": { + id: "Qwen/Qwen3.5-9B", + name: "Qwen3.5 9B", + 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.17, + output: 0.25, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } 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}, + reasoning: false, + input: ["text"], + cost: { + input: 1.25, + output: 3.75, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 500000, + } 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: 1.74, + output: 3.48, + 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.39, + output: 0.97, + 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.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">, + "moonshotai/Kimi-K2.7-Code": { + id: "moonshotai/Kimi-K2.7-Code", + name: "Kimi K2.7 Code", + 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.95, + output: 4, + cacheRead: 0.19, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } 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">, + "openai/gpt-oss-20b": { + id: "openai/gpt-oss-20b", + name: "GPT OSS 20B", + 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.05, + output: 0.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "zai-org/GLM-5": { + id: "zai-org/GLM-5", + name: "GLM-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: 1, + output: 3.2, + cacheRead: 0, + 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: "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..ea65e49c --- /dev/null +++ b/packages/ai/src/providers/vercel-ai-gateway.models.ts @@ -0,0 +1,2899 @@ +// 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.4, + 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.2, + 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.4, + 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.1, + output: 0.4, + 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.4, + 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.6, + 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.1, + 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.4, + output: 1.6, + 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.8, + output: 4, + cacheRead: 0.08, + cacheWrite: 1, + }, + contextWindow: 200000, + maxTokens: 8192, + } 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.1, + 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.9, + 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.05, + 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.1, + output: 0.4, + 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.05, + 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.2, + 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.2, + 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.4, + 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.025, + 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.97, + 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.17, + 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.9, + 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.4, + 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.1, + 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.1, + 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.1, + output: 0.1, + 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.4, + 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.15, + output: 0.15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } 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.1, + 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.57, + 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.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.1, + 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">, + "moonshotai/kimi-k2.7-code": { + id: "moonshotai/kimi-k2.7-code", + name: "Kimi K2.7 Code", + 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.19, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "moonshotai/kimi-k2.7-code-highspeed": { + id: "moonshotai/kimi-k2.7-code-highspeed", + name: "Kimi K2.7 Code High Speed", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.9, + output: 8, + cacheRead: 0.38, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } 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.2, + 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.23, + 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.4, + output: 1.6, + cacheRead: 0.1, + 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.1, + output: 0.4, + cacheRead: 0.025, + 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.025, + 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.05, + output: 0.4, + 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.025, + 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.2, + 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.05, + output: 0.2, + 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">, + "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", + 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.02, + cacheWrite: 0, + }, + 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.2, + 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.2, + output: 0.5, + cacheRead: 0.05, + 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.2, + output: 0.5, + cacheRead: 0.05, + 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.2, + 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.2, + 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.2, + 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.2, + 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.2, + 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.2, + 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.2, + 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.2, + 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.1, + 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.2, + 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.2, + 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.8, + 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.9, + cacheRead: 0.05, + 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.4, + 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.4, + 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.2, + cacheRead: 0.2, + 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-5.2": { + id: "zai/glm-5.2", + name: "GLM 5.2", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1.5, + output: 4.5, + cacheRead: 0.3, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 128000, + } 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..90865c0b --- /dev/null +++ b/packages/ai/src/providers/zai-coding-cn.models.ts @@ -0,0 +1,116 @@ +// 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-5.2": { + id: "glm-5.2", + name: "GLM-5.2", + 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}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":"high","medium":"high","high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + 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..364a158f --- /dev/null +++ b/packages/ai/src/providers/zai.models.ts @@ -0,0 +1,116 @@ +// 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-5.2": { + id: "glm-5.2", + name: "GLM-5.2", + api: "openai-completions", + provider: "zai", + baseUrl: "https://api.z.ai/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","supportsReasoningEffort":true,"zaiToolStream":true}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":"high","medium":"high","high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + 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/stream.ts b/packages/ai/src/stream.ts deleted file mode 100644 index 3f333d9e..00000000 --- a/packages/ai/src/stream.ts +++ /dev/null @@ -1,74 +0,0 @@ -import "./providers/register-builtins.ts"; - -import { getApiProvider } from "./api-registry.ts"; -import { getEnvApiKey } from "./env-api-keys.ts"; -import type { - Api, - AssistantMessage, - AssistantMessageEventStream, - Context, - Model, - ProviderStreamOptions, - SimpleStreamOptions, - 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; -} - -function withEnvApiKey( - model: Model, - options: TOptions | undefined, -): TOptions | undefined { - if (hasExplicitApiKey(options?.apiKey)) return options; - const apiKey = getEnvApiKey(model.provider, options?.env); - if (!apiKey) return options; - return { ...options, apiKey } as TOptions; -} - -function resolveApiProvider(api: Api) { - const provider = getApiProvider(api); - if (!provider) { - throw new Error(`No API provider registered for api: ${api}`); - } - return provider; -} - -export function stream( - model: Model, - context: Context, - options?: ProviderStreamOptions, -): AssistantMessageEventStream { - const provider = resolveApiProvider(model.api); - return provider.stream(model, context, withEnvApiKey(model, options) as StreamOptions); -} - -export async function complete( - model: Model, - context: Context, - options?: ProviderStreamOptions, -): Promise { - const s = stream(model, context, options); - return s.result(); -} - -export function streamSimple( - model: Model, - context: Context, - options?: SimpleStreamOptions, -): AssistantMessageEventStream { - const provider = resolveApiProvider(model.api); - return provider.streamSimple(model, context, withEnvApiKey(model, options)); -} - -export async function completeSimple( - model: Model, - context: Context, - options?: SimpleStreamOptions, -): Promise { - const s = streamSimple(model, context, options); - return s.result(); -} diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index 54f44685..48a6961e 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -1,3 +1,12 @@ +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"; @@ -56,11 +65,11 @@ 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"; -export type ImagesProvider = KnownImagesProvider | string; +export type ImagesProviderId = KnownImagesProvider | string; export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh"; export type ModelThinkingLevel = "off" | ThinkingLevel; @@ -175,6 +184,58 @@ 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; + +/** + * 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; +} + +/** + * 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; @@ -309,7 +370,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 @@ -343,7 +404,7 @@ export type ImagesStopReason = "stop" | "error" | "aborted"; export interface AssistantImages { api: ImagesApi; - provider: ImagesProvider; + provider: ImagesProviderId; model: string; output: ImagesOutputContent[]; responseId?: string; @@ -592,7 +653,7 @@ export interface Model { id: string; name: string; api: TApi; - provider: Provider; + provider: ProviderId; baseUrl: string; reasoning: boolean; /** @@ -623,6 +684,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/src/utils/oauth/anthropic.ts b/packages/ai/src/utils/oauth/anthropic.ts index 1b3e4244..591e9cde 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 { getProviderEnvValue } from "../provider-env.ts"; import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.ts"; import { generatePKCE } from "./pkce.ts"; @@ -379,6 +380,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 bb78a93a..111af0ad 100644 --- a/packages/ai/src/utils/oauth/github-copilot.ts +++ b/packages/ai/src/utils/oauth/github-copilot.ts @@ -2,7 +2,8 @@ * GitHub Copilot OAuth flow */ -import { getModels } from "../../models.ts"; +import type { OAuthAuth, OAuthCredential } from "../../auth/types.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"; @@ -328,7 +329,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); @@ -391,6 +392,42 @@ export async function loginGitHubCopilot(options: { }; } +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/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/src/utils/oauth/openai-codex.ts b/packages/ai/src/utils/oauth/openai-codex.ts index a5103c39..a2f7cd00 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 { getProviderEnvValue } from "../provider-env.ts"; import { pollOAuthDeviceCodeFlow } from "./device-code.ts"; import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.ts"; @@ -561,6 +562,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/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 18023e11..8d99ff18 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-cache-write-1h-cost.test.ts b/packages/ai/test/anthropic-cache-write-1h-cost.test.ts index f9523b40..13745e9b 100644 --- a/packages/ai/test/anthropic-cache-write-1h-cost.test.ts +++ b/packages/ai/test/anthropic-cache-write-1h-cost.test.ts @@ -1,7 +1,7 @@ import type Anthropic from "@anthropic-ai/sdk"; import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { streamAnthropic } from "../src/providers/anthropic.ts"; +import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts"; +import { getModel } from "../src/compat.ts"; import type { Context } from "../src/types.ts"; function createSseResponse(events: Array<{ event: string; data: string }>): Response { 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-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 d8daf7f7..e510ec55 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 { getModel } from "../src/models.ts"; -import { streamAnthropic } from "../src/providers/anthropic.ts"; +import { stream as streamAnthropic } from "../src/api/anthropic-messages.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 15b8a528..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 { getModel } from "../src/models.ts"; -import { streamAzureOpenAIResponses } from "../src/providers/azure-openai-responses.ts"; +import { stream as streamAzureOpenAIResponses } from "../src/api/azure-openai-responses.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 d74dedae..c43f7978 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 { getModel } from "../src/models.ts"; -import { streamBedrock } from "../src/providers/amazon-bedrock.ts"; +import { stream as streamBedrock } from "../src/api/bedrock-converse-stream.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 1017d089..43ee692c 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 { 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 { 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/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 18be2476..168cf4d1 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 { getModel } from "../src/models.ts"; -import { type BedrockOptions, streamBedrock } from "../src/providers/amazon-bedrock.ts"; +import { type BedrockOptions, stream as streamBedrock } from "../src/api/bedrock-converse-stream.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 8f4e06e7..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 { getModel } from "../src/models.ts"; -import { type BedrockOptions, streamBedrock } from "../src/providers/amazon-bedrock.ts"; +import { type BedrockOptions, stream as streamBedrock } from "../src/api/bedrock-converse-stream.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 6e2c1a5b..c80ad19c 100644 --- a/packages/ai/test/cache-retention.test.ts +++ b/packages/ai/test/cache-retention.test.ts @@ -1,10 +1,9 @@ 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, stream } from "../src/compat.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/codex-websocket-cached-probe.ts b/packages/ai/test/codex-websocket-cached-probe.ts index 7317035c..d8154ac1 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/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 8b291b89..6980f318 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 { getModel, getModels } from "../src/compat.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 ace2bd8f..74a95418 100644 --- a/packages/ai/test/github-copilot-anthropic.test.ts +++ b/packages/ai/test/github-copilot-anthropic.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vitest"; -import { getModel, getSupportedThinkingLevels } from "../src/models.ts"; -import { streamAnthropic } from "../src/providers/anthropic.ts"; +import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts"; +import { getModel } from "../src/compat.ts"; +import { getSupportedThinkingLevels } from "../src/models.ts"; import type { Context } from "../src/types.ts"; const mockState = vi.hoisted(() => ({ diff --git a/packages/ai/test/github-copilot-oauth.test.ts b/packages/ai/test/github-copilot-oauth.test.ts index 2aba3084..f5e426e3 100644 --- a/packages/ai/test/github-copilot-oauth.test.ts +++ b/packages/ai/test/github-copilot-oauth.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { getModels } from "../src/models.ts"; +import { getModels } from "../src/compat.ts"; import { githubCopilotOAuthProvider, loginGitHubCopilot, 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-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-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..46f24a77 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 { getModel } from "../src/models.ts"; -import { streamGoogleVertex } from "../src/providers/google-vertex.ts"; +import { stream as streamGoogleVertex } from "../src/api/google-vertex.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/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"); + }); +}); 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 e21f0d12..dd516962 100644 --- a/packages/ai/test/lazy-module-load.test.ts +++ b/packages/ai/test/lazy-module-load.test.ts @@ -5,6 +5,8 @@ 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 = [ "@anthropic-ai/sdk", @@ -66,8 +68,25 @@ describe("lazy provider module loading", () => { expect(result.loadedSpecifiers).toEqual([]); }); - it("loads only the Anthropic SDK when calling the root lazy wrapper", () => { + 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(); + models.getModels(); + `); + 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", @@ -81,7 +100,7 @@ describe("lazy provider module loading", () => { maxTokens: 8192, }; const context = { messages: [{ role: "user", content: "hi" }] }; - await mod.streamSimpleAnthropic(model, context).result(); + await compat.anthropicMessagesApi().streamSimple(model, context).result(); `); expect(result.loadedSpecifiers).toEqual(["@anthropic-ai/sdk"]); @@ -89,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 d197a56a..bffa292d 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/models-runtime.test.ts b/packages/ai/test/models-runtime.test.ts new file mode 100644 index 00000000..cafcb17f --- /dev/null +++ b/packages/ai/test/models-runtime.test.ts @@ -0,0 +1,416 @@ +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?: () => readonly Model[]; + refreshModels?: () => 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 ?? (() => models), + refreshModels: input.refreshModels, + 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(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 = 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", () => { + const models = createModels(); + models.setProvider( + testProvider({ + id: "broken", + getModels: () => { + throw new Error("boom"); + }, + }), + ); + models.setProvider(testProvider({ id: "ok", models: [testModel("ok", "m1")] })); + + expect(models.getModels().map((m) => m.id)).toEqual(["m1"]); + expect(models.getModels("broken")).toEqual([]); + // precise failures come from the provider directly + expect(() => models.getProvider("broken")?.getModels()).toThrow("boom"); + }); + + 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: "dyn", + getModels: () => list, + refreshModels: async () => { + refreshes++; + list = [testModel("dyn", "after")]; + }, + }), + ); + models.setProvider(testProvider({ id: "static", models: [testModel("static", "s1")] })); + + 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 () => { + 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/oauth-auth.test.ts b/packages/ai/test/oauth-auth.test.ts new file mode 100644 index 00000000..c008b18c --- /dev/null +++ b/packages/ai/test/oauth-auth.test.ts @@ -0,0 +1,125 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { InMemoryCredentialStore } from "../src/auth/credential-store.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"); + }); +}); + +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 = 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 = 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/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-codex-stream.test.ts b/packages/ai/test/openai-codex-stream.test.ts index fe4f3ea6..9c526151 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..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 { getModel } from "../src/models.ts"; -import { streamOpenAICompletions } from "../src/providers/openai-completions.ts"; +import { stream as streamOpenAICompletions } from "../src/api/openai-completions.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 be233670..5906ea30 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 75098fc1..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 { getModel } from "../src/models.ts"; -import { streamOpenAICompletions } from "../src/providers/openai-completions.ts"; +import { stream as streamOpenAICompletions } from "../src/api/openai-completions.ts"; +import { getModel } from "../src/compat.ts"; import type { Model } from "../src/types.ts"; interface FakeOpenAIClientOptions { diff --git a/packages/ai/test/openai-completions-reasoning-details.test.ts b/packages/ai/test/openai-completions-reasoning-details.test.ts index c07b1acf..88d42874 100644 --- a/packages/ai/test/openai-completions-reasoning-details.test.ts +++ b/packages/ai/test/openai-completions-reasoning-details.test.ts @@ -1,6 +1,6 @@ import { Type } from "typebox"; 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 { AssistantMessage, Model, Tool } from "../src/types.ts"; const mockState = vi.hoisted(() => ({ 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-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 138eb3e3..d1cbe14a 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-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index e3b34681..927e34b3 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 { getModel } from "../src/models.ts"; -import { convertMessages } from "../src/providers/openai-completions.ts"; -import { stream, streamSimple } from "../src/stream.ts"; +import { convertMessages } from "../src/api/openai-completions.ts"; +import { getModel, stream, streamSimple } from "../src/compat.ts"; import type { AssistantMessage, Model, SimpleStreamOptions, Tool, ToolResultMessage } from "../src/types.ts"; const mockState = vi.hoisted(() => ({ @@ -1010,6 +1009,8 @@ describe("openai-completions tool_choice", () => { }); it("stores OpenRouter Kimi K2.6 reasoning replay compat in built-in metadata", () => { + // `: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); 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 4458e51f..c8500792 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 { getModel } from "../src/models.ts"; -import { convertMessages } from "../src/providers/openai-completions.ts"; +import { convertMessages } from "../src/api/openai-completions.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 04236fed..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 { getModel } from "../src/models.ts"; -import { streamOpenAIResponses } from "../src/providers/openai-responses.ts"; +import { stream as streamOpenAIResponses } from "../src/api/openai-responses.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 10f06377..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 { getModel } from "../src/models.ts"; -import { convertResponsesMessages } from "../src/providers/openai-responses-shared.ts"; +import { convertResponsesMessages } from "../src/api/openai-responses-shared.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 f675cc8e..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 { getModel } from "../src/models.ts"; -import { convertResponsesMessages } from "../src/providers/openai-responses-shared.ts"; +import { convertResponsesMessages } from "../src/api/openai-responses-shared.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-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/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/providers.test.ts b/packages/ai/test/providers.test.ts new file mode 100644 index 00000000..3e6320a9 --- /dev/null +++ b/packages/ai/test/providers.test.ts @@ -0,0 +1,221 @@ +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 = models.getModel("anthropic", "claude-haiku-4-5"); + expect(anthropic?.api).toBe("anthropic-messages"); + + 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 = 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 = 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 = 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 = 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 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: [], + refreshModels: async () => { + fetches++; + await new Promise((resolve) => setTimeout(resolve, 5)); + return [testModel("api-a", "listed")]; + }, + api: recordingStreams("a", []), + }); + + 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); + }); +}); + +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 = 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/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/scratch.ts b/packages/ai/test/scratch.ts new file mode 100644 index 00000000..c2d83649 --- /dev/null +++ b/packages/ai/test/scratch.ts @@ -0,0 +1,57 @@ +// 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 } from "../src/models.ts"; +import { anthropicProvider } from "../src/providers/anthropic.ts"; +import type { Context } from "../src/types.ts"; + +// --------------------------------------------------------------------------- +// 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(anthropicProvider()); + +// --------------------------------------------------------------------------- +// 2. Look up a model and check auth. +// --------------------------------------------------------------------------- + +const model = 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() }], +}; + +// --------------------------------------------------------------------------- +// 3. Simple completion (request-level auth resolution happens inside). +// --------------------------------------------------------------------------- + +const message = await models.completeSimple(model, context); +console.log(`completeSimple -> [${message.stopReason}]`, message.content); + +// --------------------------------------------------------------------------- +// 4. 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)}`); 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 9f5363cb..257758dc 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/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/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/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 69e2d8cb..6795f662 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -6,6 +6,11 @@ - 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)). +- 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`. ## [0.79.10] - 2026-06-22 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-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/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 395d8161..26022f63 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 { getThemeByName, 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 3394a063..31a0ec9c 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"; @@ -41,6 +41,10 @@ export type AuthStatus = { label?: string; }; +export interface GetApiKeyOptions { + includeFallback?: boolean; +} + type LockResult = { result: T; next?: string; @@ -199,7 +203,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; @@ -238,14 +241,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); @@ -350,7 +345,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; } @@ -371,10 +365,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 }; } @@ -468,9 +458,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, options: GetApiKeyOptions = {}): Promise { // Runtime override takes highest priority const runtimeKey = this.runtimeOverrides.get(providerId); if (runtimeKey) { @@ -521,15 +510,12 @@ export class AuthStorage { } } + if (options.includeFallback === false) return undefined; + // Fall back to environment variable 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/compaction/branch-summarization.ts b/packages/coding-agent/src/core/compaction/branch-summarization.ts index 3378eb19..3f557c01 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 83315a84..83b0db57 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 dcaf389f..a93f7c85 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 305dca92..8f86bf6c 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"; @@ -801,7 +801,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; } diff --git a/packages/coding-agent/src/core/sdk.ts b/packages/coding-agent/src/core/sdk.ts index 180846cb..3bec2c32 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 100feb6b..27e9f88a 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 7fd56b46..6384b5a0 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,12 +2,8 @@ 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, - createAssistantMessageEventStream, - fauxAssistantMessage, - getModel, -} from "@earendil-works/pi-ai"; +import { type AssistantMessage, createAssistantMessageEventStream, fauxAssistantMessage } from "@earendil-works/pi-ai"; +import { 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 c95125f2..62ca9780 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 be3d5c63..a245a3fc 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 bc6496d8..2405e557 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 a6338d14..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,11 +20,11 @@ import { type Model, type SimpleStreamOptions, Type, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/compat"; 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"; 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/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; } 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, 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