From 5ae8a3c1d3d376d9cded7ef938c1bcd8309ad190 Mon Sep 17 00:00:00 2001 From: Vladimir Glafirov Date: Tue, 6 Oct 2026 02:12:37 +0000 Subject: [PATCH] fix(core): resolve reasoning variants for GitLab Duo models (#51082) --- packages/core/src/variant.ts | 38 ++++++++++++++++++++++++++ packages/core/test/variant.test.ts | 44 ++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/packages/core/src/variant.ts b/packages/core/src/variant.ts index 3a515101541..4f3cb4e3cef 100644 --- a/packages/core/src/variant.ts +++ b/packages/core/src/variant.ts @@ -526,6 +526,43 @@ const sapAICore: Protocol = (model, support) => { } } +// gitlab-ai-provider reads `providerOptions.gitlab.thinking` (Anthropic) and +// `providerOptions.gitlab.reasoningEffort` (OpenAI Chat and Responses). Its adaptive +// thinking config carries the effort inline and always requests summarized display. +const gitlabAnthropic: Protocol = (model, support) => { + const info = claudeInfo(model) + const disabled: Overlay = { settings: { thinking: { type: "disabled" } } } + const adaptive = (effort: string): Overlay => ({ settings: { thinking: { type: "adaptive", effort } } }) + switch (support.type) { + case "effort": { + if (info.manual) return gitlabAnthropic(model, { type: "budget_tokens", min: 1024 }) + const defaults = info.major === 4 && info.minor === 6 ? [...EFFORTS, "max"] : [...EFFORTS, "xhigh", "max"] + return efforts(support.values ?? defaults, (effort) => (effort === "none" ? disabled : adaptive(effort))) + } + case "toggle": { + if (info.always) return [] + const thinking = info.manual ? manualThinking(model) : adaptive("high") + if (!thinking) return [] + return toggle(disabled, thinking) + } + case "budget_tokens": + return budgets( + model, + support, + (tokens) => ({ settings: { thinking: { type: "enabled", budgetTokens: tokens } } }), + ANTHROPIC_OUTPUT_TOKEN_MAX, + ) + } +} + +const gitlab: Protocol = (model, support) => { + const id = modelID(model).toLowerCase() + if (id.startsWith("duo-workflow")) return [] + if (claudeInfo(model).family) return gitlabAnthropic(model, support) + if (id.includes("gpt") || id.includes("codex")) return openaiChat(model, support) + return [] +} + const PROTOCOLS: Readonly> = { "@opencode/ai/providers/openai": openaiResponses, "@opencode/ai/providers/azure/responses": openaiResponses, @@ -580,4 +617,5 @@ const PROTOCOLS: Readonly> = { "@opencode/ai/providers/vercel-ai-gateway": vercelGateway, [Provider.aisdk("@ai-sdk/gateway")]: vercelGateway, [Provider.aisdk("@jerome-benoit/sap-ai-provider-v2")]: sapAICore, + [Provider.aisdk("gitlab-ai-provider")]: gitlab, } diff --git a/packages/core/test/variant.test.ts b/packages/core/test/variant.test.ts index 9d2375ab4c0..8f2367bc7ac 100644 --- a/packages/core/test/variant.test.ts +++ b/packages/core/test/variant.test.ts @@ -452,3 +452,47 @@ test("spells Chat Completions variants for hosting providers", () => { { id: "xhigh", settings: { reasoningEffort: "xhigh" } }, ]) }) + +test("spells GitLab Duo variants for gitlab-ai-provider routes", () => { + const pkg = Provider.aisdk("gitlab-ai-provider") + const gitlab = (id: string, output?: number) => model(pkg, id, output, "gitlab") + + expect( + resolve(gitlab("duo-chat-opus-5-5"), [{ type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }]), + ).toEqual( + ["low", "medium", "high", "xhigh", "max"].map((effort) => ({ + id: effort, + settings: { thinking: { type: "adaptive", effort } }, + })), + ) + + expect( + resolve(gitlab("duo-chat-sonnet-5"), [{ type: "toggle" }, { type: "effort", values: ["low", "max"] }]), + ).toEqual([ + { id: "none", settings: { thinking: { type: "disabled" } } }, + { id: "low", settings: { thinking: { type: "adaptive", effort: "low" } } }, + { id: "max", settings: { thinking: { type: "adaptive", effort: "max" } } }, + ]) + + expect(resolve(gitlab("duo-chat-fable-5-1"), [{ type: "toggle" }, { type: "effort", values: ["high"] }])).toEqual([ + { id: "high", settings: { thinking: { type: "adaptive", effort: "high" } } }, + ]) + + expect(resolve(gitlab("duo-chat-opus-4-5", 64_000), [{ type: "effort" }])).toEqual([ + { id: "high", settings: { thinking: { type: "enabled", budgetTokens: 16_000 } } }, + { id: "max", settings: { thinking: { type: "enabled", budgetTokens: 31_999 } } }, + ]) + + expect(resolve(gitlab("duo-chat-gpt-6-sol"), [{ type: "effort", values: ["none", "low", "max"] }])).toEqual([ + { id: "none", settings: { reasoningEffort: "none" } }, + { id: "low", settings: { reasoningEffort: "low" } }, + { id: "max", settings: { reasoningEffort: "max" } }, + ]) + + expect(resolve(gitlab("duo-chat-gpt-5-3-codex"), [{ type: "effort", values: ["low"] }])).toEqual([ + { id: "low", settings: { reasoningEffort: "low" } }, + ]) + + expect(resolve(gitlab("duo-chat-gpt-5-4"), [])).toEqual([]) + expect(resolve(gitlab("duo-workflow-opus-5-5"), [{ type: "effort" }])).toEqual([]) +})