mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 04:42:10 +03:00
fix: model-aware supportsRedactedThinking for mixed-format providers (#4479)
Thanks @TF0rd! Rebased onto release/v3.8.32. Added the missing regression test for the model-aware redacted_thinking branch (Rule #18): minimax-m3 (claude format) gets redacted_thinking, glm-5.1 (openai) stays plain. 3/3.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// Claude helper functions for translator
|
||||
import { DEFAULT_THINKING_CLAUDE_SIGNATURE } from "../../config/defaultThinkingSignature.ts";
|
||||
import { lookupReasoning, recordReplay } from "../../services/reasoningCache.ts";
|
||||
import { getModelTargetFormat } from "../../config/providerModels.ts";
|
||||
|
||||
// MiniMax exposes a Claude-compatible endpoint but rejects Anthropic's extended
|
||||
// `output_config` parameter (used to steer reasoning effort and structured output)
|
||||
@@ -220,7 +221,8 @@ function markMessageCacheControl(msg: ClaudeMessage, ttl?: string): boolean {
|
||||
export function prepareClaudeRequest(
|
||||
body: ClaudeRequestBody,
|
||||
provider: string | null = null,
|
||||
preserveCacheControl = false
|
||||
preserveCacheControl = false,
|
||||
model: string | null = null
|
||||
): ClaudeRequestBody {
|
||||
// 0. Strip Anthropic `output_config` for providers that reject it on their
|
||||
// Claude-compatible endpoints (MiniMax). Must run before any downstream
|
||||
@@ -243,7 +245,13 @@ export function prepareClaudeRequest(
|
||||
// We use the same allowlist as prompt-caching: only Anthropic-native
|
||||
// upstreams get redacted_thinking. Everything else gets plain thinking blocks
|
||||
// backed by reasoningCache (real text) or a placeholder (cache miss).
|
||||
const supportsRedactedThinking = supportsPromptCaching;
|
||||
// Mixed-format providers (e.g. opencode-go) may have some models targeting
|
||||
// Anthropic's Messages API (targetFormat=claude) and others targeting OpenAI.
|
||||
// When the specific model targets Claude format, it's hitting a real Anthropic
|
||||
// endpoint that validates signatures — so it needs redacted_thinking too.
|
||||
const modelTargetsClaude =
|
||||
!!provider && !!model && getModelTargetFormat(provider, model) === "claude";
|
||||
const supportsRedactedThinking = supportsPromptCaching || modelTargetsClaude;
|
||||
|
||||
const systemBlocks = body.system;
|
||||
if (systemBlocks && Array.isArray(systemBlocks) && !preserveCacheControl) {
|
||||
|
||||
@@ -256,7 +256,7 @@ export function translateRequest(
|
||||
if (targetFormat === FORMATS.CLAUDE) {
|
||||
const isClaudePassthrough = sourceFormat === FORMATS.CLAUDE;
|
||||
const preserveCache = isClaudePassthrough || options?.preserveCacheControl === true;
|
||||
result = prepareClaudeRequest(result, provider, preserveCache);
|
||||
result = prepareClaudeRequest(result, provider, preserveCache, model);
|
||||
}
|
||||
|
||||
// Normalize openai-responses input shape for providers that require list input.
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Regression for #4479 (port: model-aware supportsRedactedThinking for mixed-format providers).
|
||||
*
|
||||
* Mixed-format providers like `opencode-go` have some models that target Anthropic's
|
||||
* Messages API (targetFormat="claude", e.g. minimax-m3) and others that target OpenAI's
|
||||
* /chat/completions (default, e.g. glm-5.1). The Anthropic-format models hit a real
|
||||
* Anthropic endpoint that validates signatures, so they need `redacted_thinking` blocks —
|
||||
* but `supportsRedactedThinking` used to be decided by provider name alone, so those models
|
||||
* received plain `thinking` blocks without a signature and got a 400.
|
||||
*
|
||||
* prepareClaudeRequest now takes a `model` param and also enables redacted_thinking when
|
||||
* getModelTargetFormat(provider, model) === "claude". These tests pin both branches plus
|
||||
* the backward-compatible no-model call.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { prepareClaudeRequest } = await import("../../open-sse/translator/helpers/claudeHelper.ts");
|
||||
const { DEFAULT_THINKING_CLAUDE_SIGNATURE } = await import(
|
||||
"../../open-sse/config/defaultThinkingSignature.ts"
|
||||
);
|
||||
|
||||
// Assistant turn with only a tool_use → prepareClaudeRequest injects a thinking block before
|
||||
// it. Anthropic-format upstreams get redacted_thinking{data}; everyone else gets plain
|
||||
// thinking{text}. Mirrors the shape in translator-claude-helper-thinking.test.ts.
|
||||
function bodyWithToolUseNeedingThinking() {
|
||||
return {
|
||||
thinking: { type: "enabled", budget_tokens: 4096 },
|
||||
messages: [
|
||||
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "tool_use", id: "call_x", name: "ls", input: { path: "." } }],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "tool_result", tool_use_id: "call_x", content: "README.md" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
test("opencode-go + claude-format model (minimax-m3) → redacted_thinking with synthetic data", () => {
|
||||
const result = prepareClaudeRequest(
|
||||
bodyWithToolUseNeedingThinking() as any,
|
||||
"opencode-go",
|
||||
false,
|
||||
"minimax-m3"
|
||||
);
|
||||
const content = (result as any).messages[1].content;
|
||||
assert.equal(content[0].type, "redacted_thinking", "claude-targetFormat model needs redacted_thinking");
|
||||
assert.equal(content[0].data, DEFAULT_THINKING_CLAUDE_SIGNATURE);
|
||||
assert.equal(content[0].thinking, undefined);
|
||||
});
|
||||
|
||||
test("opencode-go + OpenAI-format model (glm-5.1) → plain thinking (no redaction)", () => {
|
||||
const result = prepareClaudeRequest(
|
||||
bodyWithToolUseNeedingThinking() as any,
|
||||
"opencode-go",
|
||||
false,
|
||||
"glm-5.1"
|
||||
);
|
||||
const content = (result as any).messages[1].content;
|
||||
assert.equal(content[0].type, "thinking", "OpenAI-targetFormat model must stay plain thinking");
|
||||
assert.equal(content[0].data, undefined, "no synthetic redacted data for non-Anthropic upstream");
|
||||
});
|
||||
|
||||
test("opencode-go without model param → plain thinking (backward-compatible)", () => {
|
||||
const result = prepareClaudeRequest(bodyWithToolUseNeedingThinking() as any, "opencode-go");
|
||||
const content = (result as any).messages[1].content;
|
||||
assert.equal(content[0].type, "thinking", "no model → old provider-name behavior preserved");
|
||||
});
|
||||
Reference in New Issue
Block a user