// 权重、档位 → 检测项、型号表:对应 veridrop protocols/anthropic/config.py。 // 偏差:型号表补了 Claude 5 系列(原仓库 2026-09-20 之后没更新,不认识它们,thinking / PDF 两项会被跳过)。 import type { Mode } from "./models.js"; export const DETECTOR_WEIGHTS: Record = { identity: 5, behavioral_signature: 15, thinking_signature: 25, consistency: 10, knowledge: 10, pdf: 8, structured_output: 12, protocol: 5, integrity: 5, token_usage: 10, message_id: 5, long_context: 15, }; export const MODE_DETECTORS: Record> = { quick: new Set(["identity", "thinking_signature", "consistency", "protocol", "message_id"]), standard: new Set([ "identity", "thinking_signature", "consistency", "knowledge", "structured_output", "protocol", "integrity", "token_usage", "message_id", ]), full: new Set(Object.keys(DETECTOR_WEIGHTS)), }; export interface ModelInfo { alias: string; aliases: readonly string[]; contextTokens: number; maxOutputTokens: number; pdfPageMax: number; supportsExtendedThinking: boolean; supportsAdaptiveThinking: boolean; newTokenizer: boolean; deprecated: boolean; } function m( alias: string, contextTokens: number, maxOutputTokens: number, pdfPageMax: number, supportsExtendedThinking: boolean, supportsAdaptiveThinking: boolean, newTokenizer = false, ): ModelInfo { return { alias, aliases: [alias], contextTokens, maxOutputTokens, pdfPageMax, supportsExtendedThinking, supportsAdaptiveThinking, newTokenizer, deprecated: false, }; } /** * 顺序有讲究:lookupModel 是双向前缀匹配,按这里的顺序取第一个命中的。 * 所以更具体的别名排在前面(claude-opus-5-5 在 claude-opus-5 之前,claude-fable-5-1 在 claude-fable-5 之前)。 */ export const MODELS: readonly ModelInfo[] = [ // ── Claude 5 系列(本移植新加):1M 上下文、128K 输出、只有 adaptive 思考、Opus 4.7 起的新分词器 ── m("claude-fable-5-1", 1_000_000, 128_000, 600, false, true, true), m("claude-fable-5", 1_000_000, 128_000, 600, false, true, true), m("claude-opus-5-5", 1_000_000, 128_000, 600, false, true, true), m("claude-opus-5", 1_000_000, 128_000, 600, false, true, true), m("claude-sonnet-5-5", 1_000_000, 128_000, 600, false, true, true), m("claude-sonnet-5", 1_000_000, 128_000, 600, false, true, true), m("claude-haiku-5-5", 1_000_000, 128_000, 600, false, true, true), // ── 原仓库的表,原样 ── m("claude-opus-4-8", 1_000_000, 128_000, 600, false, true, true), m("claude-opus-4-7", 1_000_000, 128_000, 600, false, true, true), m("claude-sonnet-4-6", 1_000_000, 64_000, 600, true, true), m("claude-haiku-4-5", 200_000, 64_000, 100, true, false), m("claude-opus-4-6", 1_000_000, 128_000, 600, true, false), m("claude-sonnet-4-5", 200_000, 64_000, 100, true, false), m("claude-opus-4-5", 200_000, 64_000, 100, true, false), m("claude-opus-4-1", 200_000, 32_000, 100, true, false), ]; /** `claude-sonnet-4.5` ≡ `claude-sonnet-4-5`。 */ export function normalizeModelId(id: string): string { return id.replaceAll(".", "-").replaceAll("_", "-"); } /** * 双向前缀匹配:别名与带日期的快照名都认。 * 分两遍:先找「输入以别名开头」的(claude-opus-5 → claude-opus-5,不会被 claude-opus-5-5 抢走),再找「别名以输入开头」的。 * 原仓库是一遍里两个方向一起判;它的表里没有哪个别名是另一个的前缀,所以对原有型号两种写法结果完全一样。 * 本移植加的 claude-opus-5 / claude-opus-5-5 这类成对的别名要分两遍才分得开(偏差 1 的一部分)。 */ export function lookupModel(modelId: string): ModelInfo | null { const nid = normalizeModelId(modelId); for (const info of MODELS) { if (info.aliases.some((a) => nid.startsWith(normalizeModelId(a)))) return info; } for (const info of MODELS) { if (info.aliases.some((a) => normalizeModelId(a).startsWith(nid))) return info; } return null; } export function modelsMatch(requestModel: string, responseModel: string): boolean { if (!requestModel || !responseModel) return false; const a = normalizeModelId(requestModel); const b = normalizeModelId(responseModel); return b.startsWith(a) || a.startsWith(b); } /** * 请求体里要剥掉的参数(client.py PARAM_DEPRECATIONS)。原仓库只有 Opus 4.7 / 4.8 剥 temperature; * Claude 5 系列与 Fable 对 temperature / top_p / top_k 一律回 400,这里一并剥掉(偏差 2)。 */ export const PARAM_DEPRECATIONS: ReadonlyArray = [ ["claude-opus-4-7", ["temperature"]], ["claude-opus-4-8", ["temperature"]], ["claude-fable-", ["temperature", "top_p", "top_k"]], ["claude-opus-5", ["temperature", "top_p", "top_k"]], ["claude-sonnet-5", ["temperature", "top_p", "top_k"]], ["claude-haiku-5", ["temperature", "top_p", "top_k"]], ]; /** * 不收强制工具调用(tool_choice any / tool)的型号:官方 API 对它们回 400。structured_output 对这些型号改发 auto * (提示本身就在要天气,模型照样会调工具)。原仓库没有这一条(偏差 5)。 */ const NO_FORCED_TOOL_CHOICE = ["claude-fable-5-1", "claude-mythos-5-1", "claude-opus-5-5", "claude-sonnet-5-5"]; export function rejectsForcedToolChoice(model: string): boolean { const n = normalizeModelId(model); return NO_FORCED_TOOL_CHOICE.some((p) => n.startsWith(p)); }