fix(minimax): normalize unsigned thinking block starts (#9256)

Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates — only pre-existing audit.test.ts flake).
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-08-05 22:41:13 -03:00
committed by GitHub
parent 3a1e42d985
commit aff021e78f
7 changed files with 143 additions and 4 deletions

View File

@@ -0,0 +1 @@
- **fix(minimax):** add the required empty signature placeholder to unsigned thinking block starts. (thanks @rixzkiye)

View File

@@ -12,6 +12,7 @@ export const minimax_cnProvider: RegistryEntry = {
authType: "apikey",
authHeader: "bearer",
headers: getAnthropicCompatHeaders(),
ensureThinkingSignature: true,
models: [
// Keep parity with minimax to ensure model discovery works for minimax-cn connections.
// #3110: MiniMax M3 — frontier coding model with 1M context

View File

@@ -12,6 +12,7 @@ export const minimaxProvider: RegistryEntry = {
authType: "apikey",
authHeader: "bearer",
headers: getAnthropicCompatHeaders(),
ensureThinkingSignature: true,
models: [
// T12/T28: MiniMax default upgraded from M2.5 to M2.7
// #3110: MiniMax M3 — frontier coding model with 1M context

View File

@@ -179,6 +179,12 @@ export interface RegistryEntry {
* standard OpenAI array-shaped content untouched (see openai-responses.ts).
*/
requiresPlainStringContent?: boolean;
/**
* Anthropic-compatible providers that omit the required `signature` field
* from streamed thinking block starts. The passthrough stream adds only an
* empty placeholder; later provider `signature_delta` events remain intact.
*/
ensureThinkingSignature?: boolean;
/**
* Protocolos alternativos que este provedor aceita (ex.: um endpoint
* Anthropic-compatible alem do OpenAI-compatible padrao). A conexao escolhe

View File

@@ -21,6 +21,7 @@ import {
appendBoundedText,
buildSyntheticChatChunk,
hasActiveDeltaValue,
injectThinkingSignature,
} from "./streamHelpers.ts";
import { calculateCost } from "@/lib/usage/costCalculator";
import { buildOmniRouteSseMetadataComment } from "@/domain/omnirouteResponseMeta";
@@ -711,11 +712,9 @@ export function createSSEStream(options: StreamOptions = {}) {
}
// Drop internal commentary-phase Responses output before forwarding (#6199).
// Explicit option wins; otherwise read the feature flag (default on). Resolved
// once per stream — never on the hot per-chunk path.
// Explicit option wins; otherwise read the feature flag (default on) — resolved once per stream.
const shouldDropResponsesCommentary =
dropResponsesCommentary ?? isFeatureFlagEnabled("RESPONSES_PASSTHROUGH_DROP_COMMENTARY");
const clientExpectsResponsesStream =
(mode === STREAM_MODE.PASSTHROUGH
? clientResponseFormat === FORMATS.OPENAI_RESPONSES
@@ -1608,6 +1607,7 @@ export function createSSEStream(options: StreamOptions = {}) {
}
} else if (isClaudeSSE) {
// Claude SSE: extract usage, track content, forward as-is
const thinkingSignatureInjected = injectThinkingSignature(parsed, provider);
const extracted = extractUsage(parsed);
if (extracted) {
// Non-destructive merge: never overwrite a positive value with 0
@@ -1649,7 +1649,7 @@ export function createSSEStream(options: StreamOptions = {}) {
parsed.delta.thinking
);
}
if (restoredToolName) {
if (restoredToolName || thinkingSignatureInjected) {
output = `data: ${JSON.stringify(parsed)}\n\n`;
injectedUsage = true;
}

View File

@@ -13,6 +13,7 @@
import { FORMATS } from "../translator/formats.ts";
import { hasAnyReasoningSignal } from "./reasoningFields.ts";
import { getRegistryEntry } from "../config/providerRegistry.ts";
type SSEPayloadOptions = {
eventType?: string;
@@ -523,3 +524,24 @@ export function hasActiveDeltaValue(value: unknown): boolean {
}
return value !== null && value !== undefined;
}
// Claude SSE content_block_start normalization for providers (e.g. MiniMax) whose thinking
// blocks omit `signature` on the opening event. Strict Anthropic Messages clients deserialize
// this field before a later signature_delta arrives — inject only the empty envelope
// placeholder, never synthesize/replace a provider-supplied signature.
export function injectThinkingSignature(
parsed: { type?: string; content_block?: { type?: string; signature?: string } },
provider: string | null
): boolean {
if (
provider !== null &&
getRegistryEntry(provider)?.ensureThinkingSignature === true &&
parsed.type === "content_block_start" &&
parsed.content_block?.type === "thinking" &&
parsed.content_block.signature === undefined
) {
parsed.content_block.signature = "";
return true;
}
return false;
}

View File

@@ -0,0 +1,108 @@
import test from "node:test";
import assert from "node:assert/strict";
import { FORMATS } from "../../open-sse/translator/formats.ts";
import { getRegistryEntry } from "../../open-sse/config/providerRegistry.ts";
import { createPassthroughStreamWithLogger } from "../../open-sse/utils/stream.ts";
const encoder = new TextEncoder();
async function runPassthrough(provider: string, input: string, chunkSize = input.length) {
const source = new ReadableStream<Uint8Array>({
start(controller) {
for (let index = 0; index < input.length; index += chunkSize) {
controller.enqueue(encoder.encode(input.slice(index, index + chunkSize)));
}
controller.close();
},
});
return new Response(
source.pipeThrough(
createPassthroughStreamWithLogger(
provider,
null,
null,
"MiniMax-M2.7",
"minimax-thinking-signature-2706",
{ messages: [] },
null,
null,
null,
FORMATS.CLAUDE
)
)
).text();
}
function parseDataEvents(raw: string): Record<string, unknown>[] {
return raw
.split("\n")
.filter((line) => line.startsWith("data: "))
.map((line) => line.slice(6))
.filter((payload) => payload && payload !== "[DONE]")
.map((payload) => JSON.parse(payload) as Record<string, unknown>);
}
function thinkingStart(signature?: string) {
return {
type: "content_block_start",
index: 0,
content_block: {
type: "thinking",
thinking: "",
...(signature === undefined ? {} : { signature }),
},
};
}
test("MiniMax registries opt into thinking signature normalization", () => {
assert.equal(getRegistryEntry("minimax")?.ensureThinkingSignature, true);
assert.equal(getRegistryEntry("minimax-cn")?.ensureThinkingSignature, true);
});
test("unsigned MiniMax thinking block starts receive an empty signature", async () => {
const event = thinkingStart();
const output = await runPassthrough(
"minimax",
`event: content_block_start\ndata: ${JSON.stringify(event)}\n\n`
);
const firstEvent = parseDataEvents(output)[0];
assert.equal((firstEvent.content_block as Record<string, unknown>).signature, "");
});
test("fragmented MiniMax streams preserve real signatures and later signature deltas", async () => {
const start = thinkingStart();
const signatureDelta = {
type: "content_block_delta",
index: 0,
delta: { type: "signature_delta", signature: "minimax-signature" },
};
const input =
`event: content_block_start\ndata: ${JSON.stringify(start)}\n\n` +
`event: content_block_delta\ndata: ${JSON.stringify(signatureDelta)}\n\n`;
const events = parseDataEvents(await runPassthrough("minimax-cn", input, 7));
assert.equal((events[0].content_block as Record<string, unknown>).signature, "");
assert.deepEqual(events[1].delta, signatureDelta.delta);
const existing = parseDataEvents(
await runPassthrough(
"minimax",
`event: content_block_start\ndata: ${JSON.stringify(thinkingStart("real-signature"))}\n\n`
)
)[0];
assert.equal((existing.content_block as Record<string, unknown>).signature, "real-signature");
});
test("unrelated providers do not receive the MiniMax signature placeholder", async () => {
const events = parseDataEvents(
await runPassthrough(
"deepseek",
`event: content_block_start\ndata: ${JSON.stringify(thinkingStart())}\n\n`
)
);
assert.equal("signature" in (events[0].content_block as Record<string, unknown>), false);
});