From 58b7aff7bd79e9e855d4aa8c989e3a677468e977 Mon Sep 17 00:00:00 2001 From: HyunJun CHOI Date: Thu, 12 Feb 2026 18:24:28 +0900 Subject: [PATCH] fix: detect GPT models behind proxy providers (litellm, ollama) in isGptModel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isGptModel only matched openai/ and github-copilot/gpt- prefixes, causing models like litellm/gpt-5.2 to fall into the Claude code path. This injected Claude-specific thinking config, which the opencode runtime translated into a reasoningSummary API parameter — rejected by OpenAI. Extract model name after provider prefix and match against GPT model name patterns (gpt-*, o1, o3, o4). Closes #1788 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- src/agents/types.test.ts | 49 +++++++++++++++++++ src/agents/types.ts | 12 ++++- .../ultrawork/source-detector.ts | 16 ++---- 3 files changed, 65 insertions(+), 12 deletions(-) create mode 100644 src/agents/types.test.ts diff --git a/src/agents/types.test.ts b/src/agents/types.test.ts new file mode 100644 index 000000000..186eddd17 --- /dev/null +++ b/src/agents/types.test.ts @@ -0,0 +1,49 @@ +import { describe, test, expect } from "bun:test"; +import { isGptModel } from "./types"; + +describe("isGptModel", () => { + test("standard openai provider models", () => { + expect(isGptModel("openai/gpt-5.2")).toBe(true); + expect(isGptModel("openai/gpt-4o")).toBe(true); + expect(isGptModel("openai/o1")).toBe(true); + expect(isGptModel("openai/o3-mini")).toBe(true); + }); + + test("github copilot gpt models", () => { + expect(isGptModel("github-copilot/gpt-5.2")).toBe(true); + expect(isGptModel("github-copilot/gpt-4o")).toBe(true); + }); + + test("litellm proxied gpt models", () => { + expect(isGptModel("litellm/gpt-5.2")).toBe(true); + expect(isGptModel("litellm/gpt-4o")).toBe(true); + expect(isGptModel("litellm/o1")).toBe(true); + expect(isGptModel("litellm/o3-mini")).toBe(true); + expect(isGptModel("litellm/o4-mini")).toBe(true); + }); + + test("other proxied gpt models", () => { + expect(isGptModel("ollama/gpt-4o")).toBe(true); + expect(isGptModel("custom-provider/gpt-5.2")).toBe(true); + }); + + test("gpt4 prefix without hyphen (legacy naming)", () => { + expect(isGptModel("litellm/gpt4o")).toBe(true); + expect(isGptModel("ollama/gpt4")).toBe(true); + }); + + test("claude models are not gpt", () => { + expect(isGptModel("anthropic/claude-opus-4-6")).toBe(false); + expect(isGptModel("anthropic/claude-sonnet-4-5")).toBe(false); + expect(isGptModel("litellm/anthropic.claude-opus-4-5")).toBe(false); + }); + + test("gemini models are not gpt", () => { + expect(isGptModel("google/gemini-3-pro")).toBe(false); + expect(isGptModel("litellm/gemini-3-pro")).toBe(false); + }); + + test("opencode provider is not gpt", () => { + expect(isGptModel("opencode/claude-opus-4-6")).toBe(false); + }); +}); diff --git a/src/agents/types.ts b/src/agents/types.ts index 14da69a18..92834883f 100644 --- a/src/agents/types.ts +++ b/src/agents/types.ts @@ -66,8 +66,18 @@ export interface AgentPromptMetadata { keyTrigger?: string } +function extractModelName(model: string): string { + return model.includes("/") ? model.split("/").pop() ?? model : model +} + +const GPT_MODEL_PREFIXES = ["gpt-", "gpt4", "o1", "o3", "o4"] + export function isGptModel(model: string): boolean { - return model.startsWith("openai/") || model.startsWith("github-copilot/gpt-") + if (model.startsWith("openai/") || model.startsWith("github-copilot/gpt-")) + return true + + const modelName = extractModelName(model).toLowerCase() + return GPT_MODEL_PREFIXES.some((prefix) => modelName.startsWith(prefix)) } export type BuiltinAgentName = diff --git a/src/hooks/keyword-detector/ultrawork/source-detector.ts b/src/hooks/keyword-detector/ultrawork/source-detector.ts index 2f0a897e2..d49b86857 100644 --- a/src/hooks/keyword-detector/ultrawork/source-detector.ts +++ b/src/hooks/keyword-detector/ultrawork/source-detector.ts @@ -7,6 +7,8 @@ * 3. Everything else (Claude, etc.) → default.ts */ +import { isGptModel } from "../../../agents/types" + /** * Checks if agent is a planner-type agent. * Planners don't need ultrawork injection (they ARE the planner). @@ -20,15 +22,7 @@ export function isPlannerAgent(agentName?: string): boolean { return /\bplan\b/.test(normalized) } -/** - * Checks if model is GPT 5.2 series. - * GPT models benefit from specific prompting patterns. - */ -export function isGptModel(modelID?: string): boolean { - if (!modelID) return false - const lowerModel = modelID.toLowerCase() - return lowerModel.includes("gpt") -} +export { isGptModel } /** Ultrawork message source type */ export type UltraworkSource = "planner" | "gpt" | "default" @@ -45,8 +39,8 @@ export function getUltraworkSource( return "planner" } - // Priority 2: GPT 5.2 models - if (isGptModel(modelID)) { + // Priority 2: GPT models + if (modelID && isGptModel(modelID)) { return "gpt" }