fix(core): preserve legacy Console reasoning variants (#46586)

This commit is contained in:
Victor Navarro 2026-09-01 13:45:26 +02:00 committed by GitHub
parent e297da82ae
commit cff1d0fe01
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 161 additions and 2 deletions

View file

@ -163,7 +163,11 @@ export const OpencodePlugin = define<HttpClient.HttpClient | Bus.Service | Scope
existing.settings = Provider.mergeOverlay(existing.settings, withoutCredentials(variant.settings))
if (variant.headers !== undefined)
existing.headers = Provider.mergeHeaders(existing.headers, variant.headers)
if (variant.body !== undefined) existing.body = Provider.mergeOverlay(existing.body, variant.body)
if (variant.body !== undefined)
existing.body = Provider.mergeOverlay(
existing.body,
variantBody(variant.body, model.package ?? item.package ?? source?.provider.package),
)
}
if (config.cost !== undefined)
model.cost = (Array.isArray(config.cost) ? config.cost : [config.cost]).map((cost) => ({
@ -232,6 +236,18 @@ function fetchProviders(http: HttpClient.HttpClient, value: Credential.Value) {
)
}
function variantBody(body: Readonly<Record<string, unknown>>, 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<Value>(body: Readonly<Record<string, Value>> | undefined) {
return (
body &&

View file

@ -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* () {