From 6661e32a0221b9bb1cedb4a6aeb25fe192609c86 Mon Sep 17 00:00:00 2001 From: Zeph Op Date: Wed, 14 Jan 2026 04:56:32 +0000 Subject: [PATCH 1/6] feat: support per-rule model fallbacks for outages and credit depletion (#1267) --- packages/opencode/src/agent/agent.ts | 14 ++++++ packages/opencode/src/config/config.ts | 1 + packages/opencode/src/session/processor.ts | 24 ++++++++++- packages/opencode/test/agent/agent.test.ts | 50 ++++++++++++++++++++++ 4 files changed, 88 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 6847d29abe5e..514d8f46f731 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -32,6 +32,14 @@ export namespace Agent { providerID: z.string(), }) .optional(), + models: z + .array( + z.object({ + modelID: z.string(), + providerID: z.string(), + }), + ) + .optional(), prompt: z.string().optional(), options: z.record(z.string(), z.any()), steps: z.number().int().positive().optional(), @@ -203,6 +211,12 @@ export namespace Agent { native: false, } if (value.model) item.model = Provider.parseModel(value.model) + if (value.models) { + item.models = value.models.map((m) => Provider.parseModel(m)) + if (!item.model && item.models.length > 0) { + item.model = item.models[0] + } + } item.prompt = value.prompt ?? item.prompt item.description = value.description ?? item.description item.temperature = value.temperature ?? item.temperature diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index bf4a6035bd8c..841b1f42f541 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -527,6 +527,7 @@ export namespace Config { export const Agent = z .object({ model: z.string().optional(), + models: z.string().array().optional(), temperature: z.number().optional(), top_p: z.number().optional(), prompt: z.string().optional(), diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 71db7f136771..86dc417ab7b9 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -9,7 +9,7 @@ import { Bus } from "@/bus" import { SessionRetry } from "./retry" import { SessionStatus } from "./status" import { Plugin } from "@/plugin" -import type { Provider } from "@/provider/provider" +import { Provider } from "@/provider/provider" import { LLM } from "./llm" import { Config } from "@/config/config" import { SessionCompaction } from "./compaction" @@ -46,6 +46,10 @@ export namespace SessionProcessor { log.info("process") needsCompaction = false const shouldBreak = (await Config.get()).experimental?.continue_loop_on_deny !== true + let fallbackIndex = + streamInput.agent.models?.findIndex( + (m) => m.modelID === streamInput.model.id && m.providerID === streamInput.model.providerID, + ) ?? -1 while (true) { try { let currentText: MessageV2.TextPart | undefined @@ -343,6 +347,24 @@ export namespace SessionProcessor { }) const error = MessageV2.fromError(e, { providerID: input.model.providerID }) const retry = SessionRetry.retryable(error) + + if (streamInput.agent.models && fallbackIndex < streamInput.agent.models.length - 1) { + fallbackIndex++ + const next = streamInput.agent.models[fallbackIndex] + try { + const nextModel = await Provider.getModel(next.providerID, next.modelID) + streamInput.model = nextModel + log.info("switching to fallback model", { + to: nextModel.id, + provider: nextModel.providerID, + }) + attempt = 0 + continue + } catch (loadError) { + log.error("failed to load fallback model", { error: loadError, model: next }) + } + } + if (retry !== undefined) { attempt++ const delay = SessionRetry.delay(attempt, error.name === "APIError" ? error : undefined) diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index 624655112bb2..79fc6c5012d8 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -512,3 +512,53 @@ test("explicit Truncate.DIR deny is respected", async () => { }, }) }) + +test("custom agent with fallback models parses correctly", async () => { + await using tmp = await tmpdir({ + config: { + agent: { + fallback_agent: { + models: ["anthropic/claude-3-sonnet", "openai/gpt-4o"], + description: "Agent with fallback", + }, + }, + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const agent = await Agent.get("fallback_agent") + expect(agent).toBeDefined() + expect(agent?.models).toHaveLength(2) + expect(agent?.models?.[0].providerID).toBe("anthropic") + expect(agent?.models?.[0].modelID).toBe("claude-3-sonnet") + expect(agent?.models?.[1].providerID).toBe("openai") + expect(agent?.models?.[1].modelID).toBe("gpt-4o") + // Primary model should default to first in models list if not set + expect(agent?.model?.providerID).toBe("anthropic") + expect(agent?.model?.modelID).toBe("claude-3-sonnet") + }, + }) +}) + +test("custom agent with model and fallback models preserves specific primary", async () => { + await using tmp = await tmpdir({ + config: { + agent: { + fallback_agent: { + model: "google/gemini-pro", + models: ["anthropic/claude-3-sonnet", "openai/gpt-4o"], + }, + }, + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const agent = await Agent.get("fallback_agent") + expect(agent?.model?.providerID).toBe("google") + expect(agent?.model?.modelID).toBe("gemini-pro") + expect(agent?.models).toHaveLength(2) + }, + }) +}) From 4a095c8f2ac0719d4fc69b93d8059d02e86768c5 Mon Sep 17 00:00:00 2001 From: Zeph Op Date: Thu, 15 Jan 2026 09:24:12 +0000 Subject: [PATCH 2/6] refactor: improve code quality in model fallback implementation - Extract tryFallbackModel() helper function to reduce nesting - Break long lines for better readability - Reduce nesting from 3 levels to 2 levels - Improve separation of concerns --- packages/opencode/src/session/processor.ts | 48 +++++++++++++++------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 86dc417ab7b9..57f28563fce5 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -20,6 +20,31 @@ export namespace SessionProcessor { const DOOM_LOOP_THRESHOLD = 3 const log = Log.create({ service: "session.processor" }) + async function tryFallbackModel( + streamInput: LLM.StreamInput, + currentIndex: number, + ): Promise { + const fallbackIndex = currentIndex + 1 + const next = streamInput.agent.models?.[fallbackIndex] + + if (!next) { + return false + } + + try { + const nextModel = await Provider.getModel(next.providerID, next.modelID) + streamInput.model = nextModel + log.info("switching to fallback model", { + to: nextModel.id, + provider: nextModel.providerID, + }) + return true + } catch (loadError) { + log.error("failed to load fallback model", { error: loadError, model: next }) + return false + } + } + export type Info = Awaited> export type Result = Awaited> @@ -46,10 +71,11 @@ export namespace SessionProcessor { log.info("process") needsCompaction = false const shouldBreak = (await Config.get()).experimental?.continue_loop_on_deny !== true - let fallbackIndex = - streamInput.agent.models?.findIndex( - (m) => m.modelID === streamInput.model.id && m.providerID === streamInput.model.providerID, - ) ?? -1 + + const currentModelIndex = streamInput.agent.models?.findIndex( + (m) => m.modelID === streamInput.model.id && m.providerID === streamInput.model.providerID, + ) ?? -1 + let fallbackIndex = currentModelIndex while (true) { try { let currentText: MessageV2.TextPart | undefined @@ -348,20 +374,12 @@ export namespace SessionProcessor { const error = MessageV2.fromError(e, { providerID: input.model.providerID }) const retry = SessionRetry.retryable(error) + // Try fallback model if available if (streamInput.agent.models && fallbackIndex < streamInput.agent.models.length - 1) { - fallbackIndex++ - const next = streamInput.agent.models[fallbackIndex] - try { - const nextModel = await Provider.getModel(next.providerID, next.modelID) - streamInput.model = nextModel - log.info("switching to fallback model", { - to: nextModel.id, - provider: nextModel.providerID, - }) + const fallbackSucceeded = await tryFallbackModel(streamInput, fallbackIndex) + if (fallbackSucceeded) { attempt = 0 continue - } catch (loadError) { - log.error("failed to load fallback model", { error: loadError, model: next }) } } From 932179163f8e0ae0ad5fd6b74c5904321dc627bf Mon Sep 17 00:00:00 2001 From: manascb1344 Date: Thu, 15 Jan 2026 18:19:27 +0530 Subject: [PATCH 3/6] fix: correct fallback model index tracking and add models to known keys - Fix fallbackIndex never incrementing after successful model switch - Add 'models' to knownKeys in config transform to prevent moving to options - Simplify tryFallbackModel to take targetIndex directly - Add tests for empty models array, options isolation, and native agent fallbacks --- packages/opencode/src/config/config.ts | 1 + packages/opencode/src/session/processor.ts | 16 +++--- packages/opencode/test/agent/agent.test.ts | 67 ++++++++++++++++++++++ 3 files changed, 75 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 841b1f42f541..04e77cdcd9bc 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -559,6 +559,7 @@ export namespace Config { const knownKeys = new Set([ "name", "model", + "models", "prompt", "description", "temperature", diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 57f28563fce5..26b2bb8b3054 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -20,12 +20,8 @@ export namespace SessionProcessor { const DOOM_LOOP_THRESHOLD = 3 const log = Log.create({ service: "session.processor" }) - async function tryFallbackModel( - streamInput: LLM.StreamInput, - currentIndex: number, - ): Promise { - const fallbackIndex = currentIndex + 1 - const next = streamInput.agent.models?.[fallbackIndex] + async function tryFallbackModel(streamInput: LLM.StreamInput, targetIndex: number): Promise { + const next = streamInput.agent.models?.[targetIndex] if (!next) { return false @@ -72,9 +68,10 @@ export namespace SessionProcessor { needsCompaction = false const shouldBreak = (await Config.get()).experimental?.continue_loop_on_deny !== true - const currentModelIndex = streamInput.agent.models?.findIndex( - (m) => m.modelID === streamInput.model.id && m.providerID === streamInput.model.providerID, - ) ?? -1 + const currentModelIndex = + streamInput.agent.models?.findIndex( + (m) => m.modelID === streamInput.model.id && m.providerID === streamInput.model.providerID, + ) ?? -1 let fallbackIndex = currentModelIndex while (true) { try { @@ -376,6 +373,7 @@ export namespace SessionProcessor { // Try fallback model if available if (streamInput.agent.models && fallbackIndex < streamInput.agent.models.length - 1) { + fallbackIndex++ const fallbackSucceeded = await tryFallbackModel(streamInput, fallbackIndex) if (fallbackSucceeded) { attempt = 0 diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index 79fc6c5012d8..97ac5afeebae 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -562,3 +562,70 @@ test("custom agent with model and fallback models preserves specific primary", a }, }) }) + +test("fallback models with empty array does not set primary model", async () => { + await using tmp = await tmpdir({ + config: { + agent: { + empty_fallback: { + models: [], + description: "Agent with empty models array", + }, + }, + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const agent = await Agent.get("empty_fallback") + expect(agent).toBeDefined() + expect(agent?.models).toHaveLength(0) + expect(agent?.model).toBeUndefined() + }, + }) +}) + +test("fallback models are not moved to options", async () => { + await using tmp = await tmpdir({ + config: { + agent: { + fallback_agent: { + models: ["anthropic/claude-3-sonnet", "openai/gpt-4o"], + }, + }, + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const agent = await Agent.get("fallback_agent") + expect(agent?.models).toBeDefined() + expect(agent?.options.models).toBeUndefined() + }, + }) +}) + +test("native agent can have fallback models configured", async () => { + await using tmp = await tmpdir({ + config: { + agent: { + build: { + models: ["anthropic/claude-3-opus", "anthropic/claude-3-sonnet", "openai/gpt-4o"], + }, + }, + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const build = await Agent.get("build") + expect(build).toBeDefined() + expect(build?.native).toBe(true) + expect(build?.models).toHaveLength(3) + expect(build?.models?.[0].providerID).toBe("anthropic") + expect(build?.models?.[0].modelID).toBe("claude-3-opus") + expect(build?.model?.providerID).toBe("anthropic") + expect(build?.model?.modelID).toBe("claude-3-opus") + }, + }) +}) From 002d98228613564f11f641b36448ba629669281f Mon Sep 17 00:00:00 2001 From: manascb1344 Date: Fri, 16 Jan 2026 13:30:33 +0530 Subject: [PATCH 4/6] refactor: replace per-agent models with centralized fallbacks config - Add global fallbacks config with provider and model-specific rules - Create SessionFallback module for fallback resolution logic - Remove per-agent models field from Agent schema and config - Model-specific fallbacks take priority over provider-level - Track attempted fallbacks to avoid retries on same model BREAKING CHANGE: Fallbacks are now configured globally: Before: agent: build: models: [anthropic/claude-3-opus, openai/gpt-4o] After: fallbacks: models: anthropic/claude-3-opus: [openai/gpt-4o] provider: anthropic: [openai] --- packages/opencode/src/agent/agent.ts | 14 --- packages/opencode/src/config/config.ts | 26 +++- packages/opencode/src/session/fallback.ts | 110 ++++++++++++++++ packages/opencode/src/session/processor.ts | 46 ++----- packages/opencode/test/agent/agent.test.ts | 117 ------------------ packages/opencode/test/config/config.test.ts | 85 +++++++++++++ .../opencode/test/session/fallback.test.ts | 65 ++++++++++ 7 files changed, 296 insertions(+), 167 deletions(-) create mode 100644 packages/opencode/src/session/fallback.ts create mode 100644 packages/opencode/test/session/fallback.test.ts diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 514d8f46f731..6847d29abe5e 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -32,14 +32,6 @@ export namespace Agent { providerID: z.string(), }) .optional(), - models: z - .array( - z.object({ - modelID: z.string(), - providerID: z.string(), - }), - ) - .optional(), prompt: z.string().optional(), options: z.record(z.string(), z.any()), steps: z.number().int().positive().optional(), @@ -211,12 +203,6 @@ export namespace Agent { native: false, } if (value.model) item.model = Provider.parseModel(value.model) - if (value.models) { - item.models = value.models.map((m) => Provider.parseModel(m)) - if (!item.model && item.models.length > 0) { - item.model = item.models[0] - } - } item.prompt = value.prompt ?? item.prompt item.description = value.description ?? item.description item.temperature = value.temperature ?? item.temperature diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 04e77cdcd9bc..7c371cca255a 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -527,7 +527,6 @@ export namespace Config { export const Agent = z .object({ model: z.string().optional(), - models: z.string().array().optional(), temperature: z.number().optional(), top_p: z.number().optional(), prompt: z.string().optional(), @@ -559,7 +558,6 @@ export namespace Config { const knownKeys = new Set([ "name", "model", - "models", "prompt", "description", "temperature", @@ -839,6 +837,27 @@ export namespace Config { }) export type Provider = z.infer + export const Fallbacks = z + .object({ + provider: z + .record(z.string(), z.array(z.string())) + .optional() + .describe( + "Provider-level fallbacks: { 'anthropic': ['openai', 'google'] }. If any model from a provider fails, try providers in order.", + ), + models: z + .record(z.string(), z.array(z.string())) + .optional() + .describe( + "Model-specific fallbacks: { 'anthropic/claude-3-opus': ['openai/gpt-4o'] }. If this specific model fails, try these alternatives.", + ), + }) + .strict() + .meta({ + ref: "FallbacksConfig", + }) + export type Fallbacks = z.infer + export const Info = z .object({ $schema: z.string().optional().describe("JSON schema reference for configuration validation"), @@ -922,6 +941,9 @@ export namespace Config { .record(z.string(), Provider) .optional() .describe("Custom provider configurations and model overrides"), + fallbacks: Fallbacks.optional().describe( + "Global fallback configuration for provider outages and credit depletion", + ), mcp: z .record( z.string(), diff --git a/packages/opencode/src/session/fallback.ts b/packages/opencode/src/session/fallback.ts new file mode 100644 index 000000000000..67f6f75cf6dc --- /dev/null +++ b/packages/opencode/src/session/fallback.ts @@ -0,0 +1,110 @@ +import { Config } from "@/config/config" +import { Provider } from "@/provider/provider" +import { Log } from "@/util/log" + +export namespace SessionFallback { + const log = Log.create({ service: "session.fallback" }) + + interface FallbackResult { + model: Provider.Model + isProviderLevel: boolean + } + + export async function getFallback( + providerID: string, + modelID: string, + attempted: Set, + ): Promise { + const config = await Config.get() + const fallbacks = config.fallbacks + + if (!fallbacks) { + return null + } + + // Check model-specific fallbacks first (higher priority) + const modelKey = `${providerID}/${modelID}` + if (fallbacks.models) { + const modelFallbacks = fallbacks.models[modelKey] + if (modelFallbacks && modelFallbacks.length > 0) { + for (const fallback of modelFallbacks) { + const attemptKey = `model:${fallback}` + if (attempted.has(attemptKey)) { + continue + } + + try { + const [fallbackProvider, fallbackModel] = fallback.split("/") + if (!fallbackProvider || !fallbackModel) { + log.warn("invalid fallback model format", { fallback }) + continue + } + + const nextModel = await Provider.getModel(fallbackProvider, fallbackModel) + log.info("using model-specific fallback", { + from: modelKey, + to: nextModel.id, + provider: nextModel.providerID, + }) + return { model: nextModel, isProviderLevel: false } + } catch (loadError) { + log.error("failed to load model-specific fallback", { + error: loadError, + fallback, + from: modelKey, + }) + attempted.add(attemptKey) + } + } + } + } + + // Check provider-level fallbacks + if (fallbacks.provider) { + const providerFallbacks = fallbacks.provider[providerID] + if (providerFallbacks && providerFallbacks.length > 0) { + for (const fallbackProvider of providerFallbacks) { + const attemptKey = `provider:${fallbackProvider}` + if (attempted.has(attemptKey)) { + continue + } + + try { + // For provider fallbacks, use the provider's first available model + const providers = await Provider.list() + const fallbackProviderInfo = providers[fallbackProvider] + if (!fallbackProviderInfo) { + log.warn("fallback provider not found", { provider: fallbackProvider }) + attempted.add(attemptKey) + continue + } + + const models = Object.values(fallbackProviderInfo.models) + if (models.length === 0) { + log.warn("fallback provider has no models", { provider: fallbackProvider }) + attempted.add(attemptKey) + continue + } + + const nextModel = models[0] + log.info("using provider-level fallback", { + fromProvider: providerID, + toProvider: fallbackProvider, + toModel: nextModel.id, + }) + return { model: nextModel, isProviderLevel: true } + } catch (loadError) { + log.error("failed to load provider fallback", { + error: loadError, + provider: fallbackProvider, + fromProvider: providerID, + }) + attempted.add(attemptKey) + } + } + } + } + + return null + } +} diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 26b2bb8b3054..76384c05f68c 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -15,32 +15,12 @@ import { Config } from "@/config/config" import { SessionCompaction } from "./compaction" import { PermissionNext } from "@/permission/next" import { Question } from "@/question" +import { SessionFallback } from "./fallback" export namespace SessionProcessor { const DOOM_LOOP_THRESHOLD = 3 const log = Log.create({ service: "session.processor" }) - async function tryFallbackModel(streamInput: LLM.StreamInput, targetIndex: number): Promise { - const next = streamInput.agent.models?.[targetIndex] - - if (!next) { - return false - } - - try { - const nextModel = await Provider.getModel(next.providerID, next.modelID) - streamInput.model = nextModel - log.info("switching to fallback model", { - to: nextModel.id, - provider: nextModel.providerID, - }) - return true - } catch (loadError) { - log.error("failed to load fallback model", { error: loadError, model: next }) - return false - } - } - export type Info = Awaited> export type Result = Awaited> @@ -68,11 +48,7 @@ export namespace SessionProcessor { needsCompaction = false const shouldBreak = (await Config.get()).experimental?.continue_loop_on_deny !== true - const currentModelIndex = - streamInput.agent.models?.findIndex( - (m) => m.modelID === streamInput.model.id && m.providerID === streamInput.model.providerID, - ) ?? -1 - let fallbackIndex = currentModelIndex + const attemptedFallbacks = new Set() while (true) { try { let currentText: MessageV2.TextPart | undefined @@ -371,14 +347,16 @@ export namespace SessionProcessor { const error = MessageV2.fromError(e, { providerID: input.model.providerID }) const retry = SessionRetry.retryable(error) - // Try fallback model if available - if (streamInput.agent.models && fallbackIndex < streamInput.agent.models.length - 1) { - fallbackIndex++ - const fallbackSucceeded = await tryFallbackModel(streamInput, fallbackIndex) - if (fallbackSucceeded) { - attempt = 0 - continue - } + // Try fallback model from global config + const fallback = await SessionFallback.getFallback( + input.model.providerID, + input.model.id, + attemptedFallbacks, + ) + if (fallback) { + streamInput.model = fallback.model + attempt = 0 + continue } if (retry !== undefined) { diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index 97ac5afeebae..624655112bb2 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -512,120 +512,3 @@ test("explicit Truncate.DIR deny is respected", async () => { }, }) }) - -test("custom agent with fallback models parses correctly", async () => { - await using tmp = await tmpdir({ - config: { - agent: { - fallback_agent: { - models: ["anthropic/claude-3-sonnet", "openai/gpt-4o"], - description: "Agent with fallback", - }, - }, - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const agent = await Agent.get("fallback_agent") - expect(agent).toBeDefined() - expect(agent?.models).toHaveLength(2) - expect(agent?.models?.[0].providerID).toBe("anthropic") - expect(agent?.models?.[0].modelID).toBe("claude-3-sonnet") - expect(agent?.models?.[1].providerID).toBe("openai") - expect(agent?.models?.[1].modelID).toBe("gpt-4o") - // Primary model should default to first in models list if not set - expect(agent?.model?.providerID).toBe("anthropic") - expect(agent?.model?.modelID).toBe("claude-3-sonnet") - }, - }) -}) - -test("custom agent with model and fallback models preserves specific primary", async () => { - await using tmp = await tmpdir({ - config: { - agent: { - fallback_agent: { - model: "google/gemini-pro", - models: ["anthropic/claude-3-sonnet", "openai/gpt-4o"], - }, - }, - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const agent = await Agent.get("fallback_agent") - expect(agent?.model?.providerID).toBe("google") - expect(agent?.model?.modelID).toBe("gemini-pro") - expect(agent?.models).toHaveLength(2) - }, - }) -}) - -test("fallback models with empty array does not set primary model", async () => { - await using tmp = await tmpdir({ - config: { - agent: { - empty_fallback: { - models: [], - description: "Agent with empty models array", - }, - }, - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const agent = await Agent.get("empty_fallback") - expect(agent).toBeDefined() - expect(agent?.models).toHaveLength(0) - expect(agent?.model).toBeUndefined() - }, - }) -}) - -test("fallback models are not moved to options", async () => { - await using tmp = await tmpdir({ - config: { - agent: { - fallback_agent: { - models: ["anthropic/claude-3-sonnet", "openai/gpt-4o"], - }, - }, - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const agent = await Agent.get("fallback_agent") - expect(agent?.models).toBeDefined() - expect(agent?.options.models).toBeUndefined() - }, - }) -}) - -test("native agent can have fallback models configured", async () => { - await using tmp = await tmpdir({ - config: { - agent: { - build: { - models: ["anthropic/claude-3-opus", "anthropic/claude-3-sonnet", "openai/gpt-4o"], - }, - }, - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const build = await Agent.get("build") - expect(build).toBeDefined() - expect(build?.native).toBe(true) - expect(build?.models).toHaveLength(3) - expect(build?.models?.[0].providerID).toBe("anthropic") - expect(build?.models?.[0].modelID).toBe("claude-3-opus") - expect(build?.model?.providerID).toBe("anthropic") - expect(build?.model?.modelID).toBe("claude-3-opus") - }, - }) -}) diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 86cadca5d815..45375e5de088 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -1373,4 +1373,89 @@ describe("deduplicatePlugins", () => { }, }) }) + + describe("fallbacks config", () => { + test("parses fallbacks with provider-level rules", async () => { + await using tmp = await tmpdir({ + config: { + fallbacks: { + provider: { + anthropic: ["openai", "google"], + }, + }, + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.fallbacks).toBeDefined() + expect(config.fallbacks?.provider).toBeDefined() + expect(config.fallbacks?.provider?.anthropic).toEqual(["openai", "google"]) + }, + }) + }) + + test("parses fallbacks with model-specific rules", async () => { + await using tmp = await tmpdir({ + config: { + fallbacks: { + models: { + "anthropic/claude-3-opus": ["openai/gpt-4o"], + }, + }, + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.fallbacks).toBeDefined() + expect(config.fallbacks?.models).toBeDefined() + expect(config.fallbacks?.models?.["anthropic/claude-3-opus"]).toEqual(["openai/gpt-4o"]) + }, + }) + }) + + test("parses fallbacks with both provider and model rules", async () => { + await using tmp = await tmpdir({ + config: { + fallbacks: { + provider: { + anthropic: ["openai"], + }, + models: { + "google/gemini-pro": ["anthropic/claude-3-sonnet"], + }, + }, + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.fallbacks).toBeDefined() + expect(config.fallbacks?.provider?.anthropic).toEqual(["openai"]) + expect(config.fallbacks?.models?.["google/gemini-pro"]).toEqual(["anthropic/claude-3-sonnet"]) + }, + }) + }) + + test("allows empty fallbacks object", async () => { + await using tmp = await tmpdir({ + config: { + fallbacks: {}, + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.fallbacks).toBeDefined() + expect(config.fallbacks?.provider).toBeUndefined() + expect(config.fallbacks?.models).toBeUndefined() + }, + }) + }) + }) }) diff --git a/packages/opencode/test/session/fallback.test.ts b/packages/opencode/test/session/fallback.test.ts new file mode 100644 index 000000000000..37d443a76164 --- /dev/null +++ b/packages/opencode/test/session/fallback.test.ts @@ -0,0 +1,65 @@ +import { test, expect, describe, beforeEach, mock } from "bun:test" +import { SessionFallback } from "../../src/session/fallback" +import { Config } from "../../src/config/config" +import { Instance } from "../../src/project/instance" +import { tmpdir } from "../fixture/fixture" + +describe("SessionFallback.getFallback", () => { + test("returns null when no fallbacks configured", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const result = await SessionFallback.getFallback("anthropic", "claude-3-opus", new Set()) + expect(result).toBeNull() + }, + }) + }) + + test("returns null when model has no fallbacks and provider has no fallbacks", async () => { + await using tmp = await tmpdir({ + config: { + fallbacks: { + provider: { + google: ["openai"], + }, + models: { + "google/gemini-pro": ["anthropic/claude-3-sonnet"], + }, + }, + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const result = await SessionFallback.getFallback("anthropic", "claude-3-opus", new Set()) + expect(result).toBeNull() + }, + }) + }) + + test("skips already attempted fallbacks", async () => { + await using tmp = await tmpdir({ + config: { + fallbacks: { + models: { + "anthropic/claude-3-opus": ["openai/gpt-4o", "google/gemini-pro"], + }, + }, + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const attempted = new Set() + attempted.add("model:openai/gpt-4o") + + // Should skip openai/gpt-4o and try google/gemini-pro next + const result = await SessionFallback.getFallback("anthropic", "claude-3-opus", attempted) + // Result will be null because the models don't actually exist in test environment + // but it should have skipped the attempted one + expect(attempted.has("model:openai/gpt-4o")).toBe(true) + }, + }) + }) +}) From e1e356cab4a9425fa287e4b01efd15187b8a9450 Mon Sep 17 00:00:00 2001 From: opencode Date: Fri, 30 Jan 2026 06:48:49 +0000 Subject: [PATCH 5/6] release: v1.1.45 --- bun.lock | 36 ++++++++++++-------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 ++++----- packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/util/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 18 files changed, 38 insertions(+), 42 deletions(-) diff --git a/bun.lock b/bun.lock index 99d35168f30c..538712601d5c 100644 --- a/bun.lock +++ b/bun.lock @@ -18,14 +18,12 @@ "prettier": "3.6.2", "semver": "^7.6.0", "sst": "3.17.23", - "stackback": "0.0.2", "turbo": "2.5.6", - "why-is-node-running": "2.2.2", }, }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -75,7 +73,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -109,7 +107,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -136,7 +134,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -160,7 +158,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -184,7 +182,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -215,7 +213,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -244,7 +242,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -260,7 +258,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.1.43", + "version": "1.1.45", "bin": { "opencode": "./bin/opencode", }, @@ -364,7 +362,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -384,7 +382,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.1.43", + "version": "1.1.45", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -395,7 +393,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -408,7 +406,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -450,7 +448,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "zod": "catalog:", }, @@ -461,7 +459,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -3901,7 +3899,7 @@ "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], - "why-is-node-running": ["why-is-node-running@2.2.2", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA=="], + "why-is-node-running": ["why-is-node-running@3.2.2", "", { "bin": { "why-is-node-running": "cli.js" } }, "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A=="], "widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="], @@ -4389,8 +4387,6 @@ "opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], - "opencode/why-is-node-running": ["why-is-node-running@3.2.2", "", { "bin": { "why-is-node-running": "cli.js" } }, "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A=="], - "opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="], "opencontrol/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="], diff --git a/packages/app/package.json b/packages/app/package.json index 87ca7931c550..8459172e7cfc 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.1.43", + "version": "1.1.45", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index b10593a8fa5f..84d45d3a7b1d 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.1.43", + "version": "1.1.45", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 111390efbe76..38811d4de296 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.1.43", + "version": "1.1.45", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index c26b4584dedc..50acf1567c62 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.1.43", + "version": "1.1.45", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 564a7140cc1a..902dd447bed6 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index c1f330ce9302..1b01f8a6f2b9 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.1.43", + "version": "1.1.45", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 03016c1c58f7..2091e6857497 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.1.43", + "version": "1.1.45", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index eaa882b65bd1..283036bf5b18 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.1.43" +version = "1.1.45" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.43/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.45/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.43/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.45/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.43/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.45/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.43/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.45/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.43/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.45/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 4a9e82def5ec..9746d11dccfd 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.1.43", + "version": "1.1.45", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index bd77afa2e8c4..ab28687d3588 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.1.43", + "version": "1.1.45", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 19077d786d61..9e9232a4da7c 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.1.43", + "version": "1.1.45", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index e929d628943a..6b550512dea3 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.1.43", + "version": "1.1.45", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/slack/package.json b/packages/slack/package.json index decfd834a74b..fbcccac399a3 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.1.43", + "version": "1.1.45", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 2e7a2cfefdf1..5fde0960aca0 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.1.43", + "version": "1.1.45", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index f76106701b27..9a3eb8f7d330 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.1.43", + "version": "1.1.45", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index 0f601ba248f2..06ab5ef4e952 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.1.43", + "version": "1.1.45", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 1ffe553c2cd0..7c160a454fbd 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.1.43", + "version": "1.1.45", "publisher": "sst-dev", "repository": { "type": "git", From 00637c0269312455e55e4977a7a8f55c728e93bd Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 30 Jan 2026 00:56:47 -0600 Subject: [PATCH 6/6] fix: rm ai sdk middleware that was preventing blocks from being sent back as assistant message content (#11270) Co-authored-by: opencode-agent[bot] --- packages/opencode/src/session/llm.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 4c4f4114a281..0c765210452a 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -1,4 +1,3 @@ -import os from "os" import { Installation } from "@/installation" import { Provider } from "@/provider/provider" import { Log } from "@/util/log" @@ -9,7 +8,6 @@ import { type StreamTextResult, type Tool, type ToolSet, - extractReasoningMiddleware, tool, jsonSchema, } from "ai" @@ -261,7 +259,6 @@ export namespace LLM { return args.params }, }, - extractReasoningMiddleware({ tagName: "think", startWithReasoning: false }), ], }), experimental_telemetry: {