summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-16 01:02:50 -0400
committerGitHub <[email protected]>2026-04-16 05:02:50 +0000
commit6b2083898120c02413dae806e749872ae407d9d1 (patch)
tree11c79d49fd553c939607f0d239af64c5d56a6ee9
parentc8af8f96ce2059ebf114a25ec958ab88dc15ff76 (diff)
downloadopencode-6b2083898120c02413dae806e749872ae407d9d1.tar.gz
opencode-6b2083898120c02413dae806e749872ae407d9d1.zip
feat: unwrap provider namespaces to flat exports + barrel (#22760)
-rw-r--r--packages/opencode/src/agent/agent.ts2
-rw-r--r--packages/opencode/src/cli/cmd/github.ts2
-rw-r--r--packages/opencode/src/cli/cmd/models.ts2
-rw-r--r--packages/opencode/src/cli/cmd/providers.ts2
-rw-r--r--packages/opencode/src/effect/app-runtime.ts2
-rw-r--r--packages/opencode/src/provider/auth.ts414
-rw-r--r--packages/opencode/src/provider/error.ts326
-rw-r--r--packages/opencode/src/provider/index.ts4
-rw-r--r--packages/opencode/src/provider/models.ts294
-rw-r--r--packages/opencode/src/provider/provider.ts4
-rw-r--r--packages/opencode/src/provider/transform.ts1754
-rw-r--r--packages/opencode/src/server/instance/httpapi/provider.ts2
-rw-r--r--packages/opencode/src/server/instance/provider.ts4
-rw-r--r--packages/opencode/src/session/llm.ts2
-rw-r--r--packages/opencode/src/session/message-v2.ts2
-rw-r--r--packages/opencode/src/session/overflow.ts2
-rw-r--r--packages/opencode/src/session/prompt.ts2
-rw-r--r--packages/opencode/test/plugin/auth-override.test.ts2
-rw-r--r--packages/opencode/test/provider/provider.test.ts2
-rw-r--r--packages/opencode/test/provider/transform.test.ts2
-rw-r--r--packages/opencode/test/session/llm.test.ts4
21 files changed, 1413 insertions, 1417 deletions
diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts
index f7e3a3515..54ca48455 100644
--- a/packages/opencode/src/agent/agent.ts
+++ b/packages/opencode/src/agent/agent.ts
@@ -6,7 +6,7 @@ import { generateObject, streamObject, type ModelMessage } from "ai"
import { Instance } from "../project/instance"
import { Truncate } from "../tool"
import { Auth } from "../auth"
-import { ProviderTransform } from "../provider/transform"
+import { ProviderTransform } from "../provider"
import PROMPT_GENERATE from "./generate.txt"
import PROMPT_COMPACTION from "./prompt/compaction.txt"
diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts
index 822d78770..ed1ca2124 100644
--- a/packages/opencode/src/cli/cmd/github.ts
+++ b/packages/opencode/src/cli/cmd/github.ts
@@ -18,7 +18,7 @@ import type {
} from "@octokit/webhooks-types"
import { UI } from "../ui"
import { cmd } from "./cmd"
-import { ModelsDev } from "../../provider/models"
+import { ModelsDev } from "../../provider"
import { Instance } from "@/project/instance"
import { bootstrap } from "../bootstrap"
import { SessionShare } from "@/share"
diff --git a/packages/opencode/src/cli/cmd/models.ts b/packages/opencode/src/cli/cmd/models.ts
index af5ca2f95..446d21f5d 100644
--- a/packages/opencode/src/cli/cmd/models.ts
+++ b/packages/opencode/src/cli/cmd/models.ts
@@ -2,7 +2,7 @@ import type { Argv } from "yargs"
import { Instance } from "../../project/instance"
import { Provider } from "../../provider"
import { ProviderID } from "../../provider/schema"
-import { ModelsDev } from "../../provider/models"
+import { ModelsDev } from "../../provider"
import { cmd } from "./cmd"
import { UI } from "../ui"
import { EOL } from "os"
diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts
index 47a5c37e8..4bc3f0ea6 100644
--- a/packages/opencode/src/cli/cmd/providers.ts
+++ b/packages/opencode/src/cli/cmd/providers.ts
@@ -3,7 +3,7 @@ import { AppRuntime } from "../../effect/app-runtime"
import { cmd } from "./cmd"
import * as prompts from "@clack/prompts"
import { UI } from "../ui"
-import { ModelsDev } from "../../provider/models"
+import { ModelsDev } from "../../provider"
import { map, pipe, sortBy, values } from "remeda"
import path from "path"
import os from "os"
diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts
index 60bbfe0ef..aabafc5b4 100644
--- a/packages/opencode/src/effect/app-runtime.ts
+++ b/packages/opencode/src/effect/app-runtime.ts
@@ -16,7 +16,7 @@ import { Storage } from "@/storage"
import { Snapshot } from "@/snapshot"
import { Plugin } from "@/plugin"
import { Provider } from "@/provider"
-import { ProviderAuth } from "@/provider/auth"
+import { ProviderAuth } from "@/provider"
import { Agent } from "@/agent/agent"
import { Skill } from "@/skill"
import { Discovery } from "@/skill/discovery"
diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts
index fd71f2f7a..c0c73b2cc 100644
--- a/packages/opencode/src/provider/auth.ts
+++ b/packages/opencode/src/provider/auth.ts
@@ -9,219 +9,217 @@ import { ProviderID } from "./schema"
import { Array as Arr, Effect, Layer, Record, Result, Context, Schema } from "effect"
import z from "zod"
-export namespace ProviderAuth {
- const When = Schema.Struct({
- key: Schema.String,
- op: Schema.Literals(["eq", "neq"]),
- value: Schema.String,
- })
-
- const TextPrompt = Schema.Struct({
- type: Schema.Literal("text"),
- key: Schema.String,
- message: Schema.String,
- placeholder: Schema.optional(Schema.String),
- when: Schema.optional(When),
- })
-
- const SelectOption = Schema.Struct({
- label: Schema.String,
- value: Schema.String,
- hint: Schema.optional(Schema.String),
- })
-
- const SelectPrompt = Schema.Struct({
- type: Schema.Literal("select"),
- key: Schema.String,
- message: Schema.String,
- options: Schema.Array(SelectOption),
- when: Schema.optional(When),
- })
-
- const Prompt = Schema.Union([TextPrompt, SelectPrompt])
-
- export class Method extends Schema.Class<Method>("ProviderAuthMethod")({
- type: Schema.Literals(["oauth", "api"]),
- label: Schema.String,
- prompts: Schema.optional(Schema.Array(Prompt)),
- }) {
- static readonly zod = zod(this)
- }
-
- export const Methods = Schema.Record(Schema.String, Schema.Array(Method)).pipe(withStatics((s) => ({ zod: zod(s) })))
- export type Methods = typeof Methods.Type
-
- export class Authorization extends Schema.Class<Authorization>("ProviderAuthAuthorization")({
- url: Schema.String,
- method: Schema.Literals(["auto", "code"]),
- instructions: Schema.String,
- }) {
- static readonly zod = zod(this)
- }
-
- export const OauthMissing = NamedError.create("ProviderAuthOauthMissing", z.object({ providerID: ProviderID.zod }))
-
- export const OauthCodeMissing = NamedError.create(
- "ProviderAuthOauthCodeMissing",
- z.object({ providerID: ProviderID.zod }),
- )
-
- export const OauthCallbackFailed = NamedError.create("ProviderAuthOauthCallbackFailed", z.object({}))
-
- export const ValidationFailed = NamedError.create(
- "ProviderAuthValidationFailed",
- z.object({
- field: z.string(),
- message: z.string(),
- }),
- )
-
- export type Error =
- | Auth.AuthError
- | InstanceType<typeof OauthMissing>
- | InstanceType<typeof OauthCodeMissing>
- | InstanceType<typeof OauthCallbackFailed>
- | InstanceType<typeof ValidationFailed>
-
- type Hook = NonNullable<Hooks["auth"]>
-
- export interface Interface {
- readonly methods: () => Effect.Effect<Methods>
- readonly authorize: (input: {
- providerID: ProviderID
- method: number
- inputs?: Record<string, string>
- }) => Effect.Effect<Authorization | undefined, Error>
- readonly callback: (input: { providerID: ProviderID; method: number; code?: string }) => Effect.Effect<void, Error>
- }
-
- interface State {
- hooks: Record<ProviderID, Hook>
- pending: Map<ProviderID, AuthOAuthResult>
- }
-
- export class Service extends Context.Service<Service, Interface>()("@opencode/ProviderAuth") {}
-
- export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> = Layer.effect(
- Service,
- Effect.gen(function* () {
- const auth = yield* Auth.Service
- const plugin = yield* Plugin.Service
- const state = yield* InstanceState.make<State>(
- Effect.fn("ProviderAuth.state")(function* () {
- const plugins = yield* plugin.list()
- return {
- hooks: Record.fromEntries(
- Arr.filterMap(plugins, (x) =>
- x.auth?.provider !== undefined
- ? Result.succeed([ProviderID.make(x.auth.provider), x.auth] as const)
- : Result.failVoid,
- ),
- ),
- pending: new Map<ProviderID, AuthOAuthResult>(),
- }
- }),
- )
+const When = Schema.Struct({
+ key: Schema.String,
+ op: Schema.Literals(["eq", "neq"]),
+ value: Schema.String,
+})
+
+const TextPrompt = Schema.Struct({
+ type: Schema.Literal("text"),
+ key: Schema.String,
+ message: Schema.String,
+ placeholder: Schema.optional(Schema.String),
+ when: Schema.optional(When),
+})
+
+const SelectOption = Schema.Struct({
+ label: Schema.String,
+ value: Schema.String,
+ hint: Schema.optional(Schema.String),
+})
+
+const SelectPrompt = Schema.Struct({
+ type: Schema.Literal("select"),
+ key: Schema.String,
+ message: Schema.String,
+ options: Schema.Array(SelectOption),
+ when: Schema.optional(When),
+})
+
+const Prompt = Schema.Union([TextPrompt, SelectPrompt])
+
+export class Method extends Schema.Class<Method>("ProviderAuthMethod")({
+ type: Schema.Literals(["oauth", "api"]),
+ label: Schema.String,
+ prompts: Schema.optional(Schema.Array(Prompt)),
+}) {
+ static readonly zod = zod(this)
+}
- const decode = Schema.decodeUnknownSync(Methods)
- const methods = Effect.fn("ProviderAuth.methods")(function* () {
- const hooks = (yield* InstanceState.get(state)).hooks
- return decode(
- Record.map(hooks, (item) =>
- item.methods.map((method) => ({
- type: method.type,
- label: method.label,
- prompts: method.prompts?.map((prompt) => {
- if (prompt.type === "select") {
- return {
- type: "select" as const,
- key: prompt.key,
- message: prompt.message,
- options: prompt.options,
- when: prompt.when,
- }
- }
+export const Methods = Schema.Record(Schema.String, Schema.Array(Method)).pipe(withStatics((s) => ({ zod: zod(s) })))
+export type Methods = typeof Methods.Type
+
+export class Authorization extends Schema.Class<Authorization>("ProviderAuthAuthorization")({
+ url: Schema.String,
+ method: Schema.Literals(["auto", "code"]),
+ instructions: Schema.String,
+}) {
+ static readonly zod = zod(this)
+}
+
+export const OauthMissing = NamedError.create("ProviderAuthOauthMissing", z.object({ providerID: ProviderID.zod }))
+
+export const OauthCodeMissing = NamedError.create(
+ "ProviderAuthOauthCodeMissing",
+ z.object({ providerID: ProviderID.zod }),
+)
+
+export const OauthCallbackFailed = NamedError.create("ProviderAuthOauthCallbackFailed", z.object({}))
+
+export const ValidationFailed = NamedError.create(
+ "ProviderAuthValidationFailed",
+ z.object({
+ field: z.string(),
+ message: z.string(),
+ }),
+)
+
+export type Error =
+ | Auth.AuthError
+ | InstanceType<typeof OauthMissing>
+ | InstanceType<typeof OauthCodeMissing>
+ | InstanceType<typeof OauthCallbackFailed>
+ | InstanceType<typeof ValidationFailed>
+
+type Hook = NonNullable<Hooks["auth"]>
+
+export interface Interface {
+ readonly methods: () => Effect.Effect<Methods>
+ readonly authorize: (input: {
+ providerID: ProviderID
+ method: number
+ inputs?: Record<string, string>
+ }) => Effect.Effect<Authorization | undefined, Error>
+ readonly callback: (input: { providerID: ProviderID; method: number; code?: string }) => Effect.Effect<void, Error>
+}
+
+interface State {
+ hooks: Record<ProviderID, Hook>
+ pending: Map<ProviderID, AuthOAuthResult>
+}
+
+export class Service extends Context.Service<Service, Interface>()("@opencode/ProviderAuth") {}
+
+export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> = Layer.effect(
+ Service,
+ Effect.gen(function* () {
+ const auth = yield* Auth.Service
+ const plugin = yield* Plugin.Service
+ const state = yield* InstanceState.make<State>(
+ Effect.fn("ProviderAuth.state")(function* () {
+ const plugins = yield* plugin.list()
+ return {
+ hooks: Record.fromEntries(
+ Arr.filterMap(plugins, (x) =>
+ x.auth?.provider !== undefined
+ ? Result.succeed([ProviderID.make(x.auth.provider), x.auth] as const)
+ : Result.failVoid,
+ ),
+ ),
+ pending: new Map<ProviderID, AuthOAuthResult>(),
+ }
+ }),
+ )
+
+ const decode = Schema.decodeUnknownSync(Methods)
+ const methods = Effect.fn("ProviderAuth.methods")(function* () {
+ const hooks = (yield* InstanceState.get(state)).hooks
+ return decode(
+ Record.map(hooks, (item) =>
+ item.methods.map((method) => ({
+ type: method.type,
+ label: method.label,
+ prompts: method.prompts?.map((prompt) => {
+ if (prompt.type === "select") {
return {
- type: "text" as const,
+ type: "select" as const,
key: prompt.key,
message: prompt.message,
- placeholder: prompt.placeholder,
+ options: prompt.options,
when: prompt.when,
}
- }),
- })),
- ),
- )
- })
-
- const authorize = Effect.fn("ProviderAuth.authorize")(function* (input: {
- providerID: ProviderID
- method: number
- inputs?: Record<string, string>
- }) {
- const { hooks, pending } = yield* InstanceState.get(state)
- const method = hooks[input.providerID].methods[input.method]
- if (method.type !== "oauth") return
-
- if (method.prompts && input.inputs) {
- for (const prompt of method.prompts) {
- if (prompt.type === "text" && prompt.validate && input.inputs[prompt.key] !== undefined) {
- const error = prompt.validate(input.inputs[prompt.key])
- if (error) return yield* Effect.fail(new ValidationFailed({ field: prompt.key, message: error }))
- }
- }
- }
-
- const result = yield* Effect.promise(() => method.authorize(input.inputs))
- pending.set(input.providerID, result)
- return {
- url: result.url,
- method: result.method,
- instructions: result.instructions,
- }
- })
-
- const callback = Effect.fn("ProviderAuth.callback")(function* (input: {
- providerID: ProviderID
- method: number
- code?: string
- }) {
- const pending = (yield* InstanceState.get(state)).pending
- const match = pending.get(input.providerID)
- if (!match) return yield* Effect.fail(new OauthMissing({ providerID: input.providerID }))
- if (match.method === "code" && !input.code) {
- return yield* Effect.fail(new OauthCodeMissing({ providerID: input.providerID }))
- }
-
- const result = yield* Effect.promise(() =>
- match.method === "code" ? match.callback(input.code!) : match.callback(),
- )
- if (!result || result.type !== "success") return yield* Effect.fail(new OauthCallbackFailed({}))
-
- if ("key" in result) {
- yield* auth.set(input.providerID, {
- type: "api",
- key: result.key,
- })
- }
+ }
+ return {
+ type: "text" as const,
+ key: prompt.key,
+ message: prompt.message,
+ placeholder: prompt.placeholder,
+ when: prompt.when,
+ }
+ }),
+ })),
+ ),
+ )
+ })
- if ("refresh" in result) {
- const { type: _, provider: __, refresh, access, expires, ...extra } = result
- yield* auth.set(input.providerID, {
- type: "oauth",
- access,
- refresh,
- expires,
- ...extra,
- })
+ const authorize = Effect.fn("ProviderAuth.authorize")(function* (input: {
+ providerID: ProviderID
+ method: number
+ inputs?: Record<string, string>
+ }) {
+ const { hooks, pending } = yield* InstanceState.get(state)
+ const method = hooks[input.providerID].methods[input.method]
+ if (method.type !== "oauth") return
+
+ if (method.prompts && input.inputs) {
+ for (const prompt of method.prompts) {
+ if (prompt.type === "text" && prompt.validate && input.inputs[prompt.key] !== undefined) {
+ const error = prompt.validate(input.inputs[prompt.key])
+ if (error) return yield* Effect.fail(new ValidationFailed({ field: prompt.key, message: error }))
+ }
}
- })
-
- return Service.of({ methods, authorize, callback })
- }),
- )
-
- export const defaultLayer = Layer.suspend(() =>
- layer.pipe(Layer.provide(Auth.defaultLayer), Layer.provide(Plugin.defaultLayer)),
- )
-}
+ }
+
+ const result = yield* Effect.promise(() => method.authorize(input.inputs))
+ pending.set(input.providerID, result)
+ return {
+ url: result.url,
+ method: result.method,
+ instructions: result.instructions,
+ }
+ })
+
+ const callback = Effect.fn("ProviderAuth.callback")(function* (input: {
+ providerID: ProviderID
+ method: number
+ code?: string
+ }) {
+ const pending = (yield* InstanceState.get(state)).pending
+ const match = pending.get(input.providerID)
+ if (!match) return yield* Effect.fail(new OauthMissing({ providerID: input.providerID }))
+ if (match.method === "code" && !input.code) {
+ return yield* Effect.fail(new OauthCodeMissing({ providerID: input.providerID }))
+ }
+
+ const result = yield* Effect.promise(() =>
+ match.method === "code" ? match.callback(input.code!) : match.callback(),
+ )
+ if (!result || result.type !== "success") return yield* Effect.fail(new OauthCallbackFailed({}))
+
+ if ("key" in result) {
+ yield* auth.set(input.providerID, {
+ type: "api",
+ key: result.key,
+ })
+ }
+
+ if ("refresh" in result) {
+ const { type: _, provider: __, refresh, access, expires, ...extra } = result
+ yield* auth.set(input.providerID, {
+ type: "oauth",
+ access,
+ refresh,
+ expires,
+ ...extra,
+ })
+ }
+ })
+
+ return Service.of({ methods, authorize, callback })
+ }),
+)
+
+export const defaultLayer = Layer.suspend(() =>
+ layer.pipe(Layer.provide(Auth.defaultLayer), Layer.provide(Plugin.defaultLayer)),
+)
diff --git a/packages/opencode/src/provider/error.ts b/packages/opencode/src/provider/error.ts
index 52e525177..42378b686 100644
--- a/packages/opencode/src/provider/error.ts
+++ b/packages/opencode/src/provider/error.ts
@@ -3,195 +3,193 @@ import { STATUS_CODES } from "http"
import { iife } from "@/util/iife"
import type { ProviderID } from "./schema"
-export namespace ProviderError {
- // Adapted from overflow detection patterns in:
- // https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/overflow.ts
- const OVERFLOW_PATTERNS = [
- /prompt is too long/i, // Anthropic
- /input is too long for requested model/i, // Amazon Bedrock
- /exceeds the context window/i, // OpenAI (Completions + Responses API message text)
- /input token count.*exceeds the maximum/i, // Google (Gemini)
- /maximum prompt length is \d+/i, // xAI (Grok)
- /reduce the length of the messages/i, // Groq
- /maximum context length is \d+ tokens/i, // OpenRouter, DeepSeek, vLLM
- /exceeds the limit of \d+/i, // GitHub Copilot
- /exceeds the available context size/i, // llama.cpp server
- /greater than the context length/i, // LM Studio
- /context window exceeds limit/i, // MiniMax
- /exceeded model token limit/i, // Kimi For Coding, Moonshot
- /context[_ ]length[_ ]exceeded/i, // Generic fallback
- /request entity too large/i, // HTTP 413
- /context length is only \d+ tokens/i, // vLLM
- /input length.*exceeds.*context length/i, // vLLM
- /prompt too long; exceeded (?:max )?context length/i, // Ollama explicit overflow error
- /too large for model with \d+ maximum context length/i, // Mistral
- /model_context_window_exceeded/i, // z.ai non-standard finish_reason surfaced as error text
- ]
-
- function isOpenAiErrorRetryable(e: APICallError) {
- const status = e.statusCode
- if (!status) return e.isRetryable
- // openai sometimes returns 404 for models that are actually available
- return status === 404 || e.isRetryable
- }
+// Adapted from overflow detection patterns in:
+// https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/overflow.ts
+const OVERFLOW_PATTERNS = [
+ /prompt is too long/i, // Anthropic
+ /input is too long for requested model/i, // Amazon Bedrock
+ /exceeds the context window/i, // OpenAI (Completions + Responses API message text)
+ /input token count.*exceeds the maximum/i, // Google (Gemini)
+ /maximum prompt length is \d+/i, // xAI (Grok)
+ /reduce the length of the messages/i, // Groq
+ /maximum context length is \d+ tokens/i, // OpenRouter, DeepSeek, vLLM
+ /exceeds the limit of \d+/i, // GitHub Copilot
+ /exceeds the available context size/i, // llama.cpp server
+ /greater than the context length/i, // LM Studio
+ /context window exceeds limit/i, // MiniMax
+ /exceeded model token limit/i, // Kimi For Coding, Moonshot
+ /context[_ ]length[_ ]exceeded/i, // Generic fallback
+ /request entity too large/i, // HTTP 413
+ /context length is only \d+ tokens/i, // vLLM
+ /input length.*exceeds.*context length/i, // vLLM
+ /prompt too long; exceeded (?:max )?context length/i, // Ollama explicit overflow error
+ /too large for model with \d+ maximum context length/i, // Mistral
+ /model_context_window_exceeded/i, // z.ai non-standard finish_reason surfaced as error text
+]
+
+function isOpenAiErrorRetryable(e: APICallError) {
+ const status = e.statusCode
+ if (!status) return e.isRetryable
+ // openai sometimes returns 404 for models that are actually available
+ return status === 404 || e.isRetryable
+}
- // Providers not reliably handled in this function:
- // - z.ai: can accept overflow silently (needs token-count/context-window checks)
- function isOverflow(message: string) {
- if (OVERFLOW_PATTERNS.some((p) => p.test(message))) return true
+// Providers not reliably handled in this function:
+// - z.ai: can accept overflow silently (needs token-count/context-window checks)
+function isOverflow(message: string) {
+ if (OVERFLOW_PATTERNS.some((p) => p.test(message))) return true
- // Providers/status patterns handled outside of regex list:
- // - Cerebras: often returns "400 (no body)" / "413 (no body)"
- // - Mistral: often returns "400 (no body)" / "413 (no body)"
- return /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message)
- }
+ // Providers/status patterns handled outside of regex list:
+ // - Cerebras: often returns "400 (no body)" / "413 (no body)"
+ // - Mistral: often returns "400 (no body)" / "413 (no body)"
+ return /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message)
+}
- function message(providerID: ProviderID, e: APICallError) {
- return iife(() => {
- const msg = e.message
- if (msg === "") {
- if (e.responseBody) return e.responseBody
- if (e.statusCode) {
- const err = STATUS_CODES[e.statusCode]
- if (err) return err
- }
- return "Unknown error"
+function message(providerID: ProviderID, e: APICallError) {
+ return iife(() => {
+ const msg = e.message
+ if (msg === "") {
+ if (e.responseBody) return e.responseBody
+ if (e.statusCode) {
+ const err = STATUS_CODES[e.statusCode]
+ if (err) return err
}
+ return "Unknown error"
+ }
- if (!e.responseBody || (e.statusCode && msg !== STATUS_CODES[e.statusCode])) {
- return msg
+ if (!e.responseBody || (e.statusCode && msg !== STATUS_CODES[e.statusCode])) {
+ return msg
+ }
+
+ try {
+ const body = JSON.parse(e.responseBody)
+ // try to extract common error message fields
+ const errMsg = body.message || body.error || body.error?.message
+ if (errMsg && typeof errMsg === "string") {
+ return `${msg}: ${errMsg}`
}
+ } catch {}
- try {
- const body = JSON.parse(e.responseBody)
- // try to extract common error message fields
- const errMsg = body.message || body.error || body.error?.message
- if (errMsg && typeof errMsg === "string") {
- return `${msg}: ${errMsg}`
- }
- } catch {}
-
- // If responseBody is HTML (e.g. from a gateway or proxy error page),
- // provide a human-readable message instead of dumping raw markup
- if (/^\s*<!doctype|^\s*<html/i.test(e.responseBody)) {
- if (e.statusCode === 401) {
- return "Unauthorized: request was blocked by a gateway or proxy. Your authentication token may be missing or expired — try running `opencode auth login <your provider URL>` to re-authenticate."
- }
- if (e.statusCode === 403) {
- return "Forbidden: request was blocked by a gateway or proxy. You may not have permission to access this resource — check your account and provider settings."
- }
- return msg
+ // If responseBody is HTML (e.g. from a gateway or proxy error page),
+ // provide a human-readable message instead of dumping raw markup
+ if (/^\s*<!doctype|^\s*<html/i.test(e.responseBody)) {
+ if (e.statusCode === 401) {
+ return "Unauthorized: request was blocked by a gateway or proxy. Your authentication token may be missing or expired — try running `opencode auth login <your provider URL>` to re-authenticate."
}
+ if (e.statusCode === 403) {
+ return "Forbidden: request was blocked by a gateway or proxy. You may not have permission to access this resource — check your account and provider settings."
+ }
+ return msg
+ }
- return `${msg}: ${e.responseBody}`
- }).trim()
+ return `${msg}: ${e.responseBody}`
+ }).trim()
+}
+
+function json(input: unknown) {
+ if (typeof input === "string") {
+ try {
+ const result = JSON.parse(input)
+ if (result && typeof result === "object") return result
+ return undefined
+ } catch {
+ return undefined
+ }
+ }
+ if (typeof input === "object" && input !== null) {
+ return input
}
+ return undefined
+}
- function json(input: unknown) {
- if (typeof input === "string") {
- try {
- const result = JSON.parse(input)
- if (result && typeof result === "object") return result
- return undefined
- } catch {
- return undefined
- }
+export type ParsedStreamError =
+ | {
+ type: "context_overflow"
+ message: string
+ responseBody: string
}
- if (typeof input === "object" && input !== null) {
- return input
+ | {
+ type: "api_error"
+ message: string
+ isRetryable: false
+ responseBody: string
}
- return undefined
- }
- export type ParsedStreamError =
- | {
- type: "context_overflow"
- message: string
- responseBody: string
- }
- | {
- type: "api_error"
- message: string
- isRetryable: false
- responseBody: string
- }
+export function parseStreamError(input: unknown): ParsedStreamError | undefined {
+ const body = json(input)
+ if (!body) return
- export function parseStreamError(input: unknown): ParsedStreamError | undefined {
- const body = json(input)
- if (!body) return
-
- const responseBody = JSON.stringify(body)
- if (body.type !== "error") return
-
- switch (body?.error?.code) {
- case "context_length_exceeded":
- return {
- type: "context_overflow",
- message: "Input exceeds context window of this model",
- responseBody,
- }
- case "insufficient_quota":
- return {
- type: "api_error",
- message: "Quota exceeded. Check your plan and billing details.",
- isRetryable: false,
- responseBody,
- }
- case "usage_not_included":
- return {
- type: "api_error",
- message: "To use Codex with your ChatGPT plan, upgrade to Plus: https://chatgpt.com/explore/plus.",
- isRetryable: false,
- responseBody,
- }
- case "invalid_prompt":
- return {
- type: "api_error",
- message: typeof body?.error?.message === "string" ? body?.error?.message : "Invalid prompt.",
- isRetryable: false,
- responseBody,
- }
- }
- }
+ const responseBody = JSON.stringify(body)
+ if (body.type !== "error") return
- export type ParsedAPICallError =
- | {
- type: "context_overflow"
- message: string
- responseBody?: string
+ switch (body?.error?.code) {
+ case "context_length_exceeded":
+ return {
+ type: "context_overflow",
+ message: "Input exceeds context window of this model",
+ responseBody,
}
- | {
- type: "api_error"
- message: string
- statusCode?: number
- isRetryable: boolean
- responseHeaders?: Record<string, string>
- responseBody?: string
- metadata?: Record<string, string>
+ case "insufficient_quota":
+ return {
+ type: "api_error",
+ message: "Quota exceeded. Check your plan and billing details.",
+ isRetryable: false,
+ responseBody,
}
-
- export function parseAPICallError(input: { providerID: ProviderID; error: APICallError }): ParsedAPICallError {
- const m = message(input.providerID, input.error)
- const body = json(input.error.responseBody)
- if (isOverflow(m) || input.error.statusCode === 413 || body?.error?.code === "context_length_exceeded") {
+ case "usage_not_included":
return {
- type: "context_overflow",
- message: m,
- responseBody: input.error.responseBody,
+ type: "api_error",
+ message: "To use Codex with your ChatGPT plan, upgrade to Plus: https://chatgpt.com/explore/plus.",
+ isRetryable: false,
+ responseBody,
}
+ case "invalid_prompt":
+ return {
+ type: "api_error",
+ message: typeof body?.error?.message === "string" ? body?.error?.message : "Invalid prompt.",
+ isRetryable: false,
+ responseBody,
+ }
+ }
+}
+
+export type ParsedAPICallError =
+ | {
+ type: "context_overflow"
+ message: string
+ responseBody?: string
+ }
+ | {
+ type: "api_error"
+ message: string
+ statusCode?: number
+ isRetryable: boolean
+ responseHeaders?: Record<string, string>
+ responseBody?: string
+ metadata?: Record<string, string>
}
- const metadata = input.error.url ? { url: input.error.url } : undefined
+export function parseAPICallError(input: { providerID: ProviderID; error: APICallError }): ParsedAPICallError {
+ const m = message(input.providerID, input.error)
+ const body = json(input.error.responseBody)
+ if (isOverflow(m) || input.error.statusCode === 413 || body?.error?.code === "context_length_exceeded") {
return {
- type: "api_error",
+ type: "context_overflow",
message: m,
- statusCode: input.error.statusCode,
- isRetryable: input.providerID.startsWith("openai")
- ? isOpenAiErrorRetryable(input.error)
- : input.error.isRetryable,
- responseHeaders: input.error.responseHeaders,
responseBody: input.error.responseBody,
- metadata,
}
}
+
+ const metadata = input.error.url ? { url: input.error.url } : undefined
+ return {
+ type: "api_error",
+ message: m,
+ statusCode: input.error.statusCode,
+ isRetryable: input.providerID.startsWith("openai")
+ ? isOpenAiErrorRetryable(input.error)
+ : input.error.isRetryable,
+ responseHeaders: input.error.responseHeaders,
+ responseBody: input.error.responseBody,
+ metadata,
+ }
}
diff --git a/packages/opencode/src/provider/index.ts b/packages/opencode/src/provider/index.ts
index 3c0174548..9e8891144 100644
--- a/packages/opencode/src/provider/index.ts
+++ b/packages/opencode/src/provider/index.ts
@@ -1 +1,5 @@
export * as Provider from "./provider"
+export * as ProviderAuth from "./auth"
+export * as ProviderError from "./error"
+export * as ModelsDev from "./models"
+export * as ProviderTransform from "./transform"
diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts
index 245730e00..2924666c0 100644
--- a/packages/opencode/src/provider/models.ts
+++ b/packages/opencode/src/provider/models.ts
@@ -13,169 +13,167 @@ import { Hash } from "@opencode-ai/shared/util/hash"
// Falls back to undefined in dev mode when snapshot doesn't exist
/* @ts-ignore */
-export namespace ModelsDev {
- const log = Log.create({ service: "models.dev" })
- const source = url()
- const filepath = path.join(
- Global.Path.cache,
- source === "https://models.dev" ? "models.json" : `models-${Hash.fast(source)}.json`,
- )
- const ttl = 5 * 60 * 1000
-
- type JsonValue = string | number | boolean | null | { [key: string]: JsonValue } | JsonValue[]
-
- const JsonValue: z.ZodType<JsonValue> = z.lazy(() =>
- z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(JsonValue), z.record(z.string(), JsonValue)]),
- )
-
- const Cost = z.object({
- input: z.number(),
- output: z.number(),
- cache_read: z.number().optional(),
- cache_write: z.number().optional(),
- context_over_200k: z
- .object({
- input: z.number(),
- output: z.number(),
- cache_read: z.number().optional(),
- cache_write: z.number().optional(),
- })
- .optional(),
- })
-
- export const Model = z.object({
- id: z.string(),
- name: z.string(),
- family: z.string().optional(),
- release_date: z.string(),
- attachment: z.boolean(),
- reasoning: z.boolean(),
- temperature: z.boolean(),
- tool_call: z.boolean(),
- interleaved: z
- .union([
- z.literal(true),
- z
- .object({
- field: z.enum(["reasoning_content", "reasoning_details"]),
- })
- .strict(),
- ])
- .optional(),
- cost: Cost.optional(),
- limit: z.object({
- context: z.number(),
- input: z.number().optional(),
+const log = Log.create({ service: "models.dev" })
+const source = url()
+const filepath = path.join(
+ Global.Path.cache,
+ source === "https://models.dev" ? "models.json" : `models-${Hash.fast(source)}.json`,
+)
+const ttl = 5 * 60 * 1000
+
+type JsonValue = string | number | boolean | null | { [key: string]: JsonValue } | JsonValue[]
+
+const JsonValue: z.ZodType<JsonValue> = z.lazy(() =>
+ z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(JsonValue), z.record(z.string(), JsonValue)]),
+)
+
+const Cost = z.object({
+ input: z.number(),
+ output: z.number(),
+ cache_read: z.number().optional(),
+ cache_write: z.number().optional(),
+ context_over_200k: z
+ .object({
+ input: z.number(),
output: z.number(),
- }),
- modalities: z
- .object({
- input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
- output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
- })
- .optional(),
- experimental: z
- .object({
- modes: z
- .record(
- z.string(),
- z.object({
- cost: Cost.optional(),
- provider: z
- .object({
- body: z.record(z.string(), JsonValue).optional(),
- headers: z.record(z.string(), z.string()).optional(),
- })
- .optional(),
- }),
- )
- .optional(),
- })
- .optional(),
- status: z.enum(["alpha", "beta", "deprecated"]).optional(),
- provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(),
- })
- export type Model = z.infer<typeof Model>
-
- export const Provider = z.object({
- api: z.string().optional(),
- name: z.string(),
- env: z.array(z.string()),
- id: z.string(),
- npm: z.string().optional(),
- models: z.record(z.string(), Model),
- })
-
- export type Provider = z.infer<typeof Provider>
-
- function url() {
- return Flag.OPENCODE_MODELS_URL || "https://models.dev"
- }
+ cache_read: z.number().optional(),
+ cache_write: z.number().optional(),
+ })
+ .optional(),
+})
+
+export const Model = z.object({
+ id: z.string(),
+ name: z.string(),
+ family: z.string().optional(),
+ release_date: z.string(),
+ attachment: z.boolean(),
+ reasoning: z.boolean(),
+ temperature: z.boolean(),
+ tool_call: z.boolean(),
+ interleaved: z
+ .union([
+ z.literal(true),
+ z
+ .object({
+ field: z.enum(["reasoning_content", "reasoning_details"]),
+ })
+ .strict(),
+ ])
+ .optional(),
+ cost: Cost.optional(),
+ limit: z.object({
+ context: z.number(),
+ input: z.number().optional(),
+ output: z.number(),
+ }),
+ modalities: z
+ .object({
+ input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
+ output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
+ })
+ .optional(),
+ experimental: z
+ .object({
+ modes: z
+ .record(
+ z.string(),
+ z.object({
+ cost: Cost.optional(),
+ provider: z
+ .object({
+ body: z.record(z.string(), JsonValue).optional(),
+ headers: z.record(z.string(), z.string()).optional(),
+ })
+ .optional(),
+ }),
+ )
+ .optional(),
+ })
+ .optional(),
+ status: z.enum(["alpha", "beta", "deprecated"]).optional(),
+ provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(),
+})
+export type Model = z.infer<typeof Model>
+
+export const Provider = z.object({
+ api: z.string().optional(),
+ name: z.string(),
+ env: z.array(z.string()),
+ id: z.string(),
+ npm: z.string().optional(),
+ models: z.record(z.string(), Model),
+})
+
+export type Provider = z.infer<typeof Provider>
+
+function url() {
+ return Flag.OPENCODE_MODELS_URL || "https://models.dev"
+}
- function fresh() {
- return Date.now() - Number(Filesystem.stat(filepath)?.mtimeMs ?? 0) < ttl
- }
+function fresh() {
+ return Date.now() - Number(Filesystem.stat(filepath)?.mtimeMs ?? 0) < ttl
+}
- function skip(force: boolean) {
- return !force && fresh()
- }
+function skip(force: boolean) {
+ return !force && fresh()
+}
- const fetchApi = async () => {
- const result = await fetch(`${url()}/api.json`, {
- headers: { "User-Agent": Installation.USER_AGENT },
- signal: AbortSignal.timeout(10000),
- })
- return { ok: result.ok, text: await result.text() }
- }
+const fetchApi = async () => {
+ const result = await fetch(`${url()}/api.json`, {
+ headers: { "User-Agent": Installation.USER_AGENT },
+ signal: AbortSignal.timeout(10000),
+ })
+ return { ok: result.ok, text: await result.text() }
+}
- export const Data = lazy(async () => {
+export const Data = lazy(async () => {
+ const result = await Filesystem.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).catch(() => {})
+ if (result) return result
+ // @ts-ignore
+ const snapshot = await import("./models-snapshot.js")
+ .then((m) => m.snapshot as Record<string, unknown>)
+ .catch(() => undefined)
+ if (snapshot) return snapshot
+ if (Flag.OPENCODE_DISABLE_MODELS_FETCH) return {}
+ return Flock.withLock(`models-dev:${filepath}`, async () => {
const result = await Filesystem.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).catch(() => {})
if (result) return result
- // @ts-ignore
- const snapshot = await import("./models-snapshot.js")
- .then((m) => m.snapshot as Record<string, unknown>)
- .catch(() => undefined)
- if (snapshot) return snapshot
- if (Flag.OPENCODE_DISABLE_MODELS_FETCH) return {}
- return Flock.withLock(`models-dev:${filepath}`, async () => {
- const result = await Filesystem.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).catch(() => {})
- if (result) return result
- const result2 = await fetchApi()
- if (result2.ok) {
- await Filesystem.write(filepath, result2.text).catch((e) => {
- log.error("Failed to write models cache", { error: e })
- })
- }
- return JSON.parse(result2.text)
- })
+ const result2 = await fetchApi()
+ if (result2.ok) {
+ await Filesystem.write(filepath, result2.text).catch((e) => {
+ log.error("Failed to write models cache", { error: e })
+ })
+ }
+ return JSON.parse(result2.text)
})
+})
- export async function get() {
- const result = await Data()
- return result as Record<string, Provider>
- }
-
- export async function refresh(force = false) {
- if (skip(force)) return ModelsDev.Data.reset()
- await Flock.withLock(`models-dev:${filepath}`, async () => {
- if (skip(force)) return ModelsDev.Data.reset()
- const result = await fetchApi()
- if (!result.ok) return
- await Filesystem.write(filepath, result.text)
- ModelsDev.Data.reset()
- }).catch((e) => {
- log.error("Failed to fetch models.dev", {
- error: e,
- })
+export async function get() {
+ const result = await Data()
+ return result as Record<string, Provider>
+}
+
+export async function refresh(force = false) {
+ if (skip(force)) return Data.reset()
+ await Flock.withLock(`models-dev:${filepath}`, async () => {
+ if (skip(force)) return Data.reset()
+ const result = await fetchApi()
+ if (!result.ok) return
+ await Filesystem.write(filepath, result.text)
+ Data.reset()
+ }).catch((e) => {
+ log.error("Failed to fetch models.dev", {
+ error: e,
})
- }
+ })
}
if (!Flag.OPENCODE_DISABLE_MODELS_FETCH && !process.argv.includes("--get-yargs-completions")) {
- void ModelsDev.refresh()
+ void refresh()
setInterval(
async () => {
- await ModelsDev.refresh()
+ await refresh()
},
60 * 1000 * 60,
).unref()
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
index 432dbab34..77a45cb1b 100644
--- a/packages/opencode/src/provider/provider.ts
+++ b/packages/opencode/src/provider/provider.ts
@@ -10,7 +10,7 @@ import { Hash } from "@opencode-ai/shared/util/hash"
import { Plugin } from "../plugin"
import { NamedError } from "@opencode-ai/shared/util/error"
import { type LanguageModelV3 } from "@ai-sdk/provider"
-import { ModelsDev } from "./models"
+import * as ModelsDev from "./models"
import { Auth } from "../auth"
import { Env } from "../env"
import { Instance } from "../project/instance"
@@ -55,7 +55,7 @@ import {
} from "gitlab-ai-provider"
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
import { GoogleAuth } from "google-auth-library"
-import { ProviderTransform } from "./transform"
+import * as ProviderTransform from "./transform"
import { Installation } from "../installation"
import { ModelID, ProviderID } from "./schema"
diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts
index 5fa39441c..52632f075 100644
--- a/packages/opencode/src/provider/transform.ts
+++ b/packages/opencode/src/provider/transform.ts
@@ -3,7 +3,7 @@ import { mergeDeep, unique } from "remeda"
import type { JSONSchema7 } from "@ai-sdk/provider"
import type { JSONSchema } from "zod/v4/core"
import type * as Provider from "./provider"
-import type { ModelsDev } from "./models"
+import type * as ModelsDev from "./models"
import { iife } from "@/util/iife"
import { Flag } from "@/flag/flag"
@@ -17,570 +17,420 @@ function mimeToModality(mime: string): Modality | undefined {
return undefined
}
-export namespace ProviderTransform {
- export const OUTPUT_TOKEN_MAX = Flag.OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX || 32_000
-
- // Maps npm package to the key the AI SDK expects for providerOptions
- function sdkKey(npm: string): string | undefined {
- switch (npm) {
- case "@ai-sdk/github-copilot":
- return "copilot"
- case "@ai-sdk/azure":
- return "azure"
- case "@ai-sdk/openai":
- return "openai"
- case "@ai-sdk/amazon-bedrock":
- return "bedrock"
- case "@ai-sdk/anthropic":
- case "@ai-sdk/google-vertex/anthropic":
- return "anthropic"
- case "@ai-sdk/google-vertex":
- return "vertex"
- case "@ai-sdk/google":
- return "google"
- case "@ai-sdk/gateway":
- return "gateway"
- case "@openrouter/ai-sdk-provider":
- return "openrouter"
- }
- return undefined
+export const OUTPUT_TOKEN_MAX = Flag.OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX || 32_000
+
+// Maps npm package to the key the AI SDK expects for providerOptions
+function sdkKey(npm: string): string | undefined {
+ switch (npm) {
+ case "@ai-sdk/github-copilot":
+ return "copilot"
+ case "@ai-sdk/azure":
+ return "azure"
+ case "@ai-sdk/openai":
+ return "openai"
+ case "@ai-sdk/amazon-bedrock":
+ return "bedrock"
+ case "@ai-sdk/anthropic":
+ case "@ai-sdk/google-vertex/anthropic":
+ return "anthropic"
+ case "@ai-sdk/google-vertex":
+ return "vertex"
+ case "@ai-sdk/google":
+ return "google"
+ case "@ai-sdk/gateway":
+ return "gateway"
+ case "@openrouter/ai-sdk-provider":
+ return "openrouter"
}
+ return undefined
+}
- function normalizeMessages(
- msgs: ModelMessage[],
- model: Provider.Model,
- _options: Record<string, unknown>,
- ): ModelMessage[] {
- // Anthropic rejects messages with empty content - filter out empty string messages
- // and remove empty text/reasoning parts from array content
- if (model.api.npm === "@ai-sdk/anthropic" || model.api.npm === "@ai-sdk/amazon-bedrock") {
- msgs = msgs
- .map((msg) => {
- if (typeof msg.content === "string") {
- if (msg.content === "") return undefined
- return msg
- }
- if (!Array.isArray(msg.content)) return msg
- const filtered = msg.content.filter((part) => {
- if (part.type === "text" || part.type === "reasoning") {
- return part.text !== ""
- }
- return true
- })
- if (filtered.length === 0) return undefined
- return { ...msg, content: filtered }
- })
- .filter((msg): msg is ModelMessage => msg !== undefined && msg.content !== "")
- }
-
- if (model.api.id.includes("claude")) {
- const scrub = (id: string) => id.replace(/[^a-zA-Z0-9_-]/g, "_")
- msgs = msgs.map((msg) => {
- if (msg.role === "assistant" && Array.isArray(msg.content)) {
- return {
- ...msg,
- content: msg.content.map((part) => {
- if (part.type === "tool-call" || part.type === "tool-result") {
- return { ...part, toolCallId: scrub(part.toolCallId) }
- }
- return part
- }),
- }
+function normalizeMessages(
+ msgs: ModelMessage[],
+ model: Provider.Model,
+ _options: Record<string, unknown>,
+): ModelMessage[] {
+ // Anthropic rejects messages with empty content - filter out empty string messages
+ // and remove empty text/reasoning parts from array content
+ if (model.api.npm === "@ai-sdk/anthropic" || model.api.npm === "@ai-sdk/amazon-bedrock") {
+ msgs = msgs
+ .map((msg) => {
+ if (typeof msg.content === "string") {
+ if (msg.content === "") return undefined
+ return msg
}
- if (msg.role === "tool" && Array.isArray(msg.content)) {
- return {
- ...msg,
- content: msg.content.map((part) => {
- if (part.type === "tool-result") {
- return { ...part, toolCallId: scrub(part.toolCallId) }
- }
- return part
- }),
+ if (!Array.isArray(msg.content)) return msg
+ const filtered = msg.content.filter((part) => {
+ if (part.type === "text" || part.type === "reasoning") {
+ return part.text !== ""
}
- }
- return msg
- })
- }
- if (["@ai-sdk/anthropic", "@ai-sdk/google-vertex/anthropic"].includes(model.api.npm)) {
- // Anthropic rejects assistant turns where tool_use blocks are followed by non-tool
- // content, e.g. [tool_use, tool_use, text], with:
- // `tool_use` ids were found without `tool_result` blocks immediately after...
- //
- // Reorder that invalid shape into [text] + [tool_use, tool_use]. Consecutive
- // assistant messages are later merged by the provider/SDK, so preserving the
- // original [tool_use...] then [text] order still produces the invalid payload.
- //
- // The root cause appears to be somewhere upstream where the stream is originally
- // processed. We were unable to locate an exact narrower reproduction elsewhere,
- // so we keep this transform in place for the time being.
- msgs = msgs.flatMap((msg) => {
- if (msg.role !== "assistant" || !Array.isArray(msg.content)) return [msg]
-
- const parts = msg.content
- const first = parts.findIndex((part) => part.type === "tool-call")
- if (first === -1) return [msg]
- if (!parts.slice(first).some((part) => part.type !== "tool-call")) return [msg]
- return [
- { ...msg, content: parts.filter((part) => part.type !== "tool-call") },
- { ...msg, content: parts.filter((part) => part.type === "tool-call") },
- ]
+ return true
+ })
+ if (filtered.length === 0) return undefined
+ return { ...msg, content: filtered }
})
- }
- if (
- model.providerID === "mistral" ||
- model.api.id.toLowerCase().includes("mistral") ||
- model.api.id.toLocaleLowerCase().includes("devstral")
- ) {
- const scrub = (id: string) => {
- return id
- .replace(/[^a-zA-Z0-9]/g, "") // Remove non-alphanumeric characters
- .substring(0, 9) // Take first 9 characters
- .padEnd(9, "0") // Pad with zeros if less than 9 characters
- }
- const result: ModelMessage[] = []
- for (let i = 0; i < msgs.length; i++) {
- const msg = msgs[i]
- const nextMsg = msgs[i + 1]
+ .filter((msg): msg is ModelMessage => msg !== undefined && msg.content !== "")
+ }
- if (msg.role === "assistant" && Array.isArray(msg.content)) {
- msg.content = msg.content.map((part) => {
+ if (model.api.id.includes("claude")) {
+ const scrub = (id: string) => id.replace(/[^a-zA-Z0-9_-]/g, "_")
+ msgs = msgs.map((msg) => {
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
+ return {
+ ...msg,
+ content: msg.content.map((part) => {
if (part.type === "tool-call" || part.type === "tool-result") {
return { ...part, toolCallId: scrub(part.toolCallId) }
}
return part
- })
+ }),
}
- if (msg.role === "tool" && Array.isArray(msg.content)) {
- msg.content = msg.content.map((part) => {
+ }
+ if (msg.role === "tool" && Array.isArray(msg.content)) {
+ return {
+ ...msg,
+ content: msg.content.map((part) => {
if (part.type === "tool-result") {
return { ...part, toolCallId: scrub(part.toolCallId) }
}
return part
- })
+ }),
}
- result.push(msg)
+ }
+ return msg
+ })
+ }
+ if (["@ai-sdk/anthropic", "@ai-sdk/google-vertex/anthropic"].includes(model.api.npm)) {
+ // Anthropic rejects assistant turns where tool_use blocks are followed by non-tool
+ // content, e.g. [tool_use, tool_use, text], with:
+ // `tool_use` ids were found without `tool_result` blocks immediately after...
+ //
+ // Reorder that invalid shape into [text] + [tool_use, tool_use]. Consecutive
+ // assistant messages are later merged by the provider/SDK, so preserving the
+ // original [tool_use...] then [text] order still produces the invalid payload.
+ //
+ // The root cause appears to be somewhere upstream where the stream is originally
+ // processed. We were unable to locate an exact narrower reproduction elsewhere,
+ // so we keep this transform in place for the time being.
+ msgs = msgs.flatMap((msg) => {
+ if (msg.role !== "assistant" || !Array.isArray(msg.content)) return [msg]
+
+ const parts = msg.content
+ const first = parts.findIndex((part) => part.type === "tool-call")
+ if (first === -1) return [msg]
+ if (!parts.slice(first).some((part) => part.type !== "tool-call")) return [msg]
+ return [
+ { ...msg, content: parts.filter((part) => part.type !== "tool-call") },
+ { ...msg, content: parts.filter((part) => part.type === "tool-call") },
+ ]
+ })
+ }
+ if (
+ model.providerID === "mistral" ||
+ model.api.id.toLowerCase().includes("mistral") ||
+ model.api.id.toLocaleLowerCase().includes("devstral")
+ ) {
+ const scrub = (id: string) => {
+ return id
+ .replace(/[^a-zA-Z0-9]/g, "") // Remove non-alphanumeric characters
+ .substring(0, 9) // Take first 9 characters
+ .padEnd(9, "0") // Pad with zeros if less than 9 characters
+ }
+ const result: ModelMessage[] = []
+ for (let i = 0; i < msgs.length; i++) {
+ const msg = msgs[i]
+ const nextMsg = msgs[i + 1]
+
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
+ msg.content = msg.content.map((part) => {
+ if (part.type === "tool-call" || part.type === "tool-result") {
+ return { ...part, toolCallId: scrub(part.toolCallId) }
+ }
+ return part
+ })
+ }
+ if (msg.role === "tool" && Array.isArray(msg.content)) {
+ msg.content = msg.content.map((part) => {
+ if (part.type === "tool-result") {
+ return { ...part, toolCallId: scrub(part.toolCallId) }
+ }
+ return part
+ })
+ }
+ result.push(msg)
- // Fix message sequence: tool messages cannot be followed by user messages
- if (msg.role === "tool" && nextMsg?.role === "user") {
- result.push({
- role: "assistant",
- content: [
- {
- type: "text",
- text: "Done.",
- },
- ],
- })
- }
+ // Fix message sequence: tool messages cannot be followed by user messages
+ if (msg.role === "tool" && nextMsg?.role === "user") {
+ result.push({
+ role: "assistant",
+ content: [
+ {
+ type: "text",
+ text: "Done.",
+ },
+ ],
+ })
}
- return result
}
+ return result
+ }
- if (typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field) {
- const field = model.capabilities.interleaved.field
- return msgs.map((msg) => {
- if (msg.role === "assistant" && Array.isArray(msg.content)) {
- const reasoningParts = msg.content.filter((part: any) => part.type === "reasoning")
- const reasoningText = reasoningParts.map((part: any) => part.text).join("")
+ if (typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field) {
+ const field = model.capabilities.interleaved.field
+ return msgs.map((msg) => {
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
+ const reasoningParts = msg.content.filter((part: any) => part.type === "reasoning")
+ const reasoningText = reasoningParts.map((part: any) => part.text).join("")
- // Filter out reasoning parts from content
- const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning")
-
- // Include reasoning_content | reasoning_details directly on the message for all assistant messages
- if (reasoningText) {
- return {
- ...msg,
- content: filteredContent,
- providerOptions: {
- ...msg.providerOptions,
- openaiCompatible: {
- ...(msg.providerOptions as any)?.openaiCompatible,
- [field]: reasoningText,
- },
- },
- }
- }
+ // Filter out reasoning parts from content
+ const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning")
+ // Include reasoning_content | reasoning_details directly on the message for all assistant messages
+ if (reasoningText) {
return {
...msg,
content: filteredContent,
+ providerOptions: {
+ ...msg.providerOptions,
+ openaiCompatible: {
+ ...(msg.providerOptions as any)?.openaiCompatible,
+ [field]: reasoningText,
+ },
+ },
}
}
- return msg
- })
- }
+ return {
+ ...msg,
+ content: filteredContent,
+ }
+ }
- return msgs
+ return msg
+ })
}
- function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
- const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
- const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
-
- const providerOptions = {
- anthropic: {
- cacheControl: { type: "ephemeral" },
- },
- openrouter: {
- cacheControl: { type: "ephemeral" },
- },
- bedrock: {
- cachePoint: { type: "default" },
- },
- openaiCompatible: {
- cache_control: { type: "ephemeral" },
- },
- copilot: {
- copilot_cache_control: { type: "ephemeral" },
- },
- alibaba: {
- cacheControl: { type: "ephemeral" },
- },
- }
+ return msgs
+}
- for (const msg of unique([...system, ...final])) {
- const useMessageLevelOptions =
- model.providerID === "anthropic" ||
- model.providerID.includes("bedrock") ||
- model.api.npm === "@ai-sdk/amazon-bedrock"
- const shouldUseContentOptions = !useMessageLevelOptions && Array.isArray(msg.content) && msg.content.length > 0
-
- if (shouldUseContentOptions) {
- const lastContent = msg.content[msg.content.length - 1]
- if (
- lastContent &&
- typeof lastContent === "object" &&
- lastContent.type !== "tool-approval-request" &&
- lastContent.type !== "tool-approval-response"
- ) {
- lastContent.providerOptions = mergeDeep(lastContent.providerOptions ?? {}, providerOptions)
- continue
- }
- }
+function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
+ const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
+ const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
+
+ const providerOptions = {
+ anthropic: {
+ cacheControl: { type: "ephemeral" },
+ },
+ openrouter: {
+ cacheControl: { type: "ephemeral" },
+ },
+ bedrock: {
+ cachePoint: { type: "default" },
+ },
+ openaiCompatible: {
+ cache_control: { type: "ephemeral" },
+ },
+ copilot: {
+ copilot_cache_control: { type: "ephemeral" },
+ },
+ alibaba: {
+ cacheControl: { type: "ephemeral" },
+ },
+ }
- msg.providerOptions = mergeDeep(msg.providerOptions ?? {}, providerOptions)
+ for (const msg of unique([...system, ...final])) {
+ const useMessageLevelOptions =
+ model.providerID === "anthropic" ||
+ model.providerID.includes("bedrock") ||
+ model.api.npm === "@ai-sdk/amazon-bedrock"
+ const shouldUseContentOptions = !useMessageLevelOptions && Array.isArray(msg.content) && msg.content.length > 0
+
+ if (shouldUseContentOptions) {
+ const lastContent = msg.content[msg.content.length - 1]
+ if (
+ lastContent &&
+ typeof lastContent === "object" &&
+ lastContent.type !== "tool-approval-request" &&
+ lastContent.type !== "tool-approval-response"
+ ) {
+ lastContent.providerOptions = mergeDeep(lastContent.providerOptions ?? {}, providerOptions)
+ continue
+ }
}
- return msgs
+ msg.providerOptions = mergeDeep(msg.providerOptions ?? {}, providerOptions)
}
- function unsupportedParts(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
- return msgs.map((msg) => {
- if (msg.role !== "user" || !Array.isArray(msg.content)) return msg
-
- const filtered = msg.content.map((part) => {
- if (part.type !== "file" && part.type !== "image") return part
-
- // Check for empty base64 image data
- if (part.type === "image") {
- const imageStr = String(part.image)
- if (imageStr.startsWith("data:")) {
- const match = imageStr.match(/^data:([^;]+);base64,(.*)$/)
- if (match && (!match[2] || match[2].length === 0)) {
- return {
- type: "text" as const,
- text: "ERROR: Image file is empty or corrupted. Please provide a valid image.",
- }
+ return msgs
+}
+
+function unsupportedParts(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
+ return msgs.map((msg) => {
+ if (msg.role !== "user" || !Array.isArray(msg.content)) return msg
+
+ const filtered = msg.content.map((part) => {
+ if (part.type !== "file" && part.type !== "image") return part
+
+ // Check for empty base64 image data
+ if (part.type === "image") {
+ const imageStr = String(part.image)
+ if (imageStr.startsWith("data:")) {
+ const match = imageStr.match(/^data:([^;]+);base64,(.*)$/)
+ if (match && (!match[2] || match[2].length === 0)) {
+ return {
+ type: "text" as const,
+ text: "ERROR: Image file is empty or corrupted. Please provide a valid image.",
}
}
}
+ }
- const mime = part.type === "image" ? String(part.image).split(";")[0].replace("data:", "") : part.mediaType
- const filename = part.type === "file" ? part.filename : undefined
- const modality = mimeToModality(mime)
- if (!modality) return part
- if (model.capabilities.input[modality]) return part
-
- const name = filename ? `"${filename}"` : modality
- return {
- type: "text" as const,
- text: `ERROR: Cannot read ${name} (this model does not support ${modality} input). Inform the user.`,
- }
- })
+ const mime = part.type === "image" ? String(part.image).split(";")[0].replace("data:", "") : part.mediaType
+ const filename = part.type === "file" ? part.filename : undefined
+ const modality = mimeToModality(mime)
+ if (!modality) return part
+ if (model.capabilities.input[modality]) return part
- return { ...msg, content: filtered }
+ const name = filename ? `"${filename}"` : modality
+ return {
+ type: "text" as const,
+ text: `ERROR: Cannot read ${name} (this model does not support ${modality} input). Inform the user.`,
+ }
})
- }
- export function message(msgs: ModelMessage[], model: Provider.Model, options: Record<string, unknown>) {
- msgs = unsupportedParts(msgs, model)
- msgs = normalizeMessages(msgs, model, options)
- if (
- (model.providerID === "anthropic" ||
- model.providerID === "google-vertex-anthropic" ||
- model.api.id.includes("anthropic") ||
- model.api.id.includes("claude") ||
- model.id.includes("anthropic") ||
- model.id.includes("claude") ||
- model.api.npm === "@ai-sdk/anthropic" ||
- model.api.npm === "@ai-sdk/alibaba") &&
- model.api.npm !== "@ai-sdk/gateway"
- ) {
- msgs = applyCaching(msgs, model)
- }
+ return { ...msg, content: filtered }
+ })
+}
- // Remap providerOptions keys from stored providerID to expected SDK key
- const key = sdkKey(model.api.npm)
- if (key && key !== model.providerID) {
- const remap = (opts: Record<string, any> | undefined) => {
- if (!opts) return opts
- if (!(model.providerID in opts)) return opts
- const result = { ...opts }
- result[key] = result[model.providerID]
- delete result[model.providerID]
- return result
- }
+export function message(msgs: ModelMessage[], model: Provider.Model, options: Record<string, unknown>) {
+ msgs = unsupportedParts(msgs, model)
+ msgs = normalizeMessages(msgs, model, options)
+ if (
+ (model.providerID === "anthropic" ||
+ model.providerID === "google-vertex-anthropic" ||
+ model.api.id.includes("anthropic") ||
+ model.api.id.includes("claude") ||
+ model.id.includes("anthropic") ||
+ model.id.includes("claude") ||
+ model.api.npm === "@ai-sdk/anthropic" ||
+ model.api.npm === "@ai-sdk/alibaba") &&
+ model.api.npm !== "@ai-sdk/gateway"
+ ) {
+ msgs = applyCaching(msgs, model)
+ }
- msgs = msgs.map((msg) => {
- if (!Array.isArray(msg.content)) return { ...msg, providerOptions: remap(msg.providerOptions) }
- return {
- ...msg,
- providerOptions: remap(msg.providerOptions),
- content: msg.content.map((part) => {
- if (part.type === "tool-approval-request" || part.type === "tool-approval-response") {
- return { ...part }
- }
- return { ...part, providerOptions: remap(part.providerOptions) }
- }),
- } as typeof msg
- })
+ // Remap providerOptions keys from stored providerID to expected SDK key
+ const key = sdkKey(model.api.npm)
+ if (key && key !== model.providerID) {
+ const remap = (opts: Record<string, any> | undefined) => {
+ if (!opts) return opts
+ if (!(model.providerID in opts)) return opts
+ const result = { ...opts }
+ result[key] = result[model.providerID]
+ delete result[model.providerID]
+ return result
}
- return msgs
+ msgs = msgs.map((msg) => {
+ if (!Array.isArray(msg.content)) return { ...msg, providerOptions: remap(msg.providerOptions) }
+ return {
+ ...msg,
+ providerOptions: remap(msg.providerOptions),
+ content: msg.content.map((part) => {
+ if (part.type === "tool-approval-request" || part.type === "tool-approval-response") {
+ return { ...part }
+ }
+ return { ...part, providerOptions: remap(part.providerOptions) }
+ }),
+ } as typeof msg
+ })
}
- export function temperature(model: Provider.Model) {
- const id = model.id.toLowerCase()
- if (id.includes("qwen")) return 0.55
- if (id.includes("claude")) return undefined
- if (id.includes("gemini")) return 1.0
- if (id.includes("glm-4.6")) return 1.0
- if (id.includes("glm-4.7")) return 1.0
- if (id.includes("minimax-m2")) return 1.0
- if (id.includes("kimi-k2")) {
- // kimi-k2-thinking & kimi-k2.5 && kimi-k2p5 && kimi-k2-5
- if (["thinking", "k2.", "k2p", "k2-5"].some((s) => id.includes(s))) {
- return 1.0
- }
- return 0.6
- }
- return undefined
- }
+ return msgs
+}
- export function topP(model: Provider.Model) {
- const id = model.id.toLowerCase()
- if (id.includes("qwen")) return 1
- if (["minimax-m2", "gemini", "kimi-k2.5", "kimi-k2p5", "kimi-k2-5"].some((s) => id.includes(s))) {
- return 0.95
+export function temperature(model: Provider.Model) {
+ const id = model.id.toLowerCase()
+ if (id.includes("qwen")) return 0.55
+ if (id.includes("claude")) return undefined
+ if (id.includes("gemini")) return 1.0
+ if (id.includes("glm-4.6")) return 1.0
+ if (id.includes("glm-4.7")) return 1.0
+ if (id.includes("minimax-m2")) return 1.0
+ if (id.includes("kimi-k2")) {
+ // kimi-k2-thinking & kimi-k2.5 && kimi-k2p5 && kimi-k2-5
+ if (["thinking", "k2.", "k2p", "k2-5"].some((s) => id.includes(s))) {
+ return 1.0
}
- return undefined
+ return 0.6
}
+ return undefined
+}
- export function topK(model: Provider.Model) {
- const id = model.id.toLowerCase()
- if (id.includes("minimax-m2")) {
- if (["m2.", "m25", "m21"].some((s) => id.includes(s))) return 40
- return 20
- }
- if (id.includes("gemini")) return 64
- return undefined
+export function topP(model: Provider.Model) {
+ const id = model.id.toLowerCase()
+ if (id.includes("qwen")) return 1
+ if (["minimax-m2", "gemini", "kimi-k2.5", "kimi-k2p5", "kimi-k2-5"].some((s) => id.includes(s))) {
+ return 0.95
}
+ return undefined
+}
- const WIDELY_SUPPORTED_EFFORTS = ["low", "medium", "high"]
- const OPENAI_EFFORTS = ["none", "minimal", ...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
-
- export function variants(model: Provider.Model): Record<string, Record<string, any>> {
- if (!model.capabilities.reasoning) return {}
+export function topK(model: Provider.Model) {
+ const id = model.id.toLowerCase()
+ if (id.includes("minimax-m2")) {
+ if (["m2.", "m25", "m21"].some((s) => id.includes(s))) return 40
+ return 20
+ }
+ if (id.includes("gemini")) return 64
+ return undefined
+}
- const id = model.id.toLowerCase()
- const isAnthropicAdaptive = ["opus-4-6", "opus-4.6", "sonnet-4-6", "sonnet-4.6"].some((v) =>
- model.api.id.includes(v),
- )
- const adaptiveEfforts = ["low", "medium", "high", "max"]
- if (
- id.includes("deepseek") ||
- id.includes("minimax") ||
- id.includes("glm") ||
- id.includes("mistral") ||
- id.includes("kimi") ||
- id.includes("k2p5") ||
- id.includes("qwen") ||
- id.includes("big-pickle")
- )
- return {}
+const WIDELY_SUPPORTED_EFFORTS = ["low", "medium", "high"]
+const OPENAI_EFFORTS = ["none", "minimal", ...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
+
+export function variants(model: Provider.Model): Record<string, Record<string, any>> {
+ if (!model.capabilities.reasoning) return {}
+
+ const id = model.id.toLowerCase()
+ const isAnthropicAdaptive = ["opus-4-6", "opus-4.6", "sonnet-4-6", "sonnet-4.6"].some((v) =>
+ model.api.id.includes(v),
+ )
+ const adaptiveEfforts = ["low", "medium", "high", "max"]
+ if (
+ id.includes("deepseek") ||
+ id.includes("minimax") ||
+ id.includes("glm") ||
+ id.includes("mistral") ||
+ id.includes("kimi") ||
+ id.includes("k2p5") ||
+ id.includes("qwen") ||
+ id.includes("big-pickle")
+ )
+ return {}
- // see: https://docs.x.ai/docs/guides/reasoning#control-how-hard-the-model-thinks
- if (id.includes("grok") && id.includes("grok-3-mini")) {
- if (model.api.npm === "@openrouter/ai-sdk-provider") {
- return {
- low: { reasoning: { effort: "low" } },
- high: { reasoning: { effort: "high" } },
- }
- }
+ // see: https://docs.x.ai/docs/guides/reasoning#control-how-hard-the-model-thinks
+ if (id.includes("grok") && id.includes("grok-3-mini")) {
+ if (model.api.npm === "@openrouter/ai-sdk-provider") {
return {
- low: { reasoningEffort: "low" },
- high: { reasoningEffort: "high" },
+ low: { reasoning: { effort: "low" } },
+ high: { reasoning: { effort: "high" } },
}
}
- if (id.includes("grok")) return {}
-
- switch (model.api.npm) {
- case "@openrouter/ai-sdk-provider":
- if (!model.id.includes("gpt") && !model.id.includes("gemini-3") && !model.id.includes("claude")) return {}
- return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoning: { effort } }]))
-
- case "@ai-sdk/gateway":
- if (model.id.includes("anthropic")) {
- if (isAnthropicAdaptive) {
- return Object.fromEntries(
- adaptiveEfforts.map((effort) => [
- effort,
- {
- thinking: {
- type: "adaptive",
- },
- effort,
- },
- ]),
- )
- }
- return {
- high: {
- thinking: {
- type: "enabled",
- budgetTokens: 16000,
- },
- },
- max: {
- thinking: {
- type: "enabled",
- budgetTokens: 31999,
- },
- },
- }
- }
- if (model.id.includes("google")) {
- if (id.includes("2.5")) {
- return {
- high: {
- thinkingConfig: {
- includeThoughts: true,
- thinkingBudget: 16000,
- },
- },
- max: {
- thinkingConfig: {
- includeThoughts: true,
- thinkingBudget: 24576,
- },
- },
- }
- }
- return Object.fromEntries(
- ["low", "high"].map((effort) => [
- effort,
- {
- includeThoughts: true,
- thinkingLevel: effort,
- },
- ]),
- )
- }
- return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
-
- case "@ai-sdk/github-copilot":
- if (model.id.includes("gemini")) {
- // currently github copilot only returns thinking
- return {}
- }
- if (model.id.includes("claude")) {
- return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
- }
- const copilotEfforts = iife(() => {
- if (id.includes("5.1-codex-max") || id.includes("5.2") || id.includes("5.3"))
- return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
- const arr = [...WIDELY_SUPPORTED_EFFORTS]
- if (id.includes("gpt-5") && model.release_date >= "2025-12-04") arr.push("xhigh")
- return arr
- })
- return Object.fromEntries(
- copilotEfforts.map((effort) => [
- effort,
- {
- reasoningEffort: effort,
- reasoningSummary: "auto",
- include: ["reasoning.encrypted_content"],
- },
- ]),
- )
-
- case "@ai-sdk/cerebras":
- // https://v5.ai-sdk.dev/providers/ai-sdk-providers/cerebras
- case "@ai-sdk/togetherai":
- // https://v5.ai-sdk.dev/providers/ai-sdk-providers/togetherai
- case "@ai-sdk/xai":
- // https://v5.ai-sdk.dev/providers/ai-sdk-providers/xai
- case "@ai-sdk/deepinfra":
- // https://v5.ai-sdk.dev/providers/ai-sdk-providers/deepinfra
- case "venice-ai-sdk-provider":
- // https://docs.venice.ai/overview/guides/reasoning-models#reasoning-effort
- case "@ai-sdk/openai-compatible":
- return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
-
- case "@ai-sdk/azure":
- // https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure
- if (id === "o1-mini") return {}
- const azureEfforts = ["low", "medium", "high"]
- if (id.includes("gpt-5-") || id === "gpt-5") {
- azureEfforts.unshift("minimal")
- }
- return Object.fromEntries(
- azureEfforts.map((effort) => [
- effort,
- {
- reasoningEffort: effort,
- reasoningSummary: "auto",
- include: ["reasoning.encrypted_content"],
- },
- ]),
- )
- case "@ai-sdk/openai":
- // https://v5.ai-sdk.dev/providers/ai-sdk-providers/openai
- if (id === "gpt-5-pro") return {}
- const openaiEfforts = iife(() => {
- if (id.includes("codex")) {
- if (id.includes("5.2") || id.includes("5.3")) return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
- return WIDELY_SUPPORTED_EFFORTS
- }
- const arr = [...WIDELY_SUPPORTED_EFFORTS]
- if (id.includes("gpt-5-") || id === "gpt-5") {
- arr.unshift("minimal")
- }
- if (model.release_date >= "2025-11-13") {
- arr.unshift("none")
- }
- if (model.release_date >= "2025-12-04") {
- arr.push("xhigh")
- }
- return arr
- })
- return Object.fromEntries(
- openaiEfforts.map((effort) => [
- effort,
- {
- reasoningEffort: effort,
- reasoningSummary: "auto",
- include: ["reasoning.encrypted_content"],
- },
- ]),
- )
+ return {
+ low: { reasoningEffort: "low" },
+ high: { reasoningEffort: "high" },
+ }
+ }
+ if (id.includes("grok")) return {}
- case "@ai-sdk/anthropic":
- // https://v5.ai-sdk.dev/providers/ai-sdk-providers/anthropic
- case "@ai-sdk/google-vertex/anthropic":
- // https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex#anthropic-provider
+ switch (model.api.npm) {
+ case "@openrouter/ai-sdk-provider":
+ if (!model.id.includes("gpt") && !model.id.includes("gemini-3") && !model.id.includes("claude")) return {}
+ return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoning: { effort } }]))
+ case "@ai-sdk/gateway":
+ if (model.id.includes("anthropic")) {
if (isAnthropicAdaptive) {
return Object.fromEntries(
adaptiveEfforts.map((effort) => [
@@ -594,499 +444,647 @@ export namespace ProviderTransform {
]),
)
}
-
return {
high: {
thinking: {
type: "enabled",
- budgetTokens: Math.min(16_000, Math.floor(model.limit.output / 2 - 1)),
+ budgetTokens: 16000,
},
},
max: {
thinking: {
type: "enabled",
- budgetTokens: Math.min(31_999, model.limit.output - 1),
+ budgetTokens: 31999,
},
},
}
-
- case "@ai-sdk/amazon-bedrock":
- // https://v5.ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock
- if (isAnthropicAdaptive) {
- return Object.fromEntries(
- adaptiveEfforts.map((effort) => [
- effort,
- {
- reasoningConfig: {
- type: "adaptive",
- maxReasoningEffort: effort,
- },
- },
- ]),
- )
- }
- // For Anthropic models on Bedrock, use reasoningConfig with budgetTokens
- if (model.api.id.includes("anthropic")) {
+ }
+ if (model.id.includes("google")) {
+ if (id.includes("2.5")) {
return {
high: {
- reasoningConfig: {
- type: "enabled",
- budgetTokens: 16000,
+ thinkingConfig: {
+ includeThoughts: true,
+ thinkingBudget: 16000,
},
},
max: {
- reasoningConfig: {
- type: "enabled",
- budgetTokens: 31999,
+ thinkingConfig: {
+ includeThoughts: true,
+ thinkingBudget: 24576,
},
},
}
}
-
- // For Amazon Nova models, use reasoningConfig with maxReasoningEffort
return Object.fromEntries(
- WIDELY_SUPPORTED_EFFORTS.map((effort) => [
+ ["low", "high"].map((effort) => [
effort,
{
- reasoningConfig: {
- type: "enabled",
- maxReasoningEffort: effort,
- },
+ includeThoughts: true,
+ thinkingLevel: effort,
},
]),
)
+ }
+ return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
- case "@ai-sdk/google-vertex":
- // https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex
- case "@ai-sdk/google":
- // https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai
- if (id.includes("2.5")) {
- return {
- high: {
- thinkingConfig: {
- includeThoughts: true,
- thinkingBudget: 16000,
- },
- },
- max: {
- thinkingConfig: {
- includeThoughts: true,
- thinkingBudget: 24576,
- },
- },
- }
+ case "@ai-sdk/github-copilot":
+ if (model.id.includes("gemini")) {
+ // currently github copilot only returns thinking
+ return {}
+ }
+ if (model.id.includes("claude")) {
+ return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
+ }
+ const copilotEfforts = iife(() => {
+ if (id.includes("5.1-codex-max") || id.includes("5.2") || id.includes("5.3"))
+ return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
+ const arr = [...WIDELY_SUPPORTED_EFFORTS]
+ if (id.includes("gpt-5") && model.release_date >= "2025-12-04") arr.push("xhigh")
+ return arr
+ })
+ return Object.fromEntries(
+ copilotEfforts.map((effort) => [
+ effort,
+ {
+ reasoningEffort: effort,
+ reasoningSummary: "auto",
+ include: ["reasoning.encrypted_content"],
+ },
+ ]),
+ )
+
+ case "@ai-sdk/cerebras":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/cerebras
+ case "@ai-sdk/togetherai":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/togetherai
+ case "@ai-sdk/xai":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/xai
+ case "@ai-sdk/deepinfra":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/deepinfra
+ case "venice-ai-sdk-provider":
+ // https://docs.venice.ai/overview/guides/reasoning-models#reasoning-effort
+ case "@ai-sdk/openai-compatible":
+ return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
+
+ case "@ai-sdk/azure":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure
+ if (id === "o1-mini") return {}
+ const azureEfforts = ["low", "medium", "high"]
+ if (id.includes("gpt-5-") || id === "gpt-5") {
+ azureEfforts.unshift("minimal")
+ }
+ return Object.fromEntries(
+ azureEfforts.map((effort) => [
+ effort,
+ {
+ reasoningEffort: effort,
+ reasoningSummary: "auto",
+ include: ["reasoning.encrypted_content"],
+ },
+ ]),
+ )
+ case "@ai-sdk/openai":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/openai
+ if (id === "gpt-5-pro") return {}
+ const openaiEfforts = iife(() => {
+ if (id.includes("codex")) {
+ if (id.includes("5.2") || id.includes("5.3")) return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
+ return WIDELY_SUPPORTED_EFFORTS
}
- let levels = ["low", "high"]
- if (id.includes("3.1")) {
- levels = ["low", "medium", "high"]
+ const arr = [...WIDELY_SUPPORTED_EFFORTS]
+ if (id.includes("gpt-5-") || id === "gpt-5") {
+ arr.unshift("minimal")
}
+ if (model.release_date >= "2025-11-13") {
+ arr.unshift("none")
+ }
+ if (model.release_date >= "2025-12-04") {
+ arr.push("xhigh")
+ }
+ return arr
+ })
+ return Object.fromEntries(
+ openaiEfforts.map((effort) => [
+ effort,
+ {
+ reasoningEffort: effort,
+ reasoningSummary: "auto",
+ include: ["reasoning.encrypted_content"],
+ },
+ ]),
+ )
+ case "@ai-sdk/anthropic":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/anthropic
+ case "@ai-sdk/google-vertex/anthropic":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex#anthropic-provider
+
+ if (isAnthropicAdaptive) {
return Object.fromEntries(
- levels.map((effort) => [
+ adaptiveEfforts.map((effort) => [
effort,
{
- thinkingConfig: {
- includeThoughts: true,
- thinkingLevel: effort,
+ thinking: {
+ type: "adaptive",
},
+ effort,
},
]),
)
+ }
- case "@ai-sdk/mistral":
- // https://v5.ai-sdk.dev/providers/ai-sdk-providers/mistral
- return {}
-
- case "@ai-sdk/cohere":
- // https://v5.ai-sdk.dev/providers/ai-sdk-providers/cohere
- return {}
+ return {
+ high: {
+ thinking: {
+ type: "enabled",
+ budgetTokens: Math.min(16_000, Math.floor(model.limit.output / 2 - 1)),
+ },
+ },
+ max: {
+ thinking: {
+ type: "enabled",
+ budgetTokens: Math.min(31_999, model.limit.output - 1),
+ },
+ },
+ }
- case "@ai-sdk/groq":
- // https://v5.ai-sdk.dev/providers/ai-sdk-providers/groq
- const groqEffort = ["none", ...WIDELY_SUPPORTED_EFFORTS]
+ case "@ai-sdk/amazon-bedrock":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock
+ if (isAnthropicAdaptive) {
return Object.fromEntries(
- groqEffort.map((effort) => [
+ adaptiveEfforts.map((effort) => [
effort,
{
- reasoningEffort: effort,
+ reasoningConfig: {
+ type: "adaptive",
+ maxReasoningEffort: effort,
+ },
},
]),
)
+ }
+ // For Anthropic models on Bedrock, use reasoningConfig with budgetTokens
+ if (model.api.id.includes("anthropic")) {
+ return {
+ high: {
+ reasoningConfig: {
+ type: "enabled",
+ budgetTokens: 16000,
+ },
+ },
+ max: {
+ reasoningConfig: {
+ type: "enabled",
+ budgetTokens: 31999,
+ },
+ },
+ }
+ }
- case "@ai-sdk/perplexity":
- // https://v5.ai-sdk.dev/providers/ai-sdk-providers/perplexity
- return {}
+ // For Amazon Nova models, use reasoningConfig with maxReasoningEffort
+ return Object.fromEntries(
+ WIDELY_SUPPORTED_EFFORTS.map((effort) => [
+ effort,
+ {
+ reasoningConfig: {
+ type: "enabled",
+ maxReasoningEffort: effort,
+ },
+ },
+ ]),
+ )
+
+ case "@ai-sdk/google-vertex":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex
+ case "@ai-sdk/google":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai
+ if (id.includes("2.5")) {
+ return {
+ high: {
+ thinkingConfig: {
+ includeThoughts: true,
+ thinkingBudget: 16000,
+ },
+ },
+ max: {
+ thinkingConfig: {
+ includeThoughts: true,
+ thinkingBudget: 24576,
+ },
+ },
+ }
+ }
+ let levels = ["low", "high"]
+ if (id.includes("3.1")) {
+ levels = ["low", "medium", "high"]
+ }
- case "@jerome-benoit/sap-ai-provider-v2":
- if (model.api.id.includes("anthropic")) {
- if (isAnthropicAdaptive) {
- return Object.fromEntries(
- adaptiveEfforts.map((effort) => [
- effort,
- {
- thinking: {
- type: "adaptive",
- },
- effort,
+ return Object.fromEntries(
+ levels.map((effort) => [
+ effort,
+ {
+ thinkingConfig: {
+ includeThoughts: true,
+ thinkingLevel: effort,
+ },
+ },
+ ]),
+ )
+
+ case "@ai-sdk/mistral":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/mistral
+ return {}
+
+ case "@ai-sdk/cohere":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/cohere
+ return {}
+
+ case "@ai-sdk/groq":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/groq
+ const groqEffort = ["none", ...WIDELY_SUPPORTED_EFFORTS]
+ return Object.fromEntries(
+ groqEffort.map((effort) => [
+ effort,
+ {
+ reasoningEffort: effort,
+ },
+ ]),
+ )
+
+ case "@ai-sdk/perplexity":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/perplexity
+ return {}
+
+ case "@jerome-benoit/sap-ai-provider-v2":
+ if (model.api.id.includes("anthropic")) {
+ if (isAnthropicAdaptive) {
+ return Object.fromEntries(
+ adaptiveEfforts.map((effort) => [
+ effort,
+ {
+ thinking: {
+ type: "adaptive",
},
- ]),
- )
- }
- return {
- high: {
- thinking: {
- type: "enabled",
- budgetTokens: 16000,
+ effort,
},
+ ]),
+ )
+ }
+ return {
+ high: {
+ thinking: {
+ type: "enabled",
+ budgetTokens: 16000,
},
- max: {
- thinking: {
- type: "enabled",
- budgetTokens: 31999,
- },
+ },
+ max: {
+ thinking: {
+ type: "enabled",
+ budgetTokens: 31999,
},
- }
+ },
}
- if (model.api.id.includes("gemini") && id.includes("2.5")) {
- return {
- high: {
- thinkingConfig: {
- includeThoughts: true,
- thinkingBudget: 16000,
- },
+ }
+ if (model.api.id.includes("gemini") && id.includes("2.5")) {
+ return {
+ high: {
+ thinkingConfig: {
+ includeThoughts: true,
+ thinkingBudget: 16000,
},
- max: {
- thinkingConfig: {
- includeThoughts: true,
- thinkingBudget: 24576,
- },
+ },
+ max: {
+ thinkingConfig: {
+ includeThoughts: true,
+ thinkingBudget: 24576,
},
- }
- }
- if (model.api.id.includes("gpt") || /\bo[1-9]/.test(model.api.id)) {
- return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
+ },
}
- return {}
- }
- return {}
+ }
+ if (model.api.id.includes("gpt") || /\bo[1-9]/.test(model.api.id)) {
+ return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
+ }
+ return {}
}
+ return {}
+}
- export function options(input: {
- model: Provider.Model
- sessionID: string
- providerOptions?: Record<string, any>
- }): Record<string, any> {
- const result: Record<string, any> = {}
+export function options(input: {
+ model: Provider.Model
+ sessionID: string
+ providerOptions?: Record<string, any>
+}): Record<string, any> {
+ const result: Record<string, any> = {}
+
+ // openai and providers using openai package should set store to false by default.
+ if (
+ input.model.providerID === "openai" ||
+ input.model.api.npm === "@ai-sdk/openai" ||
+ input.model.api.npm === "@ai-sdk/github-copilot"
+ ) {
+ result["store"] = false
+ }
- // openai and providers using openai package should set store to false by default.
- if (
- input.model.providerID === "openai" ||
- input.model.api.npm === "@ai-sdk/openai" ||
- input.model.api.npm === "@ai-sdk/github-copilot"
- ) {
- result["store"] = false
+ if (input.model.api.npm === "@openrouter/ai-sdk-provider") {
+ result["usage"] = {
+ include: true,
}
-
- if (input.model.api.npm === "@openrouter/ai-sdk-provider") {
- result["usage"] = {
- include: true,
- }
- if (input.model.api.id.includes("gemini-3")) {
- result["reasoning"] = { effort: "high" }
- }
+ if (input.model.api.id.includes("gemini-3")) {
+ result["reasoning"] = { effort: "high" }
}
+ }
- if (
- input.model.providerID === "baseten" ||
- (input.model.providerID === "opencode" && ["kimi-k2-thinking", "glm-4.6"].includes(input.model.api.id))
- ) {
- result["chat_template_args"] = { enable_thinking: true }
- }
+ if (
+ input.model.providerID === "baseten" ||
+ (input.model.providerID === "opencode" && ["kimi-k2-thinking", "glm-4.6"].includes(input.model.api.id))
+ ) {
+ result["chat_template_args"] = { enable_thinking: true }
+ }
- if (
- ["zai", "zhipuai"].some((id) => input.model.providerID.includes(id)) &&
- input.model.api.npm === "@ai-sdk/openai-compatible"
- ) {
- result["thinking"] = {
- type: "enabled",
- clear_thinking: false,
- }
+ if (
+ ["zai", "zhipuai"].some((id) => input.model.providerID.includes(id)) &&
+ input.model.api.npm === "@ai-sdk/openai-compatible"
+ ) {
+ result["thinking"] = {
+ type: "enabled",
+ clear_thinking: false,
}
+ }
- if (input.model.providerID === "openai" || input.providerOptions?.setCacheKey) {
- result["promptCacheKey"] = input.sessionID
- }
+ if (input.model.providerID === "openai" || input.providerOptions?.setCacheKey) {
+ result["promptCacheKey"] = input.sessionID
+ }
- if (input.model.api.npm === "@ai-sdk/google" || input.model.api.npm === "@ai-sdk/google-vertex") {
- if (input.model.capabilities.reasoning) {
- result["thinkingConfig"] = {
- includeThoughts: true,
- }
- if (input.model.api.id.includes("gemini-3")) {
- result["thinkingConfig"]["thinkingLevel"] = "high"
- }
+ if (input.model.api.npm === "@ai-sdk/google" || input.model.api.npm === "@ai-sdk/google-vertex") {
+ if (input.model.capabilities.reasoning) {
+ result["thinkingConfig"] = {
+ includeThoughts: true,
}
- }
-
- // Enable thinking by default for kimi-k2.5/k2p5 models using anthropic SDK
- const modelId = input.model.api.id.toLowerCase()
- if (
- (input.model.api.npm === "@ai-sdk/anthropic" || input.model.api.npm === "@ai-sdk/google-vertex/anthropic") &&
- (modelId.includes("k2p5") || modelId.includes("kimi-k2.5") || modelId.includes("kimi-k2p5"))
- ) {
- result["thinking"] = {
- type: "enabled",
- budgetTokens: Math.min(16_000, Math.floor(input.model.limit.output / 2 - 1)),
+ if (input.model.api.id.includes("gemini-3")) {
+ result["thinkingConfig"]["thinkingLevel"] = "high"
}
}
+ }
- // Enable thinking for reasoning models on alibaba-cn (DashScope).
- // DashScope's OpenAI-compatible API requires `enable_thinking: true` in the request body
- // to return reasoning_content. Without it, models like kimi-k2.5, qwen-plus, qwen3, qwq,
- // deepseek-r1, etc. never output thinking/reasoning tokens.
- // Note: kimi-k2-thinking is excluded as it returns reasoning_content by default.
- if (
- input.model.providerID === "alibaba-cn" &&
- input.model.capabilities.reasoning &&
- input.model.api.npm === "@ai-sdk/openai-compatible" &&
- !modelId.includes("kimi-k2-thinking")
- ) {
- result["enable_thinking"] = true
+ // Enable thinking by default for kimi-k2.5/k2p5 models using anthropic SDK
+ const modelId = input.model.api.id.toLowerCase()
+ if (
+ (input.model.api.npm === "@ai-sdk/anthropic" || input.model.api.npm === "@ai-sdk/google-vertex/anthropic") &&
+ (modelId.includes("k2p5") || modelId.includes("kimi-k2.5") || modelId.includes("kimi-k2p5"))
+ ) {
+ result["thinking"] = {
+ type: "enabled",
+ budgetTokens: Math.min(16_000, Math.floor(input.model.limit.output / 2 - 1)),
}
+ }
- if (input.model.api.id.includes("gpt-5") && !input.model.api.id.includes("gpt-5-chat")) {
- if (!input.model.api.id.includes("gpt-5-pro")) {
- result["reasoningEffort"] = "medium"
- // Only inject reasoningSummary for providers that support it natively.
- // @ai-sdk/openai-compatible proxies (e.g. LiteLLM) do not understand this
- // parameter and return "Unknown parameter: 'reasoningSummary'".
- if (
- input.model.api.npm === "@ai-sdk/openai" ||
- input.model.api.npm === "@ai-sdk/azure" ||
- input.model.api.npm === "@ai-sdk/github-copilot"
- ) {
- result["reasoningSummary"] = "auto"
- }
- }
+ // Enable thinking for reasoning models on alibaba-cn (DashScope).
+ // DashScope's OpenAI-compatible API requires `enable_thinking: true` in the request body
+ // to return reasoning_content. Without it, models like kimi-k2.5, qwen-plus, qwen3, qwq,
+ // deepseek-r1, etc. never output thinking/reasoning tokens.
+ // Note: kimi-k2-thinking is excluded as it returns reasoning_content by default.
+ if (
+ input.model.providerID === "alibaba-cn" &&
+ input.model.capabilities.reasoning &&
+ input.model.api.npm === "@ai-sdk/openai-compatible" &&
+ !modelId.includes("kimi-k2-thinking")
+ ) {
+ result["enable_thinking"] = true
+ }
- // Only set textVerbosity for non-chat gpt-5.x models
- // Chat models (e.g. gpt-5.2-chat-latest) only support "medium" verbosity
+ if (input.model.api.id.includes("gpt-5") && !input.model.api.id.includes("gpt-5-chat")) {
+ if (!input.model.api.id.includes("gpt-5-pro")) {
+ result["reasoningEffort"] = "medium"
+ // Only inject reasoningSummary for providers that support it natively.
+ // @ai-sdk/openai-compatible proxies (e.g. LiteLLM) do not understand this
+ // parameter and return "Unknown parameter: 'reasoningSummary'".
if (
- input.model.api.id.includes("gpt-5.") &&
- !input.model.api.id.includes("codex") &&
- !input.model.api.id.includes("-chat") &&
- input.model.providerID !== "azure"
+ input.model.api.npm === "@ai-sdk/openai" ||
+ input.model.api.npm === "@ai-sdk/azure" ||
+ input.model.api.npm === "@ai-sdk/github-copilot"
) {
- result["textVerbosity"] = "low"
- }
-
- if (input.model.providerID.startsWith("opencode")) {
- result["promptCacheKey"] = input.sessionID
- result["include"] = ["reasoning.encrypted_content"]
result["reasoningSummary"] = "auto"
}
}
- if (input.model.providerID === "venice") {
- result["promptCacheKey"] = input.sessionID
+ // Only set textVerbosity for non-chat gpt-5.x models
+ // Chat models (e.g. gpt-5.2-chat-latest) only support "medium" verbosity
+ if (
+ input.model.api.id.includes("gpt-5.") &&
+ !input.model.api.id.includes("codex") &&
+ !input.model.api.id.includes("-chat") &&
+ input.model.providerID !== "azure"
+ ) {
+ result["textVerbosity"] = "low"
}
- if (input.model.providerID === "openrouter") {
- result["prompt_cache_key"] = input.sessionID
- }
- if (input.model.api.npm === "@ai-sdk/gateway") {
- result["gateway"] = {
- caching: "auto",
- }
+ if (input.model.providerID.startsWith("opencode")) {
+ result["promptCacheKey"] = input.sessionID
+ result["include"] = ["reasoning.encrypted_content"]
+ result["reasoningSummary"] = "auto"
}
+ }
- return result
+ if (input.model.providerID === "venice") {
+ result["promptCacheKey"] = input.sessionID
}
- export function smallOptions(model: Provider.Model) {
- if (
- model.providerID === "openai" ||
- model.api.npm === "@ai-sdk/openai" ||
- model.api.npm === "@ai-sdk/github-copilot"
- ) {
- if (model.api.id.includes("gpt-5")) {
- if (model.api.id.includes("5.")) {
- return { store: false, reasoningEffort: "low" }
- }
- return { store: false, reasoningEffort: "minimal" }
- }
- return { store: false }
+ if (input.model.providerID === "openrouter") {
+ result["prompt_cache_key"] = input.sessionID
+ }
+ if (input.model.api.npm === "@ai-sdk/gateway") {
+ result["gateway"] = {
+ caching: "auto",
}
- if (model.providerID === "google") {
- // gemini-3 uses thinkingLevel, gemini-2.5 uses thinkingBudget
- if (model.api.id.includes("gemini-3")) {
- return { thinkingConfig: { thinkingLevel: "minimal" } }
+ }
+
+ return result
+}
+
+export function smallOptions(model: Provider.Model) {
+ if (
+ model.providerID === "openai" ||
+ model.api.npm === "@ai-sdk/openai" ||
+ model.api.npm === "@ai-sdk/github-copilot"
+ ) {
+ if (model.api.id.includes("gpt-5")) {
+ if (model.api.id.includes("5.")) {
+ return { store: false, reasoningEffort: "low" }
}
- return { thinkingConfig: { thinkingBudget: 0 } }
+ return { store: false, reasoningEffort: "minimal" }
}
- if (model.providerID === "openrouter") {
- if (model.api.id.includes("google")) {
- return { reasoning: { enabled: false } }
- }
- return { reasoningEffort: "minimal" }
+ return { store: false }
+ }
+ if (model.providerID === "google") {
+ // gemini-3 uses thinkingLevel, gemini-2.5 uses thinkingBudget
+ if (model.api.id.includes("gemini-3")) {
+ return { thinkingConfig: { thinkingLevel: "minimal" } }
}
-
- if (model.providerID === "venice") {
- return { veniceParameters: { disableThinking: true } }
+ return { thinkingConfig: { thinkingBudget: 0 } }
+ }
+ if (model.providerID === "openrouter") {
+ if (model.api.id.includes("google")) {
+ return { reasoning: { enabled: false } }
}
-
- return {}
+ return { reasoningEffort: "minimal" }
}
- // Maps model ID prefix to provider slug used in providerOptions.
- // Example: "amazon/nova-2-lite" → "bedrock"
- const SLUG_OVERRIDES: Record<string, string> = {
- amazon: "bedrock",
+ if (model.providerID === "venice") {
+ return { veniceParameters: { disableThinking: true } }
}
- export function providerOptions(model: Provider.Model, options: { [x: string]: any }) {
- if (model.api.npm === "@ai-sdk/gateway") {
- // Gateway providerOptions are split across two namespaces:
- // - `gateway`: gateway-native routing/caching controls (order, only, byok, etc.)
- // - `<upstream slug>`: provider-specific model options (anthropic/openai/...)
- // We keep `gateway` as-is and route every other top-level option under the
- // model-derived upstream slug.
- const i = model.api.id.indexOf("/")
- const rawSlug = i > 0 ? model.api.id.slice(0, i) : undefined
- const slug = rawSlug ? (SLUG_OVERRIDES[rawSlug] ?? rawSlug) : undefined
- const gateway = options.gateway
- const rest = Object.fromEntries(Object.entries(options).filter(([k]) => k !== "gateway"))
- const has = Object.keys(rest).length > 0
-
- const result: Record<string, any> = {}
- if (gateway !== undefined) result.gateway = gateway
-
- if (has) {
- if (slug) {
- // Route model-specific options under the provider slug
- result[slug] = rest
- } else if (gateway && typeof gateway === "object" && !Array.isArray(gateway)) {
- result.gateway = { ...gateway, ...rest }
- } else {
- result.gateway = rest
- }
- }
+ return {}
+}
- return result
- }
+// Maps model ID prefix to provider slug used in providerOptions.
+// Example: "amazon/nova-2-lite" → "bedrock"
+const SLUG_OVERRIDES: Record<string, string> = {
+ amazon: "bedrock",
+}
- const key = sdkKey(model.api.npm) ?? model.providerID
- // @ai-sdk/azure delegates to OpenAIChatLanguageModel which reads from
- // providerOptions["openai"], but OpenAIResponsesLanguageModel checks
- // "azure" first. Pass both so model options work on either code path.
- if (model.api.npm === "@ai-sdk/azure") {
- return { openai: options, azure: options }
+export function providerOptions(model: Provider.Model, options: { [x: string]: any }) {
+ if (model.api.npm === "@ai-sdk/gateway") {
+ // Gateway providerOptions are split across two namespaces:
+ // - `gateway`: gateway-native routing/caching controls (order, only, byok, etc.)
+ // - `<upstream slug>`: provider-specific model options (anthropic/openai/...)
+ // We keep `gateway` as-is and route every other top-level option under the
+ // model-derived upstream slug.
+ const i = model.api.id.indexOf("/")
+ const rawSlug = i > 0 ? model.api.id.slice(0, i) : undefined
+ const slug = rawSlug ? (SLUG_OVERRIDES[rawSlug] ?? rawSlug) : undefined
+ const gateway = options.gateway
+ const rest = Object.fromEntries(Object.entries(options).filter(([k]) => k !== "gateway"))
+ const has = Object.keys(rest).length > 0
+
+ const result: Record<string, any> = {}
+ if (gateway !== undefined) result.gateway = gateway
+
+ if (has) {
+ if (slug) {
+ // Route model-specific options under the provider slug
+ result[slug] = rest
+ } else if (gateway && typeof gateway === "object" && !Array.isArray(gateway)) {
+ result.gateway = { ...gateway, ...rest }
+ } else {
+ result.gateway = rest
+ }
}
- return { [key]: options }
+
+ return result
}
- export function maxOutputTokens(model: Provider.Model): number {
- return Math.min(model.limit.output, OUTPUT_TOKEN_MAX) || OUTPUT_TOKEN_MAX
+ const key = sdkKey(model.api.npm) ?? model.providerID
+ // @ai-sdk/azure delegates to OpenAIChatLanguageModel which reads from
+ // providerOptions["openai"], but OpenAIResponsesLanguageModel checks
+ // "azure" first. Pass both so model options work on either code path.
+ if (model.api.npm === "@ai-sdk/azure") {
+ return { openai: options, azure: options }
}
+ return { [key]: options }
+}
- export function schema(model: Provider.Model, schema: JSONSchema.BaseSchema | JSONSchema7): JSONSchema7 {
- /*
- if (["openai", "azure"].includes(providerID)) {
- if (schema.type === "object" && schema.properties) {
- for (const [key, value] of Object.entries(schema.properties)) {
- if (schema.required?.includes(key)) continue
- schema.properties[key] = {
- anyOf: [
- value as JSONSchema.JSONSchema,
- {
- type: "null",
- },
- ],
- }
+export function maxOutputTokens(model: Provider.Model): number {
+ return Math.min(model.limit.output, OUTPUT_TOKEN_MAX) || OUTPUT_TOKEN_MAX
+}
+
+export function schema(model: Provider.Model, schema: JSONSchema.BaseSchema | JSONSchema7): JSONSchema7 {
+ /*
+ if (["openai", "azure"].includes(providerID)) {
+ if (schema.type === "object" && schema.properties) {
+ for (const [key, value] of Object.entries(schema.properties)) {
+ if (schema.required?.includes(key)) continue
+ schema.properties[key] = {
+ anyOf: [
+ value as JSONSchema.JSONSchema,
+ {
+ type: "null",
+ },
+ ],
}
}
}
- */
-
- // Convert integer enums to string enums for Google/Gemini
- if (model.providerID === "google" || model.api.id.includes("gemini")) {
- const isPlainObject = (node: unknown): node is Record<string, any> =>
- typeof node === "object" && node !== null && !Array.isArray(node)
- const hasCombiner = (node: unknown) =>
- isPlainObject(node) && (Array.isArray(node.anyOf) || Array.isArray(node.oneOf) || Array.isArray(node.allOf))
- const hasSchemaIntent = (node: unknown) => {
- if (!isPlainObject(node)) return false
- if (hasCombiner(node)) return true
- return [
- "type",
- "properties",
- "items",
- "prefixItems",
- "enum",
- "const",
- "$ref",
- "additionalProperties",
- "patternProperties",
- "required",
- "not",
- "if",
- "then",
- "else",
- ].some((key) => key in node)
- }
+ }
+ */
+
+ // Convert integer enums to string enums for Google/Gemini
+ if (model.providerID === "google" || model.api.id.includes("gemini")) {
+ const isPlainObject = (node: unknown): node is Record<string, any> =>
+ typeof node === "object" && node !== null && !Array.isArray(node)
+ const hasCombiner = (node: unknown) =>
+ isPlainObject(node) && (Array.isArray(node.anyOf) || Array.isArray(node.oneOf) || Array.isArray(node.allOf))
+ const hasSchemaIntent = (node: unknown) => {
+ if (!isPlainObject(node)) return false
+ if (hasCombiner(node)) return true
+ return [
+ "type",
+ "properties",
+ "items",
+ "prefixItems",
+ "enum",
+ "const",
+ "$ref",
+ "additionalProperties",
+ "patternProperties",
+ "required",
+ "not",
+ "if",
+ "then",
+ "else",
+ ].some((key) => key in node)
+ }
- const sanitizeGemini = (obj: any): any => {
- if (obj === null || typeof obj !== "object") {
- return obj
- }
+ const sanitizeGemini = (obj: any): any => {
+ if (obj === null || typeof obj !== "object") {
+ return obj
+ }
- if (Array.isArray(obj)) {
- return obj.map(sanitizeGemini)
- }
+ if (Array.isArray(obj)) {
+ return obj.map(sanitizeGemini)
+ }
- const result: any = {}
- for (const [key, value] of Object.entries(obj)) {
- if (key === "enum" && Array.isArray(value)) {
- // Convert all enum values to strings
- result[key] = value.map((v) => String(v))
- // If we have integer type with enum, change type to string
- if (result.type === "integer" || result.type === "number") {
- result.type = "string"
- }
- } else if (typeof value === "object" && value !== null) {
- result[key] = sanitizeGemini(value)
- } else {
- result[key] = value
+ const result: any = {}
+ for (const [key, value] of Object.entries(obj)) {
+ if (key === "enum" && Array.isArray(value)) {
+ // Convert all enum values to strings
+ result[key] = value.map((v) => String(v))
+ // If we have integer type with enum, change type to string
+ if (result.type === "integer" || result.type === "number") {
+ result.type = "string"
}
+ } else if (typeof value === "object" && value !== null) {
+ result[key] = sanitizeGemini(value)
+ } else {
+ result[key] = value
}
+ }
- // Filter required array to only include fields that exist in properties
- if (result.type === "object" && result.properties && Array.isArray(result.required)) {
- result.required = result.required.filter((field: any) => field in result.properties)
- }
+ // Filter required array to only include fields that exist in properties
+ if (result.type === "object" && result.properties && Array.isArray(result.required)) {
+ result.required = result.required.filter((field: any) => field in result.properties)
+ }
- if (result.type === "array" && !hasCombiner(result)) {
- if (result.items == null) {
- result.items = {}
- }
- // Ensure items has a type only when it's still schema-empty.
- if (isPlainObject(result.items) && !hasSchemaIntent(result.items)) {
- result.items.type = "string"
- }
+ if (result.type === "array" && !hasCombiner(result)) {
+ if (result.items == null) {
+ result.items = {}
}
-
- // Remove properties/required from non-object types (Gemini rejects these)
- if (result.type && result.type !== "object" && !hasCombiner(result)) {
- delete result.properties
- delete result.required
+ // Ensure items has a type only when it's still schema-empty.
+ if (isPlainObject(result.items) && !hasSchemaIntent(result.items)) {
+ result.items.type = "string"
}
+ }
- return result
+ // Remove properties/required from non-object types (Gemini rejects these)
+ if (result.type && result.type !== "object" && !hasCombiner(result)) {
+ delete result.properties
+ delete result.required
}
- schema = sanitizeGemini(schema)
+ return result
}
- return schema as JSONSchema7
+ schema = sanitizeGemini(schema)
}
+
+ return schema as JSONSchema7
}
diff --git a/packages/opencode/src/server/instance/httpapi/provider.ts b/packages/opencode/src/server/instance/httpapi/provider.ts
index e59f23f12..31dd1446a 100644
--- a/packages/opencode/src/server/instance/httpapi/provider.ts
+++ b/packages/opencode/src/server/instance/httpapi/provider.ts
@@ -1,4 +1,4 @@
-import { ProviderAuth } from "@/provider/auth"
+import { ProviderAuth } from "@/provider"
import { Effect, Layer } from "effect"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
diff --git a/packages/opencode/src/server/instance/provider.ts b/packages/opencode/src/server/instance/provider.ts
index 0057218f3..c1580437d 100644
--- a/packages/opencode/src/server/instance/provider.ts
+++ b/packages/opencode/src/server/instance/provider.ts
@@ -3,8 +3,8 @@ import { describeRoute, validator, resolver } from "hono-openapi"
import z from "zod"
import { Config } from "../../config"
import { Provider } from "../../provider"
-import { ModelsDev } from "../../provider/models"
-import { ProviderAuth } from "../../provider/auth"
+import { ModelsDev } from "../../provider"
+import { ProviderAuth } from "../../provider"
import { ProviderID } from "../../provider/schema"
import { AppRuntime } from "../../effect/app-runtime"
import { mapValues } from "remeda"
diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts
index 0652a599a..2d1577e7e 100644
--- a/packages/opencode/src/session/llm.ts
+++ b/packages/opencode/src/session/llm.ts
@@ -5,7 +5,7 @@ import * as Stream from "effect/Stream"
import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool, jsonSchema } from "ai"
import { mergeDeep, pipe } from "remeda"
import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider"
-import { ProviderTransform } from "@/provider/transform"
+import { ProviderTransform } from "@/provider"
import { Config } from "@/config"
import { Instance } from "@/project/instance"
import type { Agent } from "@/agent/agent"
diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts
index 5dcf0dcd1..f5ba74826 100644
--- a/packages/opencode/src/session/message-v2.ts
+++ b/packages/opencode/src/session/message-v2.ts
@@ -8,7 +8,7 @@ import { Snapshot } from "@/snapshot"
import { SyncEvent } from "../sync"
import { Database, NotFoundError, and, desc, eq, inArray, lt, or } from "@/storage"
import { MessageTable, PartTable, SessionTable } from "./session.sql"
-import { ProviderError } from "@/provider/error"
+import { ProviderError } from "@/provider"
import { iife } from "@/util/iife"
import { errorMessage } from "@/util/error"
import type { SystemError } from "bun"
diff --git a/packages/opencode/src/session/overflow.ts b/packages/opencode/src/session/overflow.ts
index 10f4bccda..6f48a760d 100644
--- a/packages/opencode/src/session/overflow.ts
+++ b/packages/opencode/src/session/overflow.ts
@@ -1,6 +1,6 @@
import type { Config } from "@/config"
import type { Provider } from "@/provider"
-import { ProviderTransform } from "@/provider/transform"
+import { ProviderTransform } from "@/provider"
import type { MessageV2 } from "./message-v2"
const COMPACTION_BUFFER = 20_000
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 44073c850..4b8b95baa 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -12,7 +12,7 @@ import { ModelID, ProviderID } from "../provider/schema"
import { type Tool as AITool, tool, jsonSchema, type ToolExecutionOptions, asSchema } from "ai"
import { SessionCompaction } from "./compaction"
import { Bus } from "../bus"
-import { ProviderTransform } from "../provider/transform"
+import { ProviderTransform } from "../provider"
import { SystemPrompt } from "./system"
import { Instruction } from "./instruction"
import { Plugin } from "../plugin"
diff --git a/packages/opencode/test/plugin/auth-override.test.ts b/packages/opencode/test/plugin/auth-override.test.ts
index 0c619c2ed..b570d8b14 100644
--- a/packages/opencode/test/plugin/auth-override.test.ts
+++ b/packages/opencode/test/plugin/auth-override.test.ts
@@ -4,7 +4,7 @@ import fs from "fs/promises"
import { Effect } from "effect"
import { tmpdir } from "../fixture/fixture"
import { Instance } from "../../src/project/instance"
-import { ProviderAuth } from "../../src/provider/auth"
+import { ProviderAuth } from "../../src/provider"
import { ProviderID } from "../../src/provider/schema"
describe("plugin.auth-override", () => {
diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts
index 300a5b903..df8fc4e96 100644
--- a/packages/opencode/test/provider/provider.test.ts
+++ b/packages/opencode/test/provider/provider.test.ts
@@ -6,7 +6,7 @@ import { tmpdir } from "../fixture/fixture"
import { Global } from "../../src/global"
import { Instance } from "../../src/project/instance"
import { Plugin } from "../../src/plugin/index"
-import { ModelsDev } from "../../src/provider/models"
+import { ModelsDev } from "../../src/provider"
import { Provider } from "../../src/provider"
import { ProviderID, ModelID } from "../../src/provider/schema"
import { Filesystem } from "../../src/util"
diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts
index 0e0810d0e..0666d0f64 100644
--- a/packages/opencode/test/provider/transform.test.ts
+++ b/packages/opencode/test/provider/transform.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
-import { ProviderTransform } from "../../src/provider/transform"
+import { ProviderTransform } from "../../src/provider"
import { ModelID, ProviderID } from "../../src/provider/schema"
describe("ProviderTransform.options - setCacheKey", () => {
diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts
index f26bef605..4d82096f3 100644
--- a/packages/opencode/test/session/llm.test.ts
+++ b/packages/opencode/test/session/llm.test.ts
@@ -7,8 +7,8 @@ import { makeRuntime } from "../../src/effect/run-service"
import { LLM } from "../../src/session/llm"
import { Instance } from "../../src/project/instance"
import { Provider } from "../../src/provider"
-import { ProviderTransform } from "../../src/provider/transform"
-import { ModelsDev } from "../../src/provider/models"
+import { ProviderTransform } from "../../src/provider"
+import { ModelsDev } from "../../src/provider"
import { ProviderID, ModelID } from "../../src/provider/schema"
import { Filesystem } from "../../src/util"
import { tmpdir } from "../fixture/fixture"