Skip to content

Commit b07da4e

Browse files
committed
fix: altimate-backend models hit Claude-fallback prompt that triggered GPT-5.x refusals
Root cause of the plan-mode refusals on `altimate-backend/altimate-default`: the system-prompt router in `session/system.ts` selects a base prompt by string-matching `model.api.id`. The gateway's api.id is the opaque alias `altimate-default` (kept stable for backward compatibility — users persist it in `model.json` favorites/recents), which matches none of the patterns (`gpt-5`, `gpt-`, `gemini-`, `claude`, `trinity`), so it falls through to `PROMPT_ANTHROPIC_WITHOUT_TODO` (`qwen.txt`). `qwen.txt` begins with two hard "MUST refuse if borderline/malicious" Claude-style directives. The upstream gateway model is GPT-5.x (an OpenAI-trained model), and when those directives stack with plan.txt's "STRICTLY FORBIDDEN: ANY modifications. ZERO exceptions." language, the combination is enough to push GPT-5.x into refusing benign developer requests (confirmed repro: "plan a feature to add a button called verify output in the UI of altimate-code" → "I'm sorry, but I cannot assist with that request."). Fix: route altimate-backend models by `model.family` (which the provider definition explicitly pins to `"openai"` for `altimate-default` at `provider.ts:1291`) before the api.id string-match fallthrough. The gateway-routed GPT-5.x model now gets `codex_header.txt` (its native prompt) which contains zero refusal language. Touches: - `packages/opencode/src/session/system.ts`: family-based routing for altimate-backend; falls through to existing logic otherwise. - `packages/opencode/test/session/system.test.ts`: regression tests locking the routing contract for the three relevant cases (altimate openai, altimate anthropic, non-altimate openai). This is the actual fix for #887 / AI-6957 — supersedes the warning-copy and plan.txt changes from d21f9b1 / 6bbcfbf as the *user-visible* behavior change. Those earlier commits remain correct: stronger exploration mandate in plan.txt and accurate warning copy are still improvements, just not the load-bearing fix. Typecheck clean. 620 tests pass.
1 parent 6bbcfbf commit b07da4e

2 files changed

Lines changed: 80 additions & 0 deletions

File tree

‎packages/opencode/src/session/system.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,22 @@ export namespace SystemPrompt {
2929
}
3030

3131
export function provider(model: Provider.Model) {
32+
// altimate_change start — route altimate-backend gateway models by `family`
33+
// before the api.id string-match fallthrough. The gateway's model.api.id is
34+
// the opaque alias `altimate-default` (kept stable for backward compat —
35+
// users persist it in model.json), which matches none of the patterns below.
36+
// Without this guard it fell through to PROMPT_ANTHROPIC_WITHOUT_TODO
37+
// (qwen.txt), whose leading "MUST refuse if borderline/malicious" Claude-
38+
// style directives, stacked on plan.txt's "STRICTLY FORBIDDEN/ZERO
39+
// exceptions" language, pushed the upstream GPT-5.x model into refusing
40+
// benign requests like "add a button to the UI" (see #887 / AI-6957).
41+
// Family is the authoritative signal — the provider definition pins it.
42+
if (model.providerID === "altimate-backend") {
43+
if (model.family === "openai") return [PROMPT_CODEX]
44+
if (model.family === "anthropic") return [PROMPT_ANTHROPIC]
45+
if (model.family === "gemini") return [PROMPT_GEMINI]
46+
}
47+
// altimate_change end
3248
if (model.api.id.includes("gpt-5")) return [PROMPT_CODEX]
3349
if (model.api.id.includes("gpt-") || model.api.id.includes("o1") || model.api.id.includes("o3"))
3450
return [PROMPT_BEAST]

‎packages/opencode/test/session/system.test.ts‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,72 @@ import path from "path"
33
import { Agent } from "../../src/agent/agent"
44
import { Instance } from "../../src/project/instance"
55
import { SystemPrompt } from "../../src/session/system"
6+
import type { Provider } from "../../src/provider/provider"
67
import { tmpdir } from "../fixture/fixture"
78

9+
function makeModel(overrides: Partial<Provider.Model> & { apiId: string; family?: string; providerID?: string }): Provider.Model {
10+
return {
11+
id: overrides.apiId as any,
12+
providerID: (overrides.providerID ?? "test") as any,
13+
api: { id: overrides.apiId, url: "", npm: "@ai-sdk/openai-compatible" },
14+
name: overrides.apiId,
15+
family: overrides.family,
16+
capabilities: {
17+
temperature: true,
18+
reasoning: false,
19+
attachment: false,
20+
toolcall: true,
21+
input: { text: true, audio: false, image: false, video: false, pdf: false },
22+
output: { text: true, audio: false, image: false, video: false, pdf: false },
23+
interleaved: false,
24+
},
25+
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
26+
limit: { context: 0, output: 0 },
27+
status: "active",
28+
options: {},
29+
headers: {},
30+
release_date: "2025-01-01",
31+
} as Provider.Model
32+
}
33+
34+
describe("session.system.provider routing", () => {
35+
// Regression guard for the altimate-default routing bug (GH #887 / AI-6957).
36+
// Before this fix, `altimate-default` (api.id literal, kept stable for backward
37+
// compatibility) matched none of the string-based conditions and fell through
38+
// to PROMPT_ANTHROPIC_WITHOUT_TODO (qwen.txt), whose Claude-style "MUST refuse
39+
// if borderline/malicious" directives caused the upstream GPT-5.x gateway
40+
// model to refuse benign requests in plan mode.
41+
// `codex_header.txt` (GPT-5 prompt) is the only base prompt with `## Editing
42+
// constraints` / `apply_patch` — codex-specific. The Claude-style fallback
43+
// (`qwen.txt`) is the only base prompt that begins with "Refuse to write code
44+
// or explain code that may be used maliciously".
45+
46+
test("altimate-default routes to GPT-5 prompt via provider+family, not the Anthropic fallback", () => {
47+
const prompts = SystemPrompt.provider(
48+
makeModel({ apiId: "altimate-default", providerID: "altimate-backend", family: "openai" }),
49+
)
50+
expect(prompts).toHaveLength(1)
51+
expect(prompts[0]).toMatch(/## Editing constraints/)
52+
expect(prompts[0]).toMatch(/apply_patch/)
53+
expect(prompts[0]).not.toMatch(/Refuse to write code or explain code that may be used maliciously/)
54+
})
55+
56+
test("altimate-backend anthropic-family routes to Claude prompt, not GPT-5 codex", () => {
57+
const prompts = SystemPrompt.provider(
58+
makeModel({ apiId: "altimate-something", providerID: "altimate-backend", family: "anthropic" }),
59+
)
60+
expect(prompts[0]).not.toMatch(/## Editing constraints/)
61+
expect(prompts[0]).not.toMatch(/Refuse to write code or explain code that may be used maliciously/)
62+
})
63+
64+
test("non-altimate openai models still use the existing api.id matching", () => {
65+
const gpt5 = SystemPrompt.provider(makeModel({ apiId: "gpt-5", providerID: "openai" }))
66+
expect(gpt5[0]).toMatch(/## Editing constraints/)
67+
const gpt4 = SystemPrompt.provider(makeModel({ apiId: "gpt-4o", providerID: "openai" }))
68+
expect(gpt4[0]).not.toMatch(/## Editing constraints/)
69+
})
70+
})
71+
872
describe("session.system", () => {
973
test("skills output is sorted by name and stable across calls", async () => {
1074
await using tmp = await tmpdir({

0 commit comments

Comments
 (0)