// ThinkingSignatureDetector(veridrop detectors/thinking_signature.py)⭐:thinking 块带的 signature 是 Anthropic 服务端 // 生成的加密产物,中转站伪造不了。 // 100 有 thinking(或 redacted_thinking)块,签名非空且够长 // 70 有块、有签名但短得可疑 // 30 有块、没签名 // 0 没有 thinking 块 // 只支持 adaptive 的型号(Opus 4.7 起、本移植加的 Claude 5 系列)发 {type: adaptive, display: summarized} + output_config.effort; // 两种都支持的(Sonnet 4.6)优先 extended(budget_tokens 好控成本)。都用非流式(原仓库实测流式 + adaptive 会丢 thinking 块)。 import { lookupModel, normalizeModelId } from "../config.js"; import type { DetectorResult } from "../models.js"; import { ActiveDetector, errText, type DetectClient } from "./base.js"; /** 多步辗转相除:难到 adaptive 会决定思考;数字故意不用官方文档那组 1071 / 462。 */ const PROBE_PROMPT = "Find the greatest common divisor of 2378 and 1547 using the Euclidean algorithm."; const THINKING_BUDGET_TOKENS = 2000; const MAX_TOKENS = 16000; const SIGNATURE_MIN_LEN = 50; export function adaptiveEffortForModel(model: string): string { const n = normalizeModelId(model); if (n.startsWith("claude-opus-4-7") || n.startsWith("claude-opus-4-8")) return "xhigh"; return "high"; } export class ThinkingSignatureDetector extends ActiveDetector { readonly name = "thinking_signature"; readonly displayName = "思维签名验证"; readonly weight = 25; override appliesTo(model: string): boolean { const info = lookupModel(model); if (info === null) return false; return info.supportsExtendedThinking || info.supportsAdaptiveThinking; } async run(client: DetectClient, model: string): Promise { const info = lookupModel(model); if (info === null) return this.skip("unknown model"); let thinking: Record; const extra: Record = {}; if (info.supportsExtendedThinking) { thinking = { type: "enabled", budget_tokens: THINKING_BUDGET_TOKENS }; } else if (info.supportsAdaptiveThinking) { thinking = { type: "adaptive", display: "summarized" }; extra["output_config"] = { effort: adaptiveEffortForModel(model) }; } else { return this.skip("model lacks thinking support"); } let resp: Record; try { ({ resp } = await client.messagesCreate({ model, max_tokens: MAX_TOKENS, thinking, messages: [{ role: "user", content: PROBE_PROMPT }], ...extra, })); } catch (e) { return this.result( "error", 0, { thinking_params: thinking, output_config_sent: extra["output_config"] ?? null }, null, errText(e), ); } let seen = false; let blockType: string | null = null; let signature = ""; let thinkingChars = 0; const typesSeen: string[] = []; const content = Array.isArray(resp["content"]) ? (resp["content"] as unknown[]) : []; for (const block of content) { if (!block || typeof block !== "object" || Array.isArray(block)) continue; const b = block as Record; const t = b["type"]; if (typeof t === "string") typesSeen.push(t); if (t === "thinking" || t === "redacted_thinking") { seen = true; blockType = t; const sig = b["signature"]; if (typeof sig === "string" && !signature) signature = sig; const text = b["thinking"]; if (typeof text === "string") thinkingChars += Array.from(text).length; } } const details: Record = { thinking_params: thinking, output_config_sent: extra["output_config"] ?? null, content_block_types_seen: typesSeen, thinking_block_seen: seen, thinking_block_type: blockType, thinking_text_chars: thinkingChars, signature_received: Boolean(signature), signature_length: Array.from(signature).length, signature_prefix: signature ? Array.from(signature).slice(0, 32).join("") : "", stop_reason: resp["stop_reason"] ?? null, }; let score: number; let note: string; if (!seen) [score, note] = [0, "no_thinking_block"]; else if (!signature) [score, note] = [30, "thinking_block_but_no_signature"]; else if (Array.from(signature).length < SIGNATURE_MIN_LEN) [score, note] = [70, "signature_too_short"]; else [score, note] = [100, "ok"]; details["evaluation"] = note; return this.result(score >= 70 ? "pass" : "fail", score, details); } }