From f6b2ce7b6b23c6ec92a4ffc4deb2742707dc4255 Mon Sep 17 00:00:00 2001 From: Jay1 Date: Thu, 23 Apr 2026 20:49:11 -0400 Subject: [PATCH 1/2] feat(agents): add gpt-5.5 native sisyphus support Route GPT-5.5 through the existing GPT-5.4-native Sisyphus and Hephaestus prompt family while keeping one shared model-family helper. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/agents/hephaestus/agent.test.ts | 30 ++++++++++++++++ src/agents/hephaestus/agent.ts | 4 +-- src/agents/sisyphus.ts | 4 +-- src/agents/types.test.ts | 46 ++++++++++++++++++++++++- src/agents/types.ts | 9 +++++ src/hooks/no-sisyphus-gpt/hook.ts | 8 ++--- src/hooks/no-sisyphus-gpt/index.test.ts | 23 ++++++++++++- 7 files changed, 114 insertions(+), 10 deletions(-) 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..3491c46b8 100644 --- a/src/agents/types.test.ts +++ b/src/agents/types.test.ts @@ -1,5 +1,13 @@ import { describe, test, expect } from "bun:test"; -import { isGptModel, isGeminiModel, isGlmModel, isGpt5_4Model, isMiniMaxModel } from "./types"; +import { + isGptModel, + isGeminiModel, + isGlmModel, + isGpt5_4Model, + isGpt5_5Model, + isGptNativeSisyphusModel, + isMiniMaxModel, +} from "./types"; describe("isGpt5_4Model", () => { test("detects gpt-5.4 models", () => { @@ -24,6 +32,42 @@ describe("isGpt5_4Model", () => { }); }); +describe("isGpt5_5Model", () => { + test("detects gpt-5.5 models", () => { + expect(isGpt5_5Model("gpt-5.5")).toBe(true); + expect(isGpt5_5Model("gpt-5-5")).toBe(true); + expect(isGpt5_5Model("openai/gpt-5.5")).toBe(true); + expect(isGpt5_5Model("openai/gpt-5-5")).toBe(true); + expect(isGpt5_5Model("github-copilot/gpt-5.5")).toBe(true); + }); + + test("does not match other GPT models", () => { + expect(isGpt5_5Model("openai/gpt-5.4")).toBe(false); + expect(isGpt5_5Model("openai/gpt-5.3-codex")).toBe(false); + expect(isGpt5_5Model("openai/gpt-4o")).toBe(false); + }); + + test("does not match non-GPT models", () => { + expect(isGpt5_5Model("anthropic/claude-opus-4-7")).toBe(false); + expect(isGpt5_5Model("google/gemini-3.1-pro")).toBe(false); + }); +}); + +describe("isGptNativeSisyphusModel", () => { + test("allows GPT-5.4 and GPT-5.5 variants", () => { + 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); + }); + + test("rejects non-native Sisyphus GPT models and non-GPT models", () => { + expect(isGptNativeSisyphusModel("openai/gpt-5.3-codex")).toBe(false); + expect(isGptNativeSisyphusModel("openai/gpt-4o")).toBe(false); + expect(isGptNativeSisyphusModel("anthropic/claude-opus-4-7")).toBe(false); + }); +}); + describe("isGptModel", () => { test("standard openai provider gpt models", () => { expect(isGptModel("openai/gpt-5.4")).toBe(true); diff --git a/src/agents/types.ts b/src/agents/types.ts index e5c03e006..abab2cbb2 100644 --- a/src/agents/types.ts +++ b/src/agents/types.ts @@ -84,6 +84,15 @@ export function isGpt5_4Model(model: string): boolean { return modelName.includes("gpt-5.4") || modelName.includes("gpt-5-4"); } +export function isGpt5_5Model(model: string): boolean { + const modelName = extractModelName(model).toLowerCase(); + return modelName.includes("gpt-5.5") || modelName.includes("gpt-5-5"); +} + +export function isGptNativeSisyphusModel(model: string): boolean { + return isGpt5_4Model(model) || isGpt5_5Model(model); +} + export function isGpt5_3CodexModel(model: string): boolean { const modelName = extractModelName(model).toLowerCase(); return modelName.includes("gpt-5.3-codex") || modelName.includes("gpt-5-3-codex"); 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") From 563b6569d302ff169cc756132a02db84ea851275 Mon Sep 17 00:00:00 2001 From: acamq <179265037+acamq@users.noreply.github.com> Date: Thu, 23 Apr 2026 20:37:06 -0600 Subject: [PATCH 2/2] refactor(agents): replace per-version GPT checks with regex pattern Replace isGpt5_4Model + isGpt5_5Model + OR-composed isGptNativeSisyphusModel with a single regex matching GPT-5.x where x >= 4. Automatically covers future versions (5.6, 5.7, 5.10+) without code changes. Constraint: Must continue to reject gpt-5.3-codex and gpt-5.x where x < 4 Rejected: Per-version functions | not scalable, each new version adds a function + OR clause Confidence: high Scope-risk: narrow --- src/agents/types.test.ts | 72 ++++++++++++++-------------------------- src/agents/types.ts | 13 ++------ 2 files changed, 27 insertions(+), 58 deletions(-) diff --git a/src/agents/types.test.ts b/src/agents/types.test.ts index 3491c46b8..13cb0bf93 100644 --- a/src/agents/types.test.ts +++ b/src/agents/types.test.ts @@ -3,68 +3,44 @@ import { isGptModel, isGeminiModel, isGlmModel, - isGpt5_4Model, - isGpt5_5Model, 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); - }); - - 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("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); - }); -}); - -describe("isGpt5_5Model", () => { - test("detects gpt-5.5 models", () => { - expect(isGpt5_5Model("gpt-5.5")).toBe(true); - expect(isGpt5_5Model("gpt-5-5")).toBe(true); - expect(isGpt5_5Model("openai/gpt-5.5")).toBe(true); - expect(isGpt5_5Model("openai/gpt-5-5")).toBe(true); - expect(isGpt5_5Model("github-copilot/gpt-5.5")).toBe(true); - }); - - test("does not match other GPT models", () => { - expect(isGpt5_5Model("openai/gpt-5.4")).toBe(false); - expect(isGpt5_5Model("openai/gpt-5.3-codex")).toBe(false); - expect(isGpt5_5Model("openai/gpt-4o")).toBe(false); - }); - - test("does not match non-GPT models", () => { - expect(isGpt5_5Model("anthropic/claude-opus-4-7")).toBe(false); - expect(isGpt5_5Model("google/gemini-3.1-pro")).toBe(false); - }); -}); - describe("isGptNativeSisyphusModel", () => { - test("allows GPT-5.4 and GPT-5.5 variants", () => { + 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("rejects non-native Sisyphus GPT models and non-GPT models", () => { + 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("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 abab2cbb2..1224e096b 100644 --- a/src/agents/types.ts +++ b/src/agents/types.ts @@ -79,18 +79,11 @@ export function isGptModel(model: string): boolean { return modelName.includes("gpt"); } -export function isGpt5_4Model(model: string): boolean { - const modelName = extractModelName(model).toLowerCase(); - return modelName.includes("gpt-5.4") || modelName.includes("gpt-5-4"); -} - -export function isGpt5_5Model(model: string): boolean { - const modelName = extractModelName(model).toLowerCase(); - return modelName.includes("gpt-5.5") || modelName.includes("gpt-5-5"); -} +const GPT_NATIVE_SISYPHUS_RE = /gpt-5[.-](?:[4-9]|\d{2,})/i; export function isGptNativeSisyphusModel(model: string): boolean { - return isGpt5_4Model(model) || isGpt5_5Model(model); + const modelName = extractModelName(model).toLowerCase(); + return GPT_NATIVE_SISYPHUS_RE.test(modelName); } export function isGpt5_3CodexModel(model: string): boolean {