From 635a8be437f390841786c04955bc867ecb84d619 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 12 Aug 2026 19:54:06 +0200 Subject: [PATCH] Restore model-default variant selection (#3281) * fix(providers): restore implicit model variant selection Provider catalogs list only explicit overrides, so treating the first override as the default made base model behavior unreachable. Represent the base choice in the catalog and translate it back to an omitted variant at the provider boundary. * test(providers): exercise variant catalog boundary The regression concerns the model catalog callers receive, so verify normalization through fetchCatalog instead of coupling the test to its internal builder. --- .../agent/providers/opencode-agent.test.ts | 41 ++++++++++++++++++- .../server/agent/providers/opencode-agent.ts | 38 +++++++++++------ 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/packages/server/src/server/agent/providers/opencode-agent.test.ts b/packages/server/src/server/agent/providers/opencode-agent.test.ts index 3a480f7b9..3931fe035 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.test.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.test.ts @@ -480,6 +480,7 @@ describe("OpenCodeAgentClient adapter smoke tests", () => { models: { "big-pickle": { name: "Big Pickle", + variants: { max: { reasoningEffort: "max" } }, limit: { context: 200_000, }, @@ -529,6 +530,11 @@ describe("OpenCodeAgentClient adapter smoke tests", () => { expect(catalog.models[0]).toMatchObject({ id: TEST_MODEL, label: "Big Pickle", + thinkingOptions: [ + { id: "default", label: "Default", isDefault: true }, + { id: "max", label: "max" }, + ], + defaultThinkingOptionId: "default", metadata: { providerId: "opencode", modelId: "big-pickle", @@ -824,7 +830,40 @@ describe("OpenCodeAgentClient adapter smoke tests", () => { }, 180_000); }); -describe("OpenCode adapter context-window normalization", () => { +describe("OpenCode adapter normalization", () => { + test("omits OpenCode's implicit default variant from new and updated sessions", async () => { + const runtime = new TestOpenCodeHarness(); + const openCode = new TestOpenCodeClient(); + openCode.sessionCreateResponse = { data: { id: "ses_default_variant" } }; + openCode.sessionPromptAsyncEvents = [ + { type: "session.idle", properties: { sessionID: "ses_default_variant" } }, + ]; + runtime.enqueueClient(openCode); + const client = new OpenCodeAgentClient(createTestLogger(), undefined, { + serverManager: runtime, + createClient: runtime.createClient, + }); + const session = await client.createSession({ + provider: "opencode", + cwd: "/workspace/repo", + model: "catalog-provider/single-variant-model", + thinkingOptionId: "default", + }); + + await collectTurnEvents(streamSession(session, "Use the model default")); + await session.setThinkingOption?.("max"); + await collectTurnEvents(streamSession(session, "Use max")); + await session.setThinkingOption?.("default"); + await collectTurnEvents(streamSession(session, "Return to the model default")); + + expect(openCode.calls.sessionPromptAsync).toEqual([ + expect.not.objectContaining({ variant: expect.anything() }), + expect.objectContaining({ variant: "max" }), + expect.not.objectContaining({ variant: expect.anything() }), + ]); + await session.close(); + }); + test("builds OpenCode file parts for image prompt blocks", () => { expect( __openCodeInternals.buildOpenCodePromptParts([ diff --git a/packages/server/src/server/agent/providers/opencode-agent.ts b/packages/server/src/server/agent/providers/opencode-agent.ts index 3ea0baab7..7dbe5d3b7 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.ts @@ -111,6 +111,7 @@ const OPENCODE_CAPABILITIES: AgentCapabilityFlags = { const OPENCODE_BUILD_MODE_ID = "build"; const OPENCODE_LEGACY_FULL_ACCESS_MODE_ID = "full-access"; +const OPENCODE_DEFAULT_VARIANT_ID = "default"; const OPENCODE_AUTO_ACCEPT_FEATURE_ID = "auto_accept"; const OPENCODE_PERSISTED_SESSION_LIMIT = 200; const OPENCODE_PENDING_ABORT_START_TIMEOUT_MS = 10_000; @@ -580,6 +581,14 @@ function normalizeOpenCodeModeId(modeId: string | null | undefined): string | nu return trimmed; } +function normalizeOpenCodeVariantId(variantId: string | null | undefined): string | null { + const trimmed = typeof variantId === "string" ? variantId.trim() : ""; + if (!trimmed || trimmed === OPENCODE_DEFAULT_VARIANT_ID) { + return null; + } + return trimmed; +} + function resolveOpenCodeRuntimeAgentId(modeId: string | null | undefined): string | undefined { const normalizedModeId = normalizeOpenCodeModeId(modeId); if (normalizedModeId === null) { @@ -591,15 +600,19 @@ function resolveOpenCodeRuntimeAgentId(modeId: string | null | undefined): strin } function normalizeOpenCodeConfig(config: OpenCodeAgentConfig): OpenCodeAgentConfig { - if (normalizeOpenCodeModeId(config.modeId) !== OPENCODE_LEGACY_FULL_ACCESS_MODE_ID) { - return { ...config }; + const normalized = { + ...config, + thinkingOptionId: normalizeOpenCodeVariantId(config.thinkingOptionId) ?? undefined, + }; + if (normalizeOpenCodeModeId(normalized.modeId) !== OPENCODE_LEGACY_FULL_ACCESS_MODE_ID) { + return normalized; } return { - ...config, + ...normalized, modeId: OPENCODE_BUILD_MODE_ID, featureValues: { - ...config.featureValues, + ...normalized.featureValues, [OPENCODE_AUTO_ACCEPT_FEATURE_ID]: true, }, }; @@ -723,11 +736,13 @@ function buildOpenCodeModelDefinition( }, ): AgentModelDefinition { const rawVariants = model.variants ? Object.keys(model.variants) : []; - const thinkingOptions = rawVariants.map((id, index) => ({ - id, - label: id, - isDefault: index === 0, - })); + // OpenCode lists only overrides; its base model behavior is selected by omitting `variant`. + const thinkingOptions = rawVariants.length + ? [ + { id: OPENCODE_DEFAULT_VARIANT_ID, label: "Default", isDefault: true }, + ...rawVariants.map((id) => ({ id, label: id })), + ] + : []; return { provider: "opencode", @@ -3195,10 +3210,7 @@ class OpenCodeAgentSession implements AgentSession { } async setThinkingOption(thinkingOptionId: string | null): Promise { - const normalizedThinkingOptionId = - typeof thinkingOptionId === "string" && thinkingOptionId.trim().length > 0 - ? thinkingOptionId - : null; + const normalizedThinkingOptionId = normalizeOpenCodeVariantId(thinkingOptionId); this.config.thinkingOptionId = normalizedThinkingOptionId ?? undefined; }