From a71496d52b91557511a7726265ebb1a33cf2eaea Mon Sep 17 00:00:00 2001 From: Anthony Gallon <64985097+AntzCode@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:03:06 +1300 Subject: [PATCH 1/2] feat(opencode-go): opt-in temperature for Responses models (#2055) OpenCode Go Responses-format models now send temperature only for verified model and reasoning-effort combinations. This applies to both the streaming and non-streaming Responses requests. The settings UI uses the same rule: it hides the temperature control when the value would not be sent. Hiding the control does not clear or reset a saved temperature. - grok-4.6, muse-spark-1.3-contributor and muse-spark-1.2-contributor: temperature is sent at any reasoning effort. - gpt-5.6-luna and gpt-6-luna: temperature is sent only when the effective reasoning effort is "none". At other efforts the gateway returns HTTP 400 for every value except 1, which is the OpenAI reasoning default rather than real support, so the value is left out. - Temperature is sent only when the user has set a custom value. The provider default is never sent on this route. Changes: - types: add OPENCODE_GO_TEMPERATURE_REASONING_EFFORTS and opencodeGoResponsesSupportsTemperature(), and set supportsTemperature on every Responses model. - handler: add getResponsesTemperature(), which reads the raw modelTemperature setting and applies the opt-in rule; remove the unused temperature parameter from streamResponsesMessage; update the route comments so they no longer say the Responses route is Luna-only. - webview: add isTemperatureControlSupported(), which ApiOptions uses to decide whether to show TemperatureControl. It works out the effective reasoning effort the same way the request builder does. - tests: cover the rule, the model registry, the fetcher merge, both request modes, a stale "none" effort while reasoning is switched off, and the UI visibility round trip with the saved value kept. Shared transforms (getModelParams, RouterProvider) are unchanged. Refs #2006 Closes #2055 Co-Authored-By: Claude Opus 5.5 --- .../types/src/__tests__/opencode-go.test.ts | 56 +++++++ packages/types/src/providers/opencode-go.ts | 60 +++++++- .../providers/__tests__/opencode-go.spec.ts | 143 +++++++++++++++++- .../fetchers/__tests__/opencode-go.spec.ts | 26 +++- src/api/providers/opencode-go.ts | 44 ++++-- .../src/components/settings/ApiOptions.tsx | 8 +- .../settings/__tests__/ApiOptions.spec.tsx | 69 ++++++++- .../__tests__/temperatureSupport.spec.ts | 81 ++++++++++ .../settings/utils/temperatureSupport.ts | 54 +++++++ 9 files changed, 520 insertions(+), 21 deletions(-) create mode 100644 webview-ui/src/components/settings/utils/__tests__/temperatureSupport.spec.ts create mode 100644 webview-ui/src/components/settings/utils/temperatureSupport.ts diff --git a/packages/types/src/__tests__/opencode-go.test.ts b/packages/types/src/__tests__/opencode-go.test.ts index 2f434939a1..cfcde4089c 100644 --- a/packages/types/src/__tests__/opencode-go.test.ts +++ b/packages/types/src/__tests__/opencode-go.test.ts @@ -8,7 +8,10 @@ import { isOpencodeGoAnthropicFormatModel, isOpencodeGoResponsesFormatModel, getOpencodeGoModelInfo, + OPENCODE_GO_TEMPERATURE_REASONING_EFFORTS, + opencodeGoResponsesSupportsTemperature, } from "../providers/opencode-go.js" +import { reasoningEffortsExtended } from "../model.js" describe("opencode-go registry", () => { const anthropicFormatModels = [ @@ -170,6 +173,7 @@ describe("opencode-go registry", () => { it("curates gpt-5.6-luna with its Go Responses capabilities", () => { expect(getOpencodeGoModelInfo("gpt-5.6-luna")).toMatchObject({ supportsMaxTokens: true, + supportsTemperature: false, maxTokens: 128_000, contextWindow: 1_050_000, supportsImages: true, @@ -221,6 +225,58 @@ describe("opencode-go registry", () => { }) }) + describe("opencodeGoResponsesSupportsTemperature (Zoo-Code-Org/Zoo-Code#2055)", () => { + const gptLunaModels = ["gpt-5.6-luna", "gpt-6-luna"] + const alwaysSupportedModels = ["grok-4.6", "muse-spark-1.3-contributor", "muse-spark-1.2-contributor"] + + it.each(alwaysSupportedModels)("%s supports temperature for every effort and with none sent", (id) => { + expect(getOpencodeGoModelInfo(id)?.supportsTemperature).toBe(true) + expect(opencodeGoResponsesSupportsTemperature(id, undefined)).toBe(true) + for (const effort of reasoningEffortsExtended) { + expect(opencodeGoResponsesSupportsTemperature(id, effort)).toBe(true) + } + }) + + it.each(gptLunaModels)("%s supports temperature only with reasoning effort none", (id) => { + expect(getOpencodeGoModelInfo(id)?.supportsTemperature).toBe(false) + expect(opencodeGoResponsesSupportsTemperature(id, "none")).toBe(true) + for (const effort of reasoningEffortsExtended.filter((e) => e !== "none")) { + expect(opencodeGoResponsesSupportsTemperature(id, effort)).toBe(false) + } + // No effort on the wire is not a verified configuration. + expect(opencodeGoResponsesSupportsTemperature(id, undefined)).toBe(false) + }) + + it("lists only the GPT Luna models in the effort-conditional table", () => { + expect(Object.keys(OPENCODE_GO_TEMPERATURE_REASONING_EFFORTS).sort()).toEqual(gptLunaModels) + }) + + it("lists only advertised efforts for Responses models in the effort-conditional table", () => { + for (const [id, efforts] of Object.entries(OPENCODE_GO_TEMPERATURE_REASONING_EFFORTS)) { + expect(isOpencodeGoResponsesFormatModel(id)).toBe(true) + // The table overrides a baseline opt-out, so the model must declare one. + expect(getOpencodeGoModelInfo(id)?.supportsTemperature).toBe(false) + const advertised = getOpencodeGoModelInfo(id)?.supportsReasoningEffort + expect(Array.isArray(advertised)).toBe(true) + for (const effort of efforts) { + expect(advertised).toContain(effort) + } + } + }) + + it("declares an explicit temperature capability for every Responses-format model", () => { + for (const id of OPENCODE_GO_RESPONSES_FORMAT_MODELS) { + expect(typeof getOpencodeGoModelInfo(id)?.supportsTemperature).toBe("boolean") + } + }) + + it("returns false for unknown and non-Responses models", () => { + expect(opencodeGoResponsesSupportsTemperature("some-future-model", "none")).toBe(false) + // kimi-k2.6 declares supportsTemperature: true but is not a Responses model. + expect(opencodeGoResponsesSupportsTemperature("kimi-k2.6", undefined)).toBe(false) + }) + }) + describe("opencodeGoModels registry invariants", () => { it.each([ { diff --git a/packages/types/src/providers/opencode-go.ts b/packages/types/src/providers/opencode-go.ts index 49f190ccd6..827cac1c58 100644 --- a/packages/types/src/providers/opencode-go.ts +++ b/packages/types/src/providers/opencode-go.ts @@ -1,4 +1,4 @@ -import type { ModelInfo } from "../model.js" +import type { ModelInfo, ReasoningEffortExtended } from "../model.js" // Opencode "Go" plan — OpenAI-compatible gateway. // https://opencode.ai/docs/go/ · base URL: https://opencode.ai/zen/go/v1 @@ -574,6 +574,9 @@ export const opencodeGoModels: Record = { supportsImages: true, supportsPromptCache: true, supportsMaxTokens: true, + // Temperature is accepted only with reasoning effort "none"; see + // OPENCODE_GO_TEMPERATURE_REASONING_EFFORTS. + supportsTemperature: false, supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh", "max"], reasoningEffort: "medium", inputPrice: 0.2, @@ -596,8 +599,8 @@ export const opencodeGoModels: Record = { supportsImages: true, supportsPromptCache: true, supportsMaxTokens: true, - // The OpenCode Go Responses model silently drops `temperature`, so the - // UI temperature control would otherwise render as a dead control. + // Temperature is accepted only with reasoning effort "none"; see + // OPENCODE_GO_TEMPERATURE_REASONING_EFFORTS. supportsTemperature: false, supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh", "max"], reasoningEffort: "medium", @@ -640,6 +643,7 @@ export const opencodeGoModels: Record = { supportsImages: true, supportsPromptCache: true, supportsMaxTokens: true, + supportsTemperature: true, supportsReasoningEffort: ["low", "medium", "high", "xhigh"], reasoningEffort: "high", inputPrice: 2.0, @@ -660,6 +664,7 @@ export const opencodeGoModels: Record = { supportsImages: true, supportsPromptCache: true, supportsMaxTokens: true, + supportsTemperature: true, supportsReasoningEffort: ["minimal", "low", "medium", "high", "xhigh"], reasoningEffort: "medium", inputPrice: 0.1, @@ -674,6 +679,7 @@ export const opencodeGoModels: Record = { supportsImages: true, supportsPromptCache: true, supportsMaxTokens: true, + supportsTemperature: true, supportsReasoningEffort: ["minimal", "low", "medium", "high", "xhigh"], reasoningEffort: "medium", inputPrice: 0.1, @@ -760,6 +766,54 @@ export function isOpencodeGoResponsesFormatModel(modelId: string): boolean { return OPENCODE_GO_RESPONSES_FORMAT_MODELS.has(modelId) } +/** + * Reasoning efforts for which a Responses-format model accepts `temperature` + * even though its registry entry declares `supportsTemperature: false`. + * + * Live probes of the Go gateway (Zoo-Code-Org/Zoo-Code#2006) found that the + * GPT Luna models accept every tested temperature (0–2) with reasoning effort + * `"none"`. With any other effort they return HTTP 400 "unsupported + * parameter" for every value except `1`. Temperature `1` is the OpenAI + * reasoning-model default, so accepting it does not show real support: the + * gateway allows only the default value. Temperature is therefore omitted for + * every effort other than `"none"`, including a request that would send `1` + * (Zoo-Code-Org/Zoo-Code#2055). + */ +export const OPENCODE_GO_TEMPERATURE_REASONING_EFFORTS: Readonly> = { + "gpt-5.6-luna": ["none"], + "gpt-6-luna": ["none"], +} + +/** + * Returns `true` when a Responses-format Go model accepts `temperature` for + * the effective reasoning effort that will be sent on the wire. + * + * Support is opt-in (Zoo-Code-Org/Zoo-Code#2055): a Responses model must + * declare `supportsTemperature: true` in the registry, or list the effort in + * {@link OPENCODE_GO_TEMPERATURE_REASONING_EFFORTS}. A model without either + * entry, or an unknown model ID, never receives `temperature`. Timeouts and + * HTTP 5xx results are not evidence for or against support. + * + * @param modelId - The Go model ID. + * @param reasoningEffort - The effective effort sent with the request, or + * `undefined` when the request omits `reasoning.effort`. + */ +export function opencodeGoResponsesSupportsTemperature( + modelId: string, + reasoningEffort: ReasoningEffortExtended | undefined, +): boolean { + if (!isOpencodeGoResponsesFormatModel(modelId)) { + return false + } + + const efforts = OPENCODE_GO_TEMPERATURE_REASONING_EFFORTS[modelId] + if (efforts) { + return reasoningEffort !== undefined && efforts.includes(reasoningEffort) + } + + return getOpencodeGoModelInfo(modelId)?.supportsTemperature === true +} + /** * Returns the native {@link ModelInfo} for a Go-plan model ID, or `undefined` * when the ID is not part of the curated registry. Callers should fall back to diff --git a/src/api/providers/__tests__/opencode-go.spec.ts b/src/api/providers/__tests__/opencode-go.spec.ts index 9c7977c9cd..b7d708ab94 100644 --- a/src/api/providers/__tests__/opencode-go.spec.ts +++ b/src/api/providers/__tests__/opencode-go.spec.ts @@ -43,6 +43,9 @@ vitest.mock("../fetchers/modelCache", () => ({ // Responses-format models (Zoo-Code-Org/Zoo-Code#1431 and issue #1979). "gpt-5.6-luna": { ...opencodeGoModels["gpt-5.6-luna"] }, "gpt-6-luna": { ...opencodeGoModels["gpt-6-luna"] }, + "grok-4.6": { ...opencodeGoModels["grok-4.6"] }, + "muse-spark-1.3-contributor": { ...opencodeGoModels["muse-spark-1.3-contributor"] }, + "muse-spark-1.2-contributor": { ...opencodeGoModels["muse-spark-1.2-contributor"] }, }) }), refreshModels: vitest.fn().mockImplementation(function () { @@ -51,6 +54,9 @@ vitest.mock("../fetchers/modelCache", () => ({ "qwen3.7-max": { ...opencodeGoModels["qwen3.7-max"] }, "gpt-5.6-luna": { ...opencodeGoModels["gpt-5.6-luna"] }, "gpt-6-luna": { ...opencodeGoModels["gpt-6-luna"] }, + "grok-4.6": { ...opencodeGoModels["grok-4.6"] }, + "muse-spark-1.3-contributor": { ...opencodeGoModels["muse-spark-1.3-contributor"] }, + "muse-spark-1.2-contributor": { ...opencodeGoModels["muse-spark-1.2-contributor"] }, }) }), getModelsFromCache: vitest.fn().mockReturnValue(undefined), @@ -1115,7 +1121,7 @@ describe("OpencodeGoHandler", () => { expect(callArgs.store).toBe(false) const input = callArgs.input as unknown[] expect(input.some((item) => (item as { role?: string }).role === "system")).toBe(false) - // The gateway rejects temperature for Responses-format models. + // No custom temperature is set, so the provider default is not sent. expect(callArgs.temperature).toBeUndefined() // No tools were provided, so no tools/tool_choice are sent. expect(callArgs.tools).toBeUndefined() @@ -1500,6 +1506,141 @@ describe("OpencodeGoHandler", () => { }).rejects.toThrow("Opencode Go completion error: internal server error") }) + describe("temperature (Zoo-Code-Org/Zoo-Code#2055)", () => { + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }] + + const streamRequest = async (options: ApiHandlerOptions) => { + await collectStream(new OpencodeGoHandler(options).createMessage("sys", messages)) + return mockResponsesCreate.mock.calls[0][0] as Record + } + + const completeRequest = async (options: ApiHandlerOptions) => { + mockResponsesCreate.mockResolvedValue({ output_text: "Hello!" }) + await new OpencodeGoHandler(options).completePrompt("ping") + return mockResponsesCreate.mock.calls[0][0] as Record + } + + const paths = [ + ["streaming", streamRequest], + ["non-streaming", completeRequest], + ] as const + + describe.each(paths)("%s request", (_path, sendRequest) => { + it.each([ + ["grok-4.6", undefined], + ["muse-spark-1.3-contributor", undefined], + ["muse-spark-1.2-contributor", undefined], + ["gpt-5.6-luna", "none"], + ["gpt-6-luna", "none"], + ] as const)("forwards a custom temperature for %s (effort %s)", async (modelId, reasoningEffort) => { + const callArgs = await sendRequest({ + opencodeGoApiKey: "test-key", + opencodeGoModelId: modelId, + modelTemperature: 0.37, + ...(reasoningEffort && { reasoningEffort }), + }) + + // The value keeps the UI's 0.01 resolution. + expect(callArgs.temperature).toBe(0.37) + }) + + it.each([ + ["grok-4.6", "low"], + ["grok-4.6", "high"], + ["muse-spark-1.3-contributor", "minimal"], + ["muse-spark-1.2-contributor", "high"], + ] as const)( + "forwards a custom temperature for %s with explicit effort %s", + async (modelId, reasoningEffort) => { + const callArgs = await sendRequest({ + opencodeGoApiKey: "test-key", + opencodeGoModelId: modelId, + modelTemperature: 0.37, + reasoningEffort, + }) + + // Grok and Muse support does not depend on reasoning effort. + expect(callArgs.reasoning).toEqual({ effort: reasoningEffort }) + expect(callArgs.temperature).toBe(0.37) + }, + ) + + it("omits temperature for a stale none effort when reasoning effort is switched off", async () => { + // A saved "none" does not reach the wire while the toggle is off, + // so the configuration is unverified and temperature is omitted. + const callArgs = await sendRequest({ + ...lunaOptions, + modelTemperature: 0.5, + reasoningEffort: "none", + enableReasoningEffort: false, + }) + + expect(callArgs).not.toHaveProperty("reasoning") + expect(callArgs).not.toHaveProperty("temperature") + }) + + it.each([ + ["gpt-5.6-luna", "low"], + ["gpt-5.6-luna", "max"], + ["gpt-6-luna", "medium"], + ["gpt-6-luna", "xhigh"], + ["gpt-6-luna", "disable"], + ] as const)("omits a saved temperature for %s with effort %s", async (modelId, reasoningEffort) => { + const callArgs = await sendRequest({ + opencodeGoApiKey: "test-key", + opencodeGoModelId: modelId, + modelTemperature: 0.5, + reasoningEffort, + }) + + expect(callArgs).not.toHaveProperty("temperature") + }) + + it("omits temperature 1 for a GPT Luna model with a non-none effort", async () => { + // Temperature 1 is the only value the gateway accepted with + // reasoning, but it is the model default, so it is omitted. + const callArgs = await sendRequest({ + opencodeGoApiKey: "test-key", + opencodeGoModelId: "gpt-5.6-luna", + modelTemperature: 1, + reasoningEffort: "high", + }) + + expect(callArgs).not.toHaveProperty("temperature") + }) + + it("omits a saved temperature for a GPT Luna model on its default effort", async () => { + const callArgs = await sendRequest({ ...gpt6LunaOptions, modelTemperature: 0.5 }) + + expect(callArgs.reasoning).toEqual({ effort: "medium" }) + expect(callArgs).not.toHaveProperty("temperature") + }) + + it.each([undefined, null])( + "omits temperature when no custom value is set (%s)", + async (modelTemperature) => { + const callArgs = await sendRequest({ + opencodeGoApiKey: "test-key", + opencodeGoModelId: "grok-4.6", + modelTemperature, + }) + + expect(callArgs).not.toHaveProperty("temperature") + }, + ) + + it("forwards temperature 0 as an explicit custom value", async () => { + const callArgs = await sendRequest({ + opencodeGoApiKey: "test-key", + opencodeGoModelId: "grok-4.6", + modelTemperature: 0, + }) + + expect(callArgs.temperature).toBe(0) + }) + }) + }) + it("classifies documented Responses models as Responses-format and other models as not", () => { expect(isOpencodeGoResponsesFormatModel("gpt-5.6-luna")).toBe(true) expect(isOpencodeGoResponsesFormatModel("gpt-6-luna")).toBe(true) diff --git a/src/api/providers/fetchers/__tests__/opencode-go.spec.ts b/src/api/providers/fetchers/__tests__/opencode-go.spec.ts index e51a21f115..62f17ab900 100644 --- a/src/api/providers/fetchers/__tests__/opencode-go.spec.ts +++ b/src/api/providers/fetchers/__tests__/opencode-go.spec.ts @@ -75,6 +75,26 @@ describe("Opencode Go Fetchers", () => { }) }) + it("keeps curated Responses temperature support after merging live metadata", async () => { + mockedAxios.get.mockResolvedValue({ + data: { + data: [ + { id: "grok-4.6", context_window: 400_000 }, + { id: "muse-spark-1.2-contributor", max_tokens: 65_536 }, + { id: "gpt-5.6-luna", context_window: 900_000 }, + ], + }, + }) + + const models = await getOpencodeGoModels("k") + + // Live limits never override the curated temperature capability + // (Zoo-Code-Org/Zoo-Code#2055). + expect(models["grok-4.6"]).toMatchObject({ contextWindow: 400_000, supportsTemperature: true }) + expect(models["muse-spark-1.2-contributor"]).toMatchObject({ maxTokens: 65_536, supportsTemperature: true }) + expect(models["gpt-5.6-luna"]).toMatchObject({ contextWindow: 900_000, supportsTemperature: false }) + }) + it("uses curated gpt-6-luna metadata when the live model omits metadata", async () => { mockedAxios.get.mockResolvedValue({ data: { data: [{ id: "gpt-6-luna" }] } }) @@ -86,9 +106,9 @@ describe("Opencode Go Fetchers", () => { supportsImages: true, supportsPromptCache: true, supportsMaxTokens: true, - // The Opencode Go Responses gateway rejects `temperature` for - // `gpt-6-luna`, so the curated registry must opt out so the UI - // hides the dead control rather than rendering it. + // The Go gateway accepts `temperature` for `gpt-6-luna` only with + // reasoning effort "none", so the static baseline opts out and the + // effort-aware rule opts back in (Zoo-Code-Org/Zoo-Code#2055). supportsTemperature: false, supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh", "max"], reasoningEffort: "medium", diff --git a/src/api/providers/opencode-go.ts b/src/api/providers/opencode-go.ts index db31cf1c92..c6b995f161 100644 --- a/src/api/providers/opencode-go.ts +++ b/src/api/providers/opencode-go.ts @@ -12,6 +12,7 @@ import { OPENCODE_GO_DEFAULT_TEMPERATURE, isOpencodeGoAnthropicFormatModel, isOpencodeGoResponsesFormatModel, + opencodeGoResponsesSupportsTemperature, providerIdentifiers, } from "@roo-code/types" @@ -79,9 +80,9 @@ type OpencodeGoFormat = "anthropic" | "openai" | "responses" * - Anthropic Messages (`/v1/messages`) — used by Qwen (qwen3.8-max, * qwen3.7-max, qwen3.7-plus, qwen3.6-plus) and MiniMax (minimax-m3, * minimax-m2.7, minimax-m2.5) models. - * - OpenAI Responses (`/v1/responses`) — used by `gpt-5.6-luna` and - * `gpt-6-luna`, whose chat-completions adapters are not supported by the - * Go gateway. + * - OpenAI Responses (`/v1/responses`) — used by the models in + * `OPENCODE_GO_RESPONSES_FORMAT_MODELS` (GPT Luna, Grok, and Muse Spark), + * whose chat-completions adapters are not supported by the Go gateway. * * Sending an Anthropic-format model to the chat completions endpoint is * rejected with `401 Model is not supported for format oa-compat`, so this @@ -213,9 +214,9 @@ export class OpencodeGoHandler extends RouterProvider implements SingleCompletio * * Anthropic-format models (Qwen/MiniMax) are streamed via * {@link streamAnthropicMessage} against `/v1/messages`; Responses-format - * models (`gpt-5.6-luna` and `gpt-6-luna`) are streamed via {@link streamResponsesMessage} - * against `/v1/responses`; all other models use the OpenAI-compatible chat - * completions endpoint. + * models ({@link isOpencodeGoResponsesFormatModel}) are streamed via + * {@link streamResponsesMessage} against `/v1/responses`; all other models + * use the OpenAI-compatible chat completions endpoint. * * For OpenAI-format models that require reasoning_content to be passed back * during multi-turn tool calls (`preserveReasoning`), messages are @@ -239,7 +240,6 @@ export class OpencodeGoHandler extends RouterProvider implements SingleCompletio yield* this.streamResponsesMessage( modelId, info, - temperature, maxTokens, reasoningEffort, systemPrompt, @@ -318,9 +318,31 @@ export class OpencodeGoHandler extends RouterProvider implements SingleCompletio } } + /** + * Returns the `temperature` field for a Responses request, or an empty + * object to omit it. + * + * Temperature is sent only when the user has set a custom value and the + * model accepts it for the effective reasoning effort + * ({@link opencodeGoResponsesSupportsTemperature}). The provider default is + * never sent, which keeps the gateway's own default when no custom value + * is set. A saved value for an unsupported configuration is kept in + * settings but not sent (Zoo-Code-Org/Zoo-Code#2055). + */ + private getResponsesTemperature( + modelId: string, + reasoningEffort: ReasoningEffortExtended | undefined, + ): { temperature?: number } { + const temperature = this.options.modelTemperature + return typeof temperature === "number" && opencodeGoResponsesSupportsTemperature(modelId, reasoningEffort) + ? { temperature } + : {} + } + /** * Streams an OpenAI Responses-format completion for Go models that only - * accept the `/v1/responses` endpoint (`gpt-5.6-luna` and `gpt-6-luna`). + * accept the `/v1/responses` endpoint + * ({@link isOpencodeGoResponsesFormatModel}). * * Follows the focused xAI handler pattern: the conversation is converted * with the shared {@link convertToResponsesApiInput} transform, the system @@ -338,7 +360,6 @@ export class OpencodeGoHandler extends RouterProvider implements SingleCompletio private async *streamResponsesMessage( modelId: string, info: ModelInfo, - _temperature: number | undefined, maxTokens: number | undefined, reasoningEffort: ReasoningEffortExtended | undefined, systemPrompt: string, @@ -381,8 +402,7 @@ export class OpencodeGoHandler extends RouterProvider implements SingleCompletio } as OpenAI.Responses.ResponseCreateParamsStreaming["reasoning"], } : {}), - // OpenCode Go Responses models currently reject temperature, even when - // the model metadata does not explicitly opt out of it. + ...this.getResponsesTemperature(modelId, reasoningEffort), // Honour the same includeMaxTokens/modelMaxTokens override logic as // the chat-completions and Anthropic streaming paths. ...(maxTokens !== undefined @@ -760,7 +780,7 @@ export class OpencodeGoHandler extends RouterProvider implements SingleCompletio model: modelId, input: [{ role: "user", content: [{ type: "input_text", text: prompt }] }], store: false, - // OpenCode Go Responses models currently reject temperature. + ...this.getResponsesTemperature(modelId, reasoningEffort), // Honour the same includeMaxTokens/modelMaxTokens override // logic as the chat-completions path. ...(maxTokens !== undefined diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index e531515abe..ab7457915e 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -29,6 +29,7 @@ import { handleModelChangeSideEffects, toBedrockModelId, } from "./utils/providerModelConfig" +import { isTemperatureControlSupported } from "./utils/temperatureSupport" import { vscode } from "@src/utils/vscode" import { validateApiConfigurationExcludingModelErrors, getModelValidationError } from "@src/utils/validate" @@ -836,7 +837,12 @@ const ApiOptions = ({ todoListEnabled={apiConfiguration.todoListEnabled} onChange={(field, value) => setApiConfigurationField(field, value)} /> - {selectedModelInfo?.supportsTemperature !== false && ( + {isTemperatureControlSupported({ + provider: activeSelectedProvider, + modelId: selectedModelId, + modelInfo: selectedModelInfo, + apiConfiguration, + }) && ( { expect(screen.queryByTestId("rate-limit-seconds-control")).not.toBeInTheDocument() }) + describe("OpenCode Go Responses temperature (Zoo-Code-Org/Zoo-Code#2055)", () => { + const selectOpencodeGoModel = (modelId: string) => { + vi.mocked(useSelectedModel).mockImplementation(() => ({ + provider: providerIdentifiers.opencodeGo, + id: modelId, + info: opencodeGoModels[modelId], + isLoading: false, + isError: false, + })) + } + + const opencodeGoConfig = (modelId: string, settings: ProviderSettings = {}): ProviderSettings => ({ + apiProvider: providerIdentifiers.opencodeGo, + opencodeGoModelId: modelId, + modelTemperature: 0.42, + ...settings, + }) + + afterEach(() => { + vi.mocked(useSelectedModel).mockReset() + }) + + it("shows the control for a model that always supports temperature", () => { + selectOpencodeGoModel("grok-4.6") + renderApiOptions({ apiConfiguration: opencodeGoConfig("grok-4.6") }) + + expect(screen.getByTestId("temperature-control")).toBeInTheDocument() + }) + + it("hides the control for a GPT Luna model on its default reasoning effort", () => { + selectOpencodeGoModel("gpt-6-luna") + renderApiOptions({ apiConfiguration: opencodeGoConfig("gpt-6-luna") }) + + expect(screen.queryByTestId("temperature-control")).not.toBeInTheDocument() + }) + + it("updates visibility when the reasoning effort changes without clearing the saved value", () => { + const setApiConfigurationField = vi.fn() + selectOpencodeGoModel("gpt-5.6-luna") + const renderWithEffort = (reasoningEffort: ProviderSettings["reasoningEffort"]) => ( + {}} + uriScheme={undefined} + apiConfiguration={opencodeGoConfig("gpt-5.6-luna", { reasoningEffort })} + setApiConfigurationField={setApiConfigurationField} + /> + ) + const view = renderWithExtensionState(renderWithEffort("none")) + expect(screen.getByTestId("temperature-control")).toBeInTheDocument() + + view.rerender(renderWithEffort("high")) + expect(screen.queryByTestId("temperature-control")).not.toBeInTheDocument() + + // Returning to "none" shows the control again with the saved value. + view.rerender(renderWithEffort("none")) + expect(within(screen.getByTestId("temperature-control")).getByRole("slider")).toHaveValue("0.42") + + const temperatureWrites = setApiConfigurationField.mock.calls.filter( + ([field]) => field === "modelTemperature", + ) + expect(temperatureWrites).toEqual([]) + }) + }) + describe("thinking functionality", () => { it("should show ThinkingBudget for Anthropic models that support thinking", () => { renderApiOptions({ diff --git a/webview-ui/src/components/settings/utils/__tests__/temperatureSupport.spec.ts b/webview-ui/src/components/settings/utils/__tests__/temperatureSupport.spec.ts new file mode 100644 index 0000000000..a5afe52083 --- /dev/null +++ b/webview-ui/src/components/settings/utils/__tests__/temperatureSupport.spec.ts @@ -0,0 +1,81 @@ +import { type ProviderSettings, opencodeGoModels, providerIdentifiers } from "@roo-code/types" + +import { isTemperatureControlSupported } from "../temperatureSupport" + +describe("isTemperatureControlSupported", () => { + const opencodeGo = (modelId: string, apiConfiguration: ProviderSettings = {}) => + isTemperatureControlSupported({ + provider: providerIdentifiers.opencodeGo, + modelId, + modelInfo: opencodeGoModels[modelId], + apiConfiguration: { + apiProvider: providerIdentifiers.opencodeGo, + opencodeGoModelId: modelId, + ...apiConfiguration, + }, + }) + + describe("OpenCode Go Responses models (Zoo-Code-Org/Zoo-Code#2055)", () => { + it.each(["grok-4.6", "muse-spark-1.3-contributor", "muse-spark-1.2-contributor"])( + "shows the control for %s", + (modelId) => { + expect(opencodeGo(modelId)).toBe(true) + expect(opencodeGo(modelId, { reasoningEffort: "low" })).toBe(true) + }, + ) + + it.each(["gpt-5.6-luna", "gpt-6-luna"])("shows the control for %s only with effort none", (modelId) => { + expect(opencodeGo(modelId, { reasoningEffort: "none" })).toBe(true) + expect(opencodeGo(modelId, { reasoningEffort: "high" })).toBe(false) + expect(opencodeGo(modelId, { reasoningEffort: "disable" })).toBe(false) + expect(opencodeGo(modelId, { enableReasoningEffort: false })).toBe(false) + // A stale "none" is not sent while reasoning effort is switched off. + expect(opencodeGo(modelId, { reasoningEffort: "none", enableReasoningEffort: false })).toBe(false) + // The model default effort ("medium") does not support temperature. + expect(opencodeGo(modelId)).toBe(false) + }) + + it("hides the control when the model info is not available yet", () => { + expect( + isTemperatureControlSupported({ + provider: providerIdentifiers.opencodeGo, + modelId: "gpt-6-luna", + modelInfo: undefined, + apiConfiguration: { reasoningEffort: "none" }, + }), + ).toBe(false) + }) + }) + + it("keeps the supportsTemperature opt-out for OpenCode Go chat-completions models", () => { + expect(opencodeGo("kimi-k2.6")).toBe(true) + expect( + isTemperatureControlSupported({ + provider: providerIdentifiers.opencodeGo, + modelId: "kimi-k2.6", + modelInfo: { ...opencodeGoModels["kimi-k2.6"], supportsTemperature: false }, + apiConfiguration: {}, + }), + ).toBe(false) + }) + + it("keeps the supportsTemperature opt-out for other providers", () => { + const base = { provider: providerIdentifiers.anthropic, modelId: "m", apiConfiguration: {} } + + expect(isTemperatureControlSupported({ ...base, modelInfo: undefined })).toBe(true) + expect( + isTemperatureControlSupported({ + ...base, + modelInfo: { contextWindow: 1, supportsPromptCache: false, supportsTemperature: false }, + }), + ).toBe(false) + // A Responses model ID on another provider does not use the Go rule. + expect( + isTemperatureControlSupported({ + ...base, + modelId: "gpt-6-luna", + modelInfo: { contextWindow: 1, supportsPromptCache: false }, + }), + ).toBe(true) + }) +}) diff --git a/webview-ui/src/components/settings/utils/temperatureSupport.ts b/webview-ui/src/components/settings/utils/temperatureSupport.ts new file mode 100644 index 0000000000..bc4b23ccd1 --- /dev/null +++ b/webview-ui/src/components/settings/utils/temperatureSupport.ts @@ -0,0 +1,54 @@ +import { + type ModelInfo, + type ProviderName, + type ProviderSettings, + type ReasoningEffortExtended, + isOpencodeGoResponsesFormatModel, + opencodeGoResponsesSupportsTemperature, + providerIdentifiers, +} from "@roo-code/types" + +import { shouldUseReasoningEffort } from "@roo/api" + +/** + * Returns the reasoning effort the request sends, matching `getModelParams`: + * the user's selection wins over the model default, and `"disable"` omits it. + */ +const getEffectiveReasoningEffort = ( + model: ModelInfo, + settings: ProviderSettings, +): ReasoningEffortExtended | undefined => { + if (!shouldUseReasoningEffort({ model, settings })) return undefined + + const effort = settings.reasoningEffort ?? model.reasoningEffort + return effort && effort !== "disable" ? effort : undefined +} + +/** + * Returns `true` when the settings UI should show the temperature control for + * the selected model and configuration. + * + * OpenCode Go Responses models use the same opt-in rule as the request + * builder, so the control is visible only when the request will send the + * value (Zoo-Code-Org/Zoo-Code#2055). The rule depends on the effective + * reasoning effort, so the result changes when the effort changes. Hiding + * the control does not clear a saved temperature. + */ +export const isTemperatureControlSupported = ({ + provider, + modelId, + modelInfo, + apiConfiguration, +}: { + provider: ProviderName | undefined + modelId: string | undefined + modelInfo: ModelInfo | undefined + apiConfiguration: ProviderSettings +}): boolean => { + if (provider === providerIdentifiers.opencodeGo && modelId && isOpencodeGoResponsesFormatModel(modelId)) { + const reasoningEffort = modelInfo ? getEffectiveReasoningEffort(modelInfo, apiConfiguration) : undefined + return opencodeGoResponsesSupportsTemperature(modelId, reasoningEffort) + } + + return modelInfo?.supportsTemperature !== false +} From 5689144a1edd5c565db483bbd5a2d3dcbe870dcc Mon Sep 17 00:00:00 2001 From: Anthony Gallon <64985097+AntzCode@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:21:56 +1300 Subject: [PATCH 2/2] test(webview): add visual snapshot for OpenCode Go temperature visibility (#2055) --- webview-ui/playwright/gallery/stories.tsx | 5 ++ .../OpencodeGoTemperature.visual.fixture.tsx | 62 ++++++++++++++++++ .../OpencodeGoTemperature.visual.tsx | 9 +++ .../opencode-go-temperature.png | Bin 0 -> 25005 bytes 4 files changed, 76 insertions(+) create mode 100644 webview-ui/src/components/settings/__tests__/OpencodeGoTemperature.visual.fixture.tsx create mode 100644 webview-ui/src/components/settings/__tests__/OpencodeGoTemperature.visual.tsx create mode 100644 webview-ui/src/components/settings/__tests__/__screenshots__/opencode-go-temperature.png diff --git a/webview-ui/playwright/gallery/stories.tsx b/webview-ui/playwright/gallery/stories.tsx index 08d680678a..beb9ffeb67 100644 --- a/webview-ui/playwright/gallery/stories.tsx +++ b/webview-ui/playwright/gallery/stories.tsx @@ -275,6 +275,11 @@ export const stories: Record = { await import("@/components/settings/providers/__tests__/OpenAICompatible.visual.fixture") return }, + "opencode-go-temperature": async () => { + const { OpencodeGoTemperatureFixture } = + await import("@/components/settings/__tests__/OpencodeGoTemperature.visual.fixture") + return + }, "rendered-content-contrast": async () => { const [{ AppProviders }, { RenderedContentContrastFixture }] = await Promise.all([ import("../AppProviders"), diff --git a/webview-ui/src/components/settings/__tests__/OpencodeGoTemperature.visual.fixture.tsx b/webview-ui/src/components/settings/__tests__/OpencodeGoTemperature.visual.fixture.tsx new file mode 100644 index 0000000000..a10a7190fc --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/OpencodeGoTemperature.visual.fixture.tsx @@ -0,0 +1,62 @@ +import React from "react" + +import { type ProviderSettings, opencodeGoModels, providerIdentifiers } from "@roo-code/types" + +import { TemperatureControl } from "../TemperatureControl" +import { isTemperatureControlSupported } from "../utils/temperatureSupport" +import { AppProviders } from "../../../../playwright/AppProviders" + +const SAVED_TEMPERATURE = 0.42 + +const cases: { modelId: string; reasoningEffort: ProviderSettings["reasoningEffort"] }[] = [ + { modelId: "gpt-6-luna", reasoningEffort: "none" }, + { modelId: "gpt-6-luna", reasoningEffort: "high" }, + { modelId: "grok-4.6", reasoningEffort: "high" }, +] + +/** + * Renders the temperature control through the same gate ApiOptions uses, for + * supported and unsupported OpenCode Go Responses states (#2055). Each case has + * a saved custom temperature, so a hidden control shows the value is kept but + * not offered. + */ +export function OpencodeGoTemperatureFixture() { + return ( + +
+ {cases.map(({ modelId, reasoningEffort }) => { + const apiConfiguration: ProviderSettings = { + apiProvider: providerIdentifiers.opencodeGo, + opencodeGoModelId: modelId, + reasoningEffort, + modelTemperature: SAVED_TEMPERATURE, + } + const supported = isTemperatureControlSupported({ + provider: providerIdentifiers.opencodeGo, + modelId, + modelInfo: opencodeGoModels[modelId], + apiConfiguration, + }) + return ( +
+

+ {modelId} · reasoning effort {reasoningEffort} +

+ {supported ? ( + undefined} + maxValue={2} + /> + ) : ( +

Temperature control hidden

+ )} +
+ ) + })} +
+
+ ) +} diff --git a/webview-ui/src/components/settings/__tests__/OpencodeGoTemperature.visual.tsx b/webview-ui/src/components/settings/__tests__/OpencodeGoTemperature.visual.tsx new file mode 100644 index 0000000000..b45d3d12a5 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/OpencodeGoTemperature.visual.tsx @@ -0,0 +1,9 @@ +import { expect, test } from "../../../../playwright/coverage-fixture" +import { mountedStory } from "../../../../playwright/mounted-story" + +test("shows the temperature control only for supported OpenCode Go Responses states", async ({ mount }) => { + const component = mountedStory(await mount("opencode-go-temperature")) + const story = component.getByTestId("opencode-go-temperature-story") + + await expect(story).toHaveScreenshot("opencode-go-temperature.png") +}) diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/opencode-go-temperature.png b/webview-ui/src/components/settings/__tests__/__screenshots__/opencode-go-temperature.png new file mode 100644 index 0000000000000000000000000000000000000000..e1828c9b6b5fb53abb5eed2e07eff05c3a8e2339 GIT binary patch literal 25005 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 literal 0 HcmV?d00001