- claude: handle DeepSeek thinking blocks defensively, unsigned placeholder; fix kept-vs-seen thinking detection - gemini: clamp unsupported max/xhigh thinking levels to high - testUtils: probe Cloud Code Assist for gemini-cli/antigravity with 401 refresh retry - tests: add translator regression coverage Co-authored-by: Cursor <cursoragent@cursor.com>
272 lines
11 KiB
JavaScript
272 lines
11 KiB
JavaScript
// Unified thinking normalization: extract client intent → apply provider-native format.
|
|
// Config-driven: thinking format/limits come from capabilities.js + registry transport,
|
|
// never hardcoded per-model here. See .docs/thinking/plan.md MATRIX VI-A.
|
|
|
|
import { getCapabilitiesForModel } from "../../providers/capabilities.js";
|
|
import { PROVIDERS } from "../../providers/index.js";
|
|
import { LEVEL_TO_BUDGET, budgetToLevel, effortToBudget, effortToThinkingLevel } from "./thinking.js";
|
|
|
|
// Map a target wire-format to its native thinking format (when capability has none).
|
|
const FORMAT_TO_NATIVE = {
|
|
openai: "openai",
|
|
"openai-responses": "openai",
|
|
"openai-response": "openai",
|
|
codex: "openai",
|
|
claude: "claude-budget",
|
|
gemini: "gemini-budget",
|
|
"gemini-cli": "gemini-budget",
|
|
vertex: "gemini-budget",
|
|
antigravity: "gemini-budget",
|
|
kiro: "kiro",
|
|
};
|
|
|
|
// Parse model-name suffix "model(value)" → { cleanModel, override }.
|
|
// value: level name (high) | number (8192) | auto | none. null override when absent.
|
|
export function parseSuffix(model) {
|
|
if (typeof model !== "string") return { cleanModel: model, override: null };
|
|
const m = model.match(/^(.*)\(([^()]+)\)\s*$/);
|
|
if (!m) return { cleanModel: model, override: null };
|
|
const cleanModel = m[1].trim();
|
|
const raw = m[2].trim().toLowerCase();
|
|
if (raw === "none" || raw === "off") return { cleanModel, override: { mode: "none" } };
|
|
if (raw === "auto") return { cleanModel, override: { mode: "auto" } };
|
|
if (/^\d+$/.test(raw)) return { cleanModel, override: { mode: "budget", budget: Number(raw) } };
|
|
if (LEVEL_TO_BUDGET[raw] !== undefined) return { cleanModel, override: { mode: "level", level: raw } };
|
|
return { cleanModel, override: null };
|
|
}
|
|
|
|
// Extract unified thinking intent from a request body (post-translation, mixed shapes).
|
|
// Returns { mode, budget?, level? } or null when no thinking intent present.
|
|
export function extractThinking(body) {
|
|
if (!body || typeof body !== "object") return null;
|
|
|
|
// Claude output_config.effort (explicit) — priority over adaptive thinking
|
|
const oc = body.output_config?.effort;
|
|
if (typeof oc === "string" && oc) {
|
|
const e = oc.toLowerCase();
|
|
if (e === "none" || e === "off") return { mode: "none" };
|
|
if (e === "auto") return { mode: "auto" };
|
|
return { mode: "level", level: e };
|
|
}
|
|
|
|
// Claude shape
|
|
const t = body.thinking;
|
|
if (t && typeof t === "object") {
|
|
if (t.type === "disabled") return { mode: "none" };
|
|
if (t.type === "adaptive" || t.type === "enabled") {
|
|
const budget = Number(t.budget_tokens);
|
|
if (Number.isFinite(budget) && budget > 0) return { mode: "budget", budget };
|
|
return { mode: "auto" };
|
|
}
|
|
}
|
|
|
|
// OpenAI chat / Responses shape
|
|
const effort = body.reasoning_effort ?? (typeof body.reasoning === "object" ? body.reasoning?.effort : null);
|
|
if (typeof effort === "string" && effort) {
|
|
const e = effort.toLowerCase();
|
|
if (e === "none" || e === "off") return { mode: "none" };
|
|
if (e === "auto") return { mode: "auto" };
|
|
return { mode: "level", level: e };
|
|
}
|
|
|
|
// Gemini shape (top-level, generationConfig, or request envelope)
|
|
const tc = body.thinkingConfig || body.generationConfig?.thinkingConfig || body.request?.generationConfig?.thinkingConfig;
|
|
if (tc && typeof tc === "object") {
|
|
if (typeof tc.thinkingLevel === "string") return { mode: "level", level: tc.thinkingLevel.toLowerCase() };
|
|
const tb = Number(tc.thinkingBudget);
|
|
if (Number.isFinite(tb)) {
|
|
if (tb === 0) return { mode: "none" };
|
|
if (tb < 0) return { mode: "auto" };
|
|
return { mode: "budget", budget: tb };
|
|
}
|
|
}
|
|
|
|
// Qwen shape
|
|
if (body.enable_thinking === false) return { mode: "none" };
|
|
if (body.enable_thinking === true) {
|
|
const tb = Number(body.thinking_budget);
|
|
if (Number.isFinite(tb) && tb > 0) return { mode: "budget", budget: tb };
|
|
return { mode: "auto" };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Capture thinking intent from a body. Alias of extractThinking, named for clarity
|
|
// at the call-site where intent is snapshotted before format translation.
|
|
export const captureThinking = extractThinking;
|
|
|
|
// Resolve thinking format: provider override > capability > derive(targetFormat).
|
|
function resolveFormat(targetFormat, model, provider) {
|
|
const providerFmt = provider ? PROVIDERS[provider]?.thinkingFormat : null;
|
|
if (providerFmt) return providerFmt;
|
|
const caps = getCapabilitiesForModel(provider, model);
|
|
if (caps.thinkingFormat) return caps.thinkingFormat;
|
|
return FORMAT_TO_NATIVE[targetFormat] || "openai";
|
|
}
|
|
|
|
// Convert unified config to a budget number (for budget-based formats).
|
|
function toBudget(cfg, range) {
|
|
let budget;
|
|
if (cfg.mode === "budget") budget = cfg.budget;
|
|
else if (cfg.mode === "level") budget = effortToBudget(cfg.level);
|
|
else if (cfg.mode === "auto") return -1;
|
|
if (!Number.isFinite(budget)) return undefined;
|
|
if (range) {
|
|
if (range.min != null && budget < range.min) budget = range.min;
|
|
if (range.max != null && budget > range.max) budget = range.max;
|
|
}
|
|
return budget;
|
|
}
|
|
|
|
// Convert unified config to a discrete level string.
|
|
function toLevel(cfg) {
|
|
if (cfg.mode === "level") return cfg.level;
|
|
if (cfg.mode === "budget") return budgetToLevel(cfg.budget) || "medium";
|
|
if (cfg.mode === "auto") return "auto";
|
|
return null;
|
|
}
|
|
|
|
function toGeminiThinkingLevel(cfg) {
|
|
const raw = cfg.mode === "auto" ? "high" : (toLevel(cfg) || "high");
|
|
return effortToThinkingLevel(raw);
|
|
}
|
|
|
|
// Gemini nests thinkingConfig under generationConfig. gemini-cli / antigravity wrap
|
|
// the whole request in a { request: { generationConfig } } envelope — target the
|
|
// envelope's generationConfig when present, else the top-level one.
|
|
function setGeminiThinking(body, tc) {
|
|
const gc = body.request?.generationConfig
|
|
? body.request.generationConfig
|
|
: (body.generationConfig && typeof body.generationConfig === "object"
|
|
? body.generationConfig
|
|
: (body.generationConfig = {}));
|
|
gc.thinkingConfig = tc;
|
|
}
|
|
|
|
// Strip every known thinking field from a body (used before re-applying / when unsupported).
|
|
function stripAll(body) {
|
|
delete body.thinking;
|
|
delete body.reasoning_effort;
|
|
delete body.reasoning;
|
|
delete body.thinkingConfig;
|
|
delete body.enable_thinking;
|
|
delete body.thinking_budget;
|
|
delete body.output_config;
|
|
if (body.generationConfig) delete body.generationConfig.thinkingConfig;
|
|
if (body.request?.generationConfig) delete body.request.generationConfig.thinkingConfig;
|
|
}
|
|
|
|
// Apply unified thinking config to body in the resolved provider-native format.
|
|
function applyFormat(fmt, body, cfg, caps) {
|
|
const none = cfg.mode === "none";
|
|
const canDisable = caps.thinkingCanDisable !== false;
|
|
// Model cannot disable thinking → clamp "none" to minimal effort instead.
|
|
const eff = none && !canDisable ? { mode: "level", level: "minimal" } : cfg;
|
|
|
|
switch (fmt) {
|
|
case "openai": {
|
|
if (none && canDisable) { body.reasoning_effort = "none"; break; }
|
|
const level = toLevel(eff);
|
|
if (level) body.reasoning_effort = level === "xhigh" || level === "max" ? "high" : level;
|
|
break;
|
|
}
|
|
case "claude-adaptive": {
|
|
if (none && canDisable) { body.thinking = { type: "disabled" }; break; }
|
|
const level = toLevel(eff);
|
|
body.output_config = { effort: level === "xhigh" ? "high" : level };
|
|
break;
|
|
}
|
|
case "claude-budget": {
|
|
if (none && canDisable) { body.thinking = { type: "disabled" }; break; }
|
|
const budget = toBudget(eff, caps.thinkingRange);
|
|
body.thinking = budget === -1 ? { type: "enabled" } : { type: "enabled", budget_tokens: budget || 8192 };
|
|
break;
|
|
}
|
|
case "gemini-level": {
|
|
const level = none ? "minimal" : toGeminiThinkingLevel(eff);
|
|
setGeminiThinking(body, { thinkingLevel: level, includeThoughts: level !== "minimal" });
|
|
break;
|
|
}
|
|
case "gemini-budget": {
|
|
if (none && canDisable) { setGeminiThinking(body, { thinkingBudget: 0, includeThoughts: false }); break; }
|
|
const budget = toBudget(eff, caps.thinkingRange);
|
|
setGeminiThinking(body, { thinkingBudget: budget ?? -1, includeThoughts: true });
|
|
break;
|
|
}
|
|
case "zai": {
|
|
// Z.ai ignores thinking.disabled → must use enable_thinking:false to turn off.
|
|
if (none && canDisable) { body.enable_thinking = false; delete body.thinking; break; }
|
|
body.thinking = { type: "enabled" };
|
|
break;
|
|
}
|
|
case "qwen": {
|
|
if (none && canDisable) { body.enable_thinking = false; break; }
|
|
body.enable_thinking = true;
|
|
const budget = toBudget(eff, caps.thinkingRange);
|
|
if (Number.isFinite(budget) && budget > 0) body.thinking_budget = budget;
|
|
break;
|
|
}
|
|
case "deepseek": {
|
|
if (none && canDisable) { body.thinking = { type: "disabled" }; break; }
|
|
body.thinking = { type: "enabled" };
|
|
// DeepSeek: low/medium→high, xhigh/max→max.
|
|
const level = toLevel(eff);
|
|
body.reasoning_effort = level === "xhigh" || level === "max" ? "max" : "high";
|
|
break;
|
|
}
|
|
case "kimi": {
|
|
if (none && canDisable) { body.thinking = { type: "disabled" }; break; }
|
|
const level = toLevel(eff);
|
|
if (level) body.reasoning_effort = level === "max" ? "high" : level;
|
|
break;
|
|
}
|
|
case "minimax": {
|
|
// M3 adaptive; M2.x cannot disable (handled via canDisable clamp).
|
|
body.thinking = { type: none && canDisable ? "disabled" : "adaptive" };
|
|
break;
|
|
}
|
|
case "hunyuan": {
|
|
if (none && canDisable) { body.thinking = { type: "disabled" }; break; }
|
|
const budget = toBudget(eff, caps.thinkingRange);
|
|
body.thinking = budget === -1 ? { type: "enabled" } : { type: "enabled", budget_tokens: budget || 8192 };
|
|
break;
|
|
}
|
|
case "step": {
|
|
if (none && canDisable) break;
|
|
const level = toLevel(eff);
|
|
if (level) body.reasoning_effort = level === "xhigh" || level === "max" ? "high" : level;
|
|
break;
|
|
}
|
|
case "kiro":
|
|
// Kiro thinking handled via system-tag injection in openai-to-kiro.js; no body field here.
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Public entry: normalize thinking for the resolved target format.
|
|
// Mutates and returns body. No-op when model has no reasoning capability.
|
|
// `intent` is a pre-captured config (from captureThinking on the original body);
|
|
// falls back to extracting from the current body when omitted.
|
|
export function applyThinking(targetFormat, model, body, provider = null, intent = undefined) {
|
|
if (!body || typeof body !== "object") return body;
|
|
|
|
const { cleanModel, override } = parseSuffix(model);
|
|
const cfg = override || intent || extractThinking(body);
|
|
const caps = getCapabilitiesForModel(provider, cleanModel);
|
|
|
|
// Model cannot reason → strip any stray thinking fields.
|
|
if (!caps.reasoning) {
|
|
stripAll(body);
|
|
return body;
|
|
}
|
|
if (!cfg) return body;
|
|
|
|
const fmt = resolveFormat(targetFormat, cleanModel, provider);
|
|
stripAll(body);
|
|
applyFormat(fmt, body, cfg, caps);
|
|
return body;
|
|
}
|