Merge pull request #2107 from code-yeongyu/fix/issue-2054-hephaestus-model-opt-out

fix(no-hephaestus-non-gpt): add opt-out for model enforcement
This commit is contained in:
YeonGyu-Kim
2026-02-25 16:16:50 +09:00
committed by GitHub
4 changed files with 58 additions and 11 deletions

View File

@@ -59,7 +59,9 @@ export const AgentOverridesSchema = z.object({
build: AgentOverrideConfigSchema.optional(), build: AgentOverrideConfigSchema.optional(),
plan: AgentOverrideConfigSchema.optional(), plan: AgentOverrideConfigSchema.optional(),
sisyphus: AgentOverrideConfigSchema.optional(), sisyphus: AgentOverrideConfigSchema.optional(),
hephaestus: AgentOverrideConfigSchema.optional(), hephaestus: AgentOverrideConfigSchema.extend({
allow_non_gpt_model: z.boolean().optional(),
}).optional(),
"sisyphus-junior": AgentOverrideConfigSchema.optional(), "sisyphus-junior": AgentOverrideConfigSchema.optional(),
"OpenCode-Builder": AgentOverrideConfigSchema.optional(), "OpenCode-Builder": AgentOverrideConfigSchema.optional(),
prometheus: AgentOverrideConfigSchema.optional(), prometheus: AgentOverrideConfigSchema.optional(),

View File

@@ -12,12 +12,16 @@ const TOAST_MESSAGE = [
].join("\n") ].join("\n")
const SISYPHUS_DISPLAY = getAgentDisplayName("sisyphus") const SISYPHUS_DISPLAY = getAgentDisplayName("sisyphus")
function showToast(ctx: PluginInput, sessionID: string): void { type NoHephaestusNonGptHookOptions = {
allowNonGptModel?: boolean
}
function showToast(ctx: PluginInput, sessionID: string, variant: "error" | "warning"): void {
ctx.client.tui.showToast({ ctx.client.tui.showToast({
body: { body: {
title: TOAST_TITLE, title: TOAST_TITLE,
message: TOAST_MESSAGE, message: TOAST_MESSAGE,
variant: "error", variant,
duration: 10000, duration: 10000,
}, },
}).catch((error) => { }).catch((error) => {
@@ -28,7 +32,10 @@ function showToast(ctx: PluginInput, sessionID: string): void {
}) })
} }
export function createNoHephaestusNonGptHook(ctx: PluginInput) { export function createNoHephaestusNonGptHook(
ctx: PluginInput,
options?: NoHephaestusNonGptHookOptions,
) {
return { return {
"chat.message": async (input: { "chat.message": async (input: {
sessionID: string sessionID: string
@@ -40,9 +47,13 @@ export function createNoHephaestusNonGptHook(ctx: PluginInput) {
const rawAgent = input.agent ?? getSessionAgent(input.sessionID) ?? "" const rawAgent = input.agent ?? getSessionAgent(input.sessionID) ?? ""
const agentKey = getAgentConfigKey(rawAgent) const agentKey = getAgentConfigKey(rawAgent)
const modelID = input.model?.modelID const modelID = input.model?.modelID
const allowNonGptModel = options?.allowNonGptModel === true
if (agentKey === "hephaestus" && modelID && !isGptModel(modelID)) { if (agentKey === "hephaestus" && modelID && !isGptModel(modelID)) {
showToast(ctx, input.sessionID) showToast(ctx, input.sessionID, allowNonGptModel ? "warning" : "error")
if (allowNonGptModel) {
return
}
input.agent = SISYPHUS_DISPLAY input.agent = SISYPHUS_DISPLAY
if (output?.message) { if (output?.message) {
output.message.agent = SISYPHUS_DISPLAY output.message.agent = SISYPHUS_DISPLAY

View File

@@ -1,3 +1,5 @@
/// <reference types="bun-types" />
import { describe, expect, spyOn, test } from "bun:test" import { describe, expect, spyOn, test } from "bun:test"
import { _resetForTesting, updateSessionAgent } from "../../features/claude-code-session-state" import { _resetForTesting, updateSessionAgent } from "../../features/claude-code-session-state"
import { getAgentDisplayName } from "../../shared/agent-display-names" import { getAgentDisplayName } from "../../shared/agent-display-names"
@@ -8,7 +10,7 @@ const SISYPHUS_DISPLAY = getAgentDisplayName("sisyphus")
function createOutput() { function createOutput() {
return { return {
message: {}, message: {} as { agent?: string; [key: string]: unknown },
parts: [], parts: [],
} }
} }
@@ -16,7 +18,7 @@ function createOutput() {
describe("no-hephaestus-non-gpt hook", () => { describe("no-hephaestus-non-gpt hook", () => {
test("shows toast on every chat.message when hephaestus uses non-gpt model", async () => { test("shows toast on every chat.message when hephaestus uses non-gpt model", async () => {
// given - hephaestus with claude model // given - hephaestus with claude model
const showToast = spyOn({ fn: async () => ({}) }, "fn") const showToast = spyOn({ fn: async (_input: unknown) => ({}) }, "fn")
const hook = createNoHephaestusNonGptHook({ const hook = createNoHephaestusNonGptHook({
client: { tui: { showToast } }, client: { tui: { showToast } },
} as any) } as any)
@@ -49,9 +51,38 @@ describe("no-hephaestus-non-gpt hook", () => {
}) })
}) })
test("shows warning and does not switch agent when allow_non_gpt_model is enabled", async () => {
// given - hephaestus with claude model and opt-out enabled
const showToast = spyOn({ fn: async (_input: unknown) => ({}) }, "fn")
const hook = createNoHephaestusNonGptHook({
client: { tui: { showToast } },
} as any, {
allowNonGptModel: true,
})
const output = createOutput()
// when - chat.message runs
await hook["chat.message"]?.({
sessionID: "ses_opt_out",
agent: HEPHAESTUS_DISPLAY,
model: { providerID: "anthropic", modelID: "claude-opus-4-6" },
}, output)
// then - warning toast is shown but agent is not switched
expect(showToast).toHaveBeenCalledTimes(1)
expect(output.message.agent).toBeUndefined()
expect(showToast.mock.calls[0]?.[0]).toMatchObject({
body: {
title: "NEVER Use Hephaestus with Non-GPT",
variant: "warning",
},
})
})
test("does not show toast when hephaestus uses gpt model", async () => { test("does not show toast when hephaestus uses gpt model", async () => {
// given - hephaestus with gpt model // given - hephaestus with gpt model
const showToast = spyOn({ fn: async () => ({}) }, "fn") const showToast = spyOn({ fn: async (_input: unknown) => ({}) }, "fn")
const hook = createNoHephaestusNonGptHook({ const hook = createNoHephaestusNonGptHook({
client: { tui: { showToast } }, client: { tui: { showToast } },
} as any) } as any)
@@ -72,7 +103,7 @@ describe("no-hephaestus-non-gpt hook", () => {
test("does not show toast for non-hephaestus agent", async () => { test("does not show toast for non-hephaestus agent", async () => {
// given - sisyphus with claude model (non-gpt) // given - sisyphus with claude model (non-gpt)
const showToast = spyOn({ fn: async () => ({}) }, "fn") const showToast = spyOn({ fn: async (_input: unknown) => ({}) }, "fn")
const hook = createNoHephaestusNonGptHook({ const hook = createNoHephaestusNonGptHook({
client: { tui: { showToast } }, client: { tui: { showToast } },
} as any) } as any)
@@ -95,7 +126,7 @@ describe("no-hephaestus-non-gpt hook", () => {
// given - session agent saved as hephaestus // given - session agent saved as hephaestus
_resetForTesting() _resetForTesting()
updateSessionAgent("ses_4", HEPHAESTUS_DISPLAY) updateSessionAgent("ses_4", HEPHAESTUS_DISPLAY)
const showToast = spyOn({ fn: async () => ({}) }, "fn") const showToast = spyOn({ fn: async (_input: unknown) => ({}) }, "fn")
const hook = createNoHephaestusNonGptHook({ const hook = createNoHephaestusNonGptHook({
client: { tui: { showToast } }, client: { tui: { showToast } },
} as any) } as any)

View File

@@ -232,7 +232,10 @@ export function createSessionHooks(args: {
: null : null
const noHephaestusNonGpt = isHookEnabled("no-hephaestus-non-gpt") const noHephaestusNonGpt = isHookEnabled("no-hephaestus-non-gpt")
? safeHook("no-hephaestus-non-gpt", () => createNoHephaestusNonGptHook(ctx)) ? safeHook("no-hephaestus-non-gpt", () =>
createNoHephaestusNonGptHook(ctx, {
allowNonGptModel: pluginConfig.agents?.hephaestus?.allow_non_gpt_model,
}))
: null : null
const questionLabelTruncator = isHookEnabled("question-label-truncator") const questionLabelTruncator = isHookEnabled("question-label-truncator")