diff --git a/src/agents/hephaestus/agent.test.ts b/src/agents/hephaestus/agent.test.ts index 5721f006a..0ba99ce9d 100644 --- a/src/agents/hephaestus/agent.test.ts +++ b/src/agents/hephaestus/agent.test.ts @@ -23,6 +23,23 @@ describe("getHephaestusPromptSource", () => { expect(source3).toBe("gpt-5-4"); }); + test("returns 'gpt-5-4' for gpt-5.5 models", () => { + // given + const model1 = "openai/gpt-5.5"; + const model2 = "openai/gpt-5-5"; + const model3 = "github-copilot/gpt-5.5"; + + // when + const source1 = getHephaestusPromptSource(model1); + const source2 = getHephaestusPromptSource(model2); + const source3 = getHephaestusPromptSource(model3); + + // then + expect(source1).toBe("gpt-5-4"); + expect(source2).toBe("gpt-5-4"); + expect(source3).toBe("gpt-5-4"); + }); + test("returns 'gpt-5-3-codex' for GPT 5.3 Codex models", () => { // given const model1 = "openai/gpt-5.3-codex"; @@ -96,6 +113,19 @@ describe("getHephaestusPrompt", () => { expect(prompt).toContain(""); }); + test("GPT 5.5 model returns GPT-5.4 optimized prompt", () => { + // given + const model = "openai/gpt-5.5"; + + // when + const prompt = getHephaestusPrompt(model); + + // then + expect(prompt).toContain("You build context by examining"); + expect(prompt).toContain("Never chain together bash commands"); + expect(prompt).toContain(""); + }); + test("GPT 5.3-codex model returns GPT-5.3 prompt", () => { // given const model = "openai/gpt-5.3-codex"; diff --git a/src/agents/hephaestus/agent.ts b/src/agents/hephaestus/agent.ts index e42214d8f..148c85834 100644 --- a/src/agents/hephaestus/agent.ts +++ b/src/agents/hephaestus/agent.ts @@ -1,6 +1,6 @@ import type { AgentConfig } from "@opencode-ai/sdk"; import type { AgentMode, AgentPromptMetadata } from "../types"; -import { isGpt5_4Model, isGpt5_3CodexModel } from "../types"; +import { isGpt5_3CodexModel, isGptNativeSisyphusModel } from "../types"; import type { AvailableAgent, AvailableTool, @@ -21,7 +21,7 @@ export type HephaestusPromptSource = "gpt-5-4" | "gpt-5-3-codex" | "gpt"; export function getHephaestusPromptSource( model?: string, ): HephaestusPromptSource { - if (model && isGpt5_4Model(model)) { + if (model && isGptNativeSisyphusModel(model)) { return "gpt-5-4"; } if (model && isGpt5_3CodexModel(model)) { diff --git a/src/agents/sisyphus.ts b/src/agents/sisyphus.ts index 81a863d54..6f938e841 100644 --- a/src/agents/sisyphus.ts +++ b/src/agents/sisyphus.ts @@ -1,6 +1,6 @@ import type { AgentConfig } from "@opencode-ai/sdk"; import type { AgentMode, AgentPromptMetadata } from "./types"; -import { isGptModel, isGeminiModel, isGpt5_4Model } from "./types"; +import { isGptModel, isGeminiModel, isGptNativeSisyphusModel } from "./types"; import { buildGeminiToolMandate, buildGeminiDelegationOverride, @@ -480,7 +480,7 @@ export function createSisyphusAgent( const categories = availableCategories ?? []; const agents = availableAgents ?? []; - if (isGpt5_4Model(model)) { + if (isGptNativeSisyphusModel(model)) { const prompt = buildGpt54SisyphusPrompt( model, agents, diff --git a/src/agents/types.test.ts b/src/agents/types.test.ts index 4c94e2868..13cb0bf93 100644 --- a/src/agents/types.test.ts +++ b/src/agents/types.test.ts @@ -1,26 +1,46 @@ import { describe, test, expect } from "bun:test"; -import { isGptModel, isGeminiModel, isGlmModel, isGpt5_4Model, isMiniMaxModel } from "./types"; +import { + isGptModel, + isGeminiModel, + isGlmModel, + isGptNativeSisyphusModel, + isMiniMaxModel, +} from "./types"; -describe("isGpt5_4Model", () => { - test("detects gpt-5.4 models", () => { - expect(isGpt5_4Model("openai/gpt-5.4")).toBe(true); - expect(isGpt5_4Model("openai/gpt-5-4")).toBe(true); - expect(isGpt5_4Model("openai/gpt-5.4-codex")).toBe(true); - expect(isGpt5_4Model("github-copilot/gpt-5.4")).toBe(true); - expect(isGpt5_4Model("venice/gpt-5-4")).toBe(true); +describe("isGptNativeSisyphusModel", () => { + test("allows GPT-5.x where x >= 4", () => { + expect(isGptNativeSisyphusModel("openai/gpt-5.4")).toBe(true); + expect(isGptNativeSisyphusModel("openai/gpt-5-4")).toBe(true); + expect(isGptNativeSisyphusModel("openai/gpt-5.5")).toBe(true); + expect(isGptNativeSisyphusModel("openai/gpt-5-5")).toBe(true); + expect(isGptNativeSisyphusModel("openai/gpt-5.9")).toBe(true); + expect(isGptNativeSisyphusModel("openai/gpt-5-9")).toBe(true); + expect(isGptNativeSisyphusModel("openai/gpt-5.10")).toBe(true); + expect(isGptNativeSisyphusModel("openai/gpt-5-10")).toBe(true); }); - test("does not match other GPT models", () => { - expect(isGpt5_4Model("openai/gpt-5.3-codex")).toBe(false); - expect(isGpt5_4Model("openai/gpt-5.1")).toBe(false); - expect(isGpt5_4Model("openai/gpt-4o")).toBe(false); - expect(isGpt5_4Model("github-copilot/gpt-4o")).toBe(false); + test("allows with various providers and suffixes", () => { + expect(isGptNativeSisyphusModel("github-copilot/gpt-5.4")).toBe(true); + expect(isGptNativeSisyphusModel("venice/gpt-5-4")).toBe(true); + expect(isGptNativeSisyphusModel("openai/gpt-5.4-codex")).toBe(true); + expect(isGptNativeSisyphusModel("openai/gpt-5.5-mini")).toBe(true); }); - test("does not match non-GPT models", () => { - expect(isGpt5_4Model("anthropic/claude-opus-4-7")).toBe(false); - expect(isGpt5_4Model("google/gemini-3.1-pro")).toBe(false); - expect(isGpt5_4Model("openai/o1")).toBe(false); + test("rejects GPT-5.x where x < 4", () => { + expect(isGptNativeSisyphusModel("openai/gpt-5.3-codex")).toBe(false); + expect(isGptNativeSisyphusModel("openai/gpt-5.1")).toBe(false); + expect(isGptNativeSisyphusModel("openai/gpt-5-0")).toBe(false); + }); + + test("rejects other GPT models", () => { + expect(isGptNativeSisyphusModel("openai/gpt-4o")).toBe(false); + expect(isGptNativeSisyphusModel("github-copilot/gpt-4o")).toBe(false); + }); + + test("rejects non-GPT models", () => { + expect(isGptNativeSisyphusModel("anthropic/claude-opus-4-7")).toBe(false); + expect(isGptNativeSisyphusModel("google/gemini-3.1-pro")).toBe(false); + expect(isGptNativeSisyphusModel("openai/o1")).toBe(false); }); }); diff --git a/src/agents/types.ts b/src/agents/types.ts index e5c03e006..1224e096b 100644 --- a/src/agents/types.ts +++ b/src/agents/types.ts @@ -79,9 +79,11 @@ export function isGptModel(model: string): boolean { return modelName.includes("gpt"); } -export function isGpt5_4Model(model: string): boolean { +const GPT_NATIVE_SISYPHUS_RE = /gpt-5[.-](?:[4-9]|\d{2,})/i; + +export function isGptNativeSisyphusModel(model: string): boolean { const modelName = extractModelName(model).toLowerCase(); - return modelName.includes("gpt-5.4") || modelName.includes("gpt-5-4"); + return GPT_NATIVE_SISYPHUS_RE.test(modelName); } export function isGpt5_3CodexModel(model: string): boolean { diff --git a/src/hooks/no-sisyphus-gpt/hook.ts b/src/hooks/no-sisyphus-gpt/hook.ts index fa1b53ebd..d5ed53c34 100644 --- a/src/hooks/no-sisyphus-gpt/hook.ts +++ b/src/hooks/no-sisyphus-gpt/hook.ts @@ -1,5 +1,5 @@ import type { PluginInput } from "@opencode-ai/plugin" -import { isGptModel, isGpt5_4Model } from "../../agents/types" +import { isGptModel, isGptNativeSisyphusModel } from "../../agents/types" import { getSessionAgent, resolveRegisteredAgentName, @@ -11,8 +11,8 @@ import { getAgentConfigKey } from "../../shared/agent-display-names" const TOAST_TITLE = "NEVER Use Sisyphus with GPT" const TOAST_MESSAGE = [ "Sisyphus works best with Claude Opus, and works fine with Kimi/GLM models.", - "Do NOT use Sisyphus with GPT (except GPT-5.4 which has specialized support).", - "For GPT models (other than 5.4), always use Hephaestus.", + "Do NOT use Sisyphus with GPT (except GPT-5.4 and GPT-5.5 which have specialized support).", + "For other GPT models, always use Hephaestus.", ].join("\n") function showToast(ctx: PluginInput, sessionID: string): void { ctx.client.tui.showToast({ @@ -43,7 +43,7 @@ export function createNoSisyphusGptHook(ctx: PluginInput) { const agentKey = getAgentConfigKey(rawAgent) const modelID = input.model?.modelID - if (agentKey === "sisyphus" && modelID && isGptModel(modelID) && !isGpt5_4Model(modelID)) { + if (agentKey === "sisyphus" && modelID && isGptModel(modelID) && !isGptNativeSisyphusModel(modelID)) { showToast(ctx, input.sessionID) input.agent = resolveRegisteredAgentName("hephaestus") ?? "hephaestus" if (output?.message) { diff --git a/src/hooks/no-sisyphus-gpt/index.test.ts b/src/hooks/no-sisyphus-gpt/index.test.ts index baeb23722..5dd7616b4 100644 --- a/src/hooks/no-sisyphus-gpt/index.test.ts +++ b/src/hooks/no-sisyphus-gpt/index.test.ts @@ -43,7 +43,7 @@ describe("no-sisyphus-gpt hook", () => { expect(showToast.mock.calls[0]?.[0]).toMatchObject({ body: { title: "NEVER Use Sisyphus with GPT", - message: expect.stringContaining("For GPT models (other than 5.4), always use Hephaestus."), + message: expect.stringContaining("For other GPT models, always use Hephaestus."), variant: "error", }, }) @@ -70,6 +70,27 @@ describe("no-sisyphus-gpt hook", () => { expect(output.message.agent).toBeUndefined() }) + test("does not show toast for gpt-5.5 model (native Sisyphus support)", async () => { + // given - sisyphus with gpt-5.5 model (should be allowed) + const showToast = spyOn({ fn: async () => ({}) }, "fn") + const hook = createNoSisyphusGptHook({ + client: { tui: { showToast } }, + } as any) + + const output = createOutput() + + // when - chat.message runs with gpt-5.5 + await hook["chat.message"]?.({ + sessionID: "ses_gpt55", + agent: SISYPHUS_DISPLAY, + model: { providerID: "openai", modelID: "gpt-5.5" }, + }, output) + + // then - no toast, agent NOT switched to Hephaestus + expect(showToast).toHaveBeenCalledTimes(0) + expect(output.message.agent).toBeUndefined() + }) + test("does not show toast for non-gpt model", async () => { // given - sisyphus with claude model const showToast = spyOn({ fn: async () => ({}) }, "fn")