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:
Tyler Ford
2026-06-21 07:48:06 -04:00
committed by GitHub
parent c3fd2b9aa1
commit 52e6a8b80f
3 changed files with 83 additions and 3 deletions

View File

@@ -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) {

View File

@@ -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.

View File

@@ -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");
});