diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index c402238685f9..b383c81ab740 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -20,6 +20,22 @@ function mimeToModality(mime: string): Modality | undefined { export namespace ProviderTransform { export const OUTPUT_TOKEN_MAX = Flag.OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX || 32_000 + // Detects whether a model targets the Anthropic/Claude API regardless of + // the SDK package used to reach it (direct, proxy, vertex, bedrock, etc.). + function isClaudeModel(model: Provider.Model): boolean { + return ( + 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/google-vertex/anthropic" || + model.api.npm === "@ai-sdk/amazon-bedrock" + ) + } + // Maps npm package to the key the AI SDK expects for providerOptions function sdkKey(npm: string): string | undefined { switch (npm) { @@ -51,9 +67,7 @@ export namespace ProviderTransform { model: Provider.Model, options: Record, ): 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") { + if (isClaudeModel(model)) { msgs = msgs .map((msg) => { if (typeof msg.content === "string") { @@ -73,7 +87,7 @@ export namespace ProviderTransform { .filter((msg): msg is ModelMessage => msg !== undefined && msg.content !== "") } - if (model.api.id.includes("claude")) { + if (isClaudeModel(model)) { const scrub = (id: string) => id.replace(/[^a-zA-Z0-9_-]/g, "_") return msgs.map((msg) => { if (msg.role === "assistant" && Array.isArray(msg.content)) { @@ -278,16 +292,7 @@ export namespace ProviderTransform { export function message(msgs: ModelMessage[], model: Provider.Model, options: Record) { 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/gateway" - ) { + if (isClaudeModel(model) && model.api.npm !== "@ai-sdk/gateway") { msgs = applyCaching(msgs, model) } diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 0aee396f44a3..536b641600fc 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -1197,6 +1197,7 @@ describe("ProviderTransform.message - anthropic empty content filtering", () => test("does not filter for non-anthropic providers", () => { const openaiModel = { ...anthropicModel, + id: "openai/gpt-4", providerID: "openai", api: { id: "gpt-4",