From cff1d0fe01e2d4c4af9786529edad645a5d5770e Mon Sep 17 00:00:00 2001 From: Victor Navarro Date: Tue, 1 Sep 2026 13:45:26 +0200 Subject: [PATCH] fix(core): preserve legacy Console reasoning variants (#46586) --- packages/core/src/plugin/provider/opencode.ts | 18 ++- .../test/plugin/provider-opencode.test.ts | 145 +++++++++++++++++- 2 files changed, 161 insertions(+), 2 deletions(-) diff --git a/packages/core/src/plugin/provider/opencode.ts b/packages/core/src/plugin/provider/opencode.ts index dd6f5524e51..0c6601f55be 100644 --- a/packages/core/src/plugin/provider/opencode.ts +++ b/packages/core/src/plugin/provider/opencode.ts @@ -163,7 +163,11 @@ export const OpencodePlugin = define ({ @@ -232,6 +236,18 @@ function fetchProviders(http: HttpClient.HttpClient, value: Credential.Value) { ) } +function variantBody(body: Readonly>, packageName: string | undefined) { + if (packageName !== Provider.aisdk("@ai-sdk/openai")) return body + const { reasoningEffort, reasoningSummary, ...native } = body + const reasoning = { + ...(typeof reasoningEffort === "string" ? { effort: reasoningEffort } : {}), + ...(typeof reasoningSummary === "string" ? { summary: reasoningSummary } : {}), + } + if (Object.keys(reasoning).length === 0) return body + // Existing Console variants stored SDK options here before V2 consumed raw bodies. + return Provider.mergeOverlay({ reasoning }, native) +} + function withoutCredentials(body: Readonly> | undefined) { return ( body && diff --git a/packages/core/test/plugin/provider-opencode.test.ts b/packages/core/test/plugin/provider-opencode.test.ts index 0f721471a63..7338cf1a4c0 100644 --- a/packages/core/test/plugin/provider-opencode.test.ts +++ b/packages/core/test/plugin/provider-opencode.test.ts @@ -1,10 +1,13 @@ import { describe, expect } from "bun:test" +import { LLM } from "@opencode-ai/ai" +import { LLMClient, RequestExecutor } from "@opencode-ai/ai/route" import { Money } from "@opencode-ai/schema/money" -import { Effect } from "effect" +import { Effect, Layer, Stream } from "effect" import { Catalog } from "@opencode-ai/core/catalog" import { Credential } from "@opencode-ai/core/credential" import { Integration } from "@opencode-ai/core/integration" import { Model } from "@opencode-ai/core/model" +import { ModelResolver } from "@opencode-ai/core/model-resolver" import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { OpencodePlugin } from "@opencode-ai/core/plugin/provider/opencode" @@ -376,6 +379,146 @@ describe("OpencodePlugin", () => { ), ) + it.live("normalizes legacy Console OpenAI variant bodies without changing native bodies", () => + Effect.acquireUseRelease( + Effect.sync(() => { + const variants = [ + { + id: "legacy", + body: { reasoningEffort: "high", reasoningSummary: "auto" }, + expected: { reasoning: { effort: "high", summary: "auto" } }, + }, + { + id: "effort-only", + body: { reasoningEffort: "low" }, + expected: { reasoning: { effort: "low" } }, + }, + { + id: "summary-only", + body: { reasoningSummary: "detailed" }, + expected: { reasoning: { summary: "detailed" } }, + }, + { + id: "native", + body: { reasoning: { effort: "high", summary: "auto" } }, + expected: { reasoning: { effort: "high", summary: "auto" } }, + }, + { + id: "mixed", + body: { + reasoningEffort: "low", + reasoningSummary: "auto", + reasoning: { effort: "high", summary: "detailed" }, + }, + expected: { reasoning: { effort: "high", summary: "detailed" } }, + }, + { id: "plain", body: {}, expected: {} }, + ] + const models = { + astra: { + modelID: "api-astra", + variants: variants.map((variant) => ({ + id: variant.id, + headers: { "x-variant": variant.id }, + body: { + ...variant.body, + include: ["reasoning.encrypted_content"], + metadata: { custom: "unchanged" }, + }, + })), + }, + } + const requests: { body: unknown; variant: string | null }[] = [] + return { + variants, + requests, + server: Bun.serve({ + port: 0, + fetch: async (request) => { + if (new URL(request.url).pathname === "/responses") { + requests.push({ body: await request.json(), variant: request.headers.get("x-variant") }) + return new Response('data: {"type":"response.completed","response":{"id":"resp_test"}}\n\n', { + headers: { "content-type": "text/event-stream" }, + }) + } + return Response.json({ + providers: { + remote: { + canonical: "openai", + package: "aisdk:@ai-sdk/openai", + settings: { baseURL: new URL(request.url).origin }, + models: { + ...models, + override: { ...models.astra, package: "aisdk:@ai-sdk/openai-compatible" }, + }, + }, + compatible: { + canonical: "openai", + package: "aisdk:@ai-sdk/openai-compatible", + models, + }, + }, + }) + }, + }), + } + }), + ({ variants, requests, server }) => + Effect.gen(function* () { + const credentials = yield* Credential.Service + const catalog = yield* Catalog.Service + const credential = Credential.Key.make({ + type: "key", + key: "secret", + metadata: { server: server.url.origin }, + }) + yield* credentials.create({ integrationID: Integration.ID.make("opencode"), value: credential }) + yield* addPlugin() + const model = required(yield* catalog.model.get(Provider.ID.make("remote"), Model.ID.make("astra"))) + expect(model.canonical).toBe(Provider.ID.openai) + yield* Effect.forEach(variants, (variant, index) => + Effect.gen(function* () { + const projected = required(model.variants.find((item) => item.id === variant.id)) + expect(projected.body).toEqual({ + ...variant.expected, + include: ["reasoning.encrypted_content"], + metadata: { custom: "unchanged" }, + }) + expect(projected.settings).toBeUndefined() + const resolved = yield* ModelResolver.resolveModel(model, Model.VariantID.make(variant.id), credential) + yield* LLMClient.stream(LLM.request({ model: resolved, prompt: "Hello" })).pipe( + Stream.runDrain, + Effect.provide(LLMClient.layer.pipe(Layer.provide(RequestExecutor.layer))), + ) + expect(requests).toHaveLength(index + 1) + const request = required(requests.at(-1)) + expect(request.variant).toBe(variant.id) + expect(request.body).toMatchObject({ + ...variant.expected, + model: "api-astra", + include: ["reasoning.encrypted_content"], + metadata: { custom: "unchanged" }, + }) + expect(request.body).not.toHaveProperty("reasoningEffort") + expect(request.body).not.toHaveProperty("reasoningSummary") + if (variant.id === "plain") expect(request.body).not.toHaveProperty("reasoning") + }), + ) + const compatible = required(yield* catalog.model.get(Provider.ID.make("compatible"), Model.ID.make("astra"))) + const override = required(yield* catalog.model.get(Provider.ID.make("remote"), Model.ID.make("override"))) + for (const model of [compatible, override]) { + expect(model.variants.find((variant) => variant.id === "legacy")?.body).toEqual({ + reasoningEffort: "high", + reasoningSummary: "auto", + include: ["reasoning.encrypted_content"], + metadata: { custom: "unchanged" }, + }) + } + }), + ({ server }) => Effect.promise(() => server.stop(true)), + ), + ) + it.effect("uses a public key and disables paid models without credentials", () => withEnv({ OPENCODE_API_KEY: undefined }, () => Effect.gen(function* () {