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; }