Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fixes/15841-haiku-5-5-adaptive-thinking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(claude):** `claude-haiku-5-5` requests keep `thinking.type: "adaptive"` instead of being downgraded to `"enabled"`, which Anthropic rejected with HTTP 400 ([#15841](https://github.com/diegosouzapw/OmniRoute/pull/15841)) — thanks @aldoeliacim
13 changes: 10 additions & 3 deletions open-sse/services/claudeHaikuConstraints.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { isAdaptiveThinkingOnly } from "../../src/shared/constants/modelSpecs.ts";

type JsonRecord = Record<string, unknown>;

// Models that reject `thinking.type:"adaptive"` and `output_config.effort` —
// today, all Claude Haiku-tier models (4.5 / 3.5 / dated and aliased). Adaptive
// thinking + the effort knob landed on Sonnet 4.6 / Opus 4.5+ only; sending
// either on Haiku is a hard 400 from the Messages API.
// Claude Haiku-tier models that predate adaptive thinking (4.5 / 3.5 / dated and
// aliased). Haiku 5.5+ is adaptive-only (spec `adaptiveThinkingOnly`), so it
// must NOT be rewritten here: the family regex alone wrongly downgrades it.
// Adaptive thinking + the effort knob landed on Sonnet 4.6 / Opus 4.5+ and
// Haiku 5.5; sending `enabled` to those is a hard 400 from the Messages API.
const HAIKU_CONSTRAINT_PATTERN = /haiku/i;

// Default budget when collapsing `thinking.type:"adaptive"` to a manual shape
Expand All @@ -18,6 +22,9 @@ function asRecord(value: unknown): JsonRecord | null {

function modelRejectsAdaptiveAndEffort(modelId: string | null | undefined): boolean {
if (typeof modelId !== "string" || modelId.length === 0) return false;
// Spec is authoritative: an adaptive-only model accepts adaptive+effort, so
// it is never rejected here regardless of its family name.
if (isAdaptiveThinkingOnly(modelId)) return false;
return HAIKU_CONSTRAINT_PATTERN.test(modelId);
}

Expand Down
15 changes: 15 additions & 0 deletions src/shared/constants/modelSpecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,21 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
aliases: BEDROCK_CLAUDE_ALIASES("claude-sonnet-5"),
},

// ── Claude Haiku 5.5 ────────────────────────────────────────────
// Adaptive-thinking-only, like Sonnet 5.5: `thinking.type:"enabled"` returns
// 400 ("use thinking.type.adaptive and output_config.effort"). Haiku 4.5 and
// earlier still accept manual budgets, so this spec must NOT be widened to
// the /haiku/ family. Limits from the gateway catalog (context 1M, output 128K).
"claude-haiku-5-5": {
maxOutputTokens: 128000,
contextWindow: 1000000,
supportsThinking: true,
supportsTools: true,
supportsVision: true,
adaptiveThinkingOnly: true,
aliases: BEDROCK_CLAUDE_ALIASES("claude-haiku-5-5"),
},

// ── Claude Sonnet 5.5 ───────────────────────────────────────────
"claude-sonnet-5-5": {
// Same shape as Sonnet 5, but it rejects thinking.type:"disabled"
Expand Down
24 changes: 24 additions & 0 deletions tests/unit/claude-haiku-no-adaptive-thinking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,30 @@ describe("normalizeClaudeHaikuConstraints", () => {
assert.strictEqual(out, input);
});

// Regression: Haiku 5.5 is adaptive-only (spec adaptiveThinkingOnly). The
// family regex used to rewrite its adaptive+effort to manual `enabled`, which
// Anthropic rejects with a 400 ("use thinking.type.adaptive ...").
it("leaves claude-haiku-5-5 adaptive thinking + effort untouched", () => {
const input = {
model: "claude-haiku-5-5",
thinking: { type: "adaptive" },
output_config: { effort: "high" },
};
const out = normalizeClaudeHaikuConstraints(input, input.model);
assert.strictEqual(out, input);
assert.deepEqual(out.thinking, { type: "adaptive" });
assert.deepEqual(out.output_config, { effort: "high" });
});

it("still downgrades adaptive on pre-5 Haiku (4.5 keeps the manual-only contract)", () => {
const input = {
model: "claude-haiku-4-5-20251001",
thinking: { type: "adaptive" },
};
const out = normalizeClaudeHaikuConstraints(input, input.model);
assert.deepEqual(out.thinking, { type: "enabled", budget_tokens: 10000 });
});

it("leaves haiku bodies that have no adaptive thinking or effort untouched", () => {
const input = {
model: "claude-haiku-4-5-20251001",
Expand Down
Loading