mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
fix(translator): ESM-safe registry + tool-id pairing + responses max_tokens; add real-creds tests
- translator/index.js: replace require() with static side-effect imports (ESM-safe), lazy-init registry maps to survive circular import order - openai-responses->openai: map max_output_tokens -> max_tokens (avoid leaking field upstream) - gemini/antigravity -> openai: derive deterministic tool_call id from name so functionCall/functionResponse pair correctly (fixes provider tool-pairing 400s) - add offline unit tests (finish-reason, usage, session-manager, ollama malformed args, const guard) - add real-creds integration tests (provider-cases + all-formats matrix: 6 inbound formats x 4 scenarios) Includes co-located provider registry refactor (pricing/capabilities/media providers) and sessionManager updates. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
// Model capabilities — what each model can read/do beyond plain text.
|
||||
//
|
||||
// Fallback order (first match wins), result merged over DEFAULT_CAPABILITIES:
|
||||
// 1. PROVIDER_CAPABILITIES[provider][model] — provider-specific override
|
||||
// 2. MODEL_CAPABILITIES[model] — canonical exact id (handles exceptions)
|
||||
// 3. PATTERN_CAPABILITIES — glob match, ordered specific -> generic
|
||||
// 4. DEFAULT_CAPABILITIES — safe floor (always returned)
|
||||
//
|
||||
// ── HOW TO ADD / UPDATE A MODEL ──────────────────────────────────────
|
||||
// Authoritative data source: https://models.dev/api.json (145 providers, 4000+
|
||||
// models, MIT). Each model exposes the exact fields we map below:
|
||||
// modalities.input ["text","image","pdf","audio","video"] -> vision / pdf / audioInput / videoInput
|
||||
// modalities.output ["text","image","audio"] -> imageOutput / audioOutput
|
||||
// reasoning -> reasoning tool_call -> tools
|
||||
// limit.context -> contextWindow limit.output -> maxOutput
|
||||
// Look up the model id, then:
|
||||
// • If a PATTERN below already covers it correctly -> nothing to do.
|
||||
// • If it is an exception (pattern would mis-match) -> add an exact entry to
|
||||
// MODEL_CAPABILITIES (only the fields that differ from DEFAULT).
|
||||
// • If a whole new family -> add an ordered PATTERN (specific before generic).
|
||||
// NOTE: models.dev has NO "search" flag (web search is a runtime tool, not a
|
||||
// model spec); set `search` from vendor docs (Claude 4.x+, GPT-5.x/4o, Gemini
|
||||
// 2.0+, Grok, Perplexity). Verify with: curl -s https://models.dev/api.json
|
||||
|
||||
import { matchPattern } from "./pricing.js";
|
||||
|
||||
/**
|
||||
* Safe floor — every resolved result is merged over this so consumers
|
||||
* never need null-checks. Most modern LLMs meet these limits.
|
||||
*/
|
||||
export const DEFAULT_CAPABILITIES = {
|
||||
// input modalities
|
||||
vision: false, // read images
|
||||
pdf: false, // read PDF / documents
|
||||
audioInput: false, // read audio
|
||||
videoInput: false, // read video
|
||||
// output modalities
|
||||
imageOutput: false, // generate images
|
||||
audioOutput: false, // generate audio
|
||||
// features
|
||||
search: false, // built-in web search tool / grounding
|
||||
tools: true, // function / tool calling
|
||||
reasoning: false, // thinking / reasoning
|
||||
// limits (tokens)
|
||||
contextWindow: 200000,
|
||||
maxOutput: 64000,
|
||||
};
|
||||
|
||||
/**
|
||||
* Canonical exact-id overrides — used for exceptions that patterns would
|
||||
* otherwise mis-match. Only declare deltas vs DEFAULT.
|
||||
*/
|
||||
export const MODEL_CAPABILITIES = {
|
||||
// Claude 4.6/4.7 have 1M context (override generic claude pattern at 200k)
|
||||
"claude-opus-4.6": { vision: true, reasoning: true, search: true, contextWindow: 1000000, maxOutput: 128000 },
|
||||
"claude-opus-4.7": { vision: true, reasoning: true, search: true, contextWindow: 1000000, maxOutput: 128000 },
|
||||
"claude-opus-4-6": { vision: true, reasoning: true, search: true, contextWindow: 1000000, maxOutput: 128000 },
|
||||
"claude-sonnet-4.6": { vision: true, reasoning: true, search: true, contextWindow: 1000000, maxOutput: 64000 },
|
||||
"claude-sonnet-4-6": { vision: true, reasoning: true, search: true, contextWindow: 1000000, maxOutput: 64000 },
|
||||
|
||||
// Gemini image-gen / OpenAI image / xai image variants
|
||||
"gpt-image-1": { imageOutput: true, tools: false },
|
||||
|
||||
// GLM vision variant (text GLM has no vision)
|
||||
"glm-4.6v": { vision: true, reasoning: true, contextWindow: 128000 },
|
||||
|
||||
// Qwen plain coder/text (no vision) — registry "vision-model" / "coder-model" aliases
|
||||
"vision-model": { vision: true, reasoning: true, contextWindow: 1000000 },
|
||||
"coder-model": { reasoning: true, contextWindow: 1000000 },
|
||||
};
|
||||
|
||||
/**
|
||||
* Provider-specific capability overrides. Keyed by provider alias/id.
|
||||
*/
|
||||
export const PROVIDER_CAPABILITIES = {};
|
||||
|
||||
/**
|
||||
* Pattern fallback — glob (* = wildcard), matched case-insensitively and
|
||||
* anchored (^...$) so a pattern must match the full model id. ORDER MATTERS:
|
||||
* vision/specific variants first, text-only/generic families last, to avoid
|
||||
* a broad family pattern swallowing an exception (e.g. glm-4.6v vs glm-5).
|
||||
*/
|
||||
export const PATTERN_CAPABILITIES = [
|
||||
// ── Claude (4.x+ = vision + thinking + web search) ───────────────
|
||||
{ pattern: "*claude*opus*", caps: { vision: true, reasoning: true, search: true } },
|
||||
{ pattern: "*claude*sonnet*", caps: { vision: true, reasoning: true, search: true } },
|
||||
{ pattern: "*claude*haiku*", caps: { vision: true, reasoning: true, search: true } },
|
||||
{ pattern: "*claude*fable*", caps: { vision: true, reasoning: true, search: true, contextWindow: 1000000, maxOutput: 128000 } },
|
||||
{ pattern: "*claude*mythos*", caps: { vision: true, reasoning: true, search: true, contextWindow: 1000000, maxOutput: 128000 } },
|
||||
{ pattern: "*claude-3*", caps: { vision: true } },
|
||||
{ pattern: "*claude*", caps: { vision: true, reasoning: true, search: true } },
|
||||
|
||||
// ── Gemini (all 2.0+ multimodal + google_search grounding, 1M ctx) ─
|
||||
{ pattern: "*gemini*image*", caps: { vision: true, imageOutput: true, contextWindow: 1048576 } },
|
||||
{ pattern: "*gemini-3*pro*", caps: { vision: true, audioInput: true, videoInput: true, reasoning: true, search: true, contextWindow: 1048576, maxOutput: 65535 } },
|
||||
{ pattern: "*gemini-3*", caps: { vision: true, audioInput: true, videoInput: true, search: true, contextWindow: 1048576, maxOutput: 65536 } },
|
||||
{ pattern: "*gemini-2*", caps: { vision: true, audioInput: true, videoInput: true, search: true, contextWindow: 1048576, maxOutput: 65536 } },
|
||||
{ pattern: "*gemini*", caps: { vision: true, search: true, contextWindow: 1048576 } },
|
||||
{ pattern: "*gemma*", caps: { vision: true, contextWindow: 128000 } },
|
||||
{ pattern: "*nanobanana*", caps: { vision: true, imageOutput: true } },
|
||||
|
||||
// ── OpenAI GPT-5.x (vision + thinking + web search) ──────────────
|
||||
{ pattern: "*gpt-5*image*", caps: { imageOutput: true } },
|
||||
{ pattern: "*gpt-5*codex*", caps: { reasoning: true, search: true, contextWindow: 400000, maxOutput: 128000 } },
|
||||
{ pattern: "*gpt-5*", caps: { vision: true, reasoning: true, search: true, contextWindow: 400000, maxOutput: 128000 } },
|
||||
{ pattern: "*gpt-4o*", caps: { vision: true, search: true, contextWindow: 128000, maxOutput: 16384 } },
|
||||
{ pattern: "*gpt-4.1*", caps: { vision: true, contextWindow: 1000000, maxOutput: 32768 } },
|
||||
{ pattern: "*gpt-4-turbo*", caps: { vision: true, contextWindow: 128000 } },
|
||||
{ pattern: "*gpt-4*", caps: { contextWindow: 128000 } },
|
||||
{ pattern: "*gpt-3.5*", caps: { contextWindow: 16385, maxOutput: 4096 } },
|
||||
{ pattern: "*gpt-oss*", caps: { reasoning: true, contextWindow: 128000 } },
|
||||
|
||||
// ── OpenAI o-series (reasoning, vision) ──────────────────────────
|
||||
{ pattern: "*o1-mini*", caps: { reasoning: true, contextWindow: 128000 } },
|
||||
{ pattern: "*o1*", caps: { vision: true, reasoning: true, contextWindow: 200000, maxOutput: 100000 } },
|
||||
{ pattern: "*o3*", caps: { vision: true, reasoning: true, contextWindow: 200000, maxOutput: 100000 } },
|
||||
{ pattern: "*o4*", caps: { vision: true, reasoning: true, contextWindow: 200000, maxOutput: 100000 } },
|
||||
|
||||
// ── Grok (vision + Live Search) ──────────────────────────────────
|
||||
{ pattern: "*grok*image*", caps: { imageOutput: true } },
|
||||
{ pattern: "*grok-code*", caps: { reasoning: true, contextWindow: 256000 } },
|
||||
{ pattern: "*grok-4*", caps: { vision: true, reasoning: true, search: true, contextWindow: 256000 } },
|
||||
{ pattern: "*grok-3*", caps: { vision: true, reasoning: true, search: true, contextWindow: 131072 } },
|
||||
{ pattern: "*grok*", caps: { vision: true, reasoning: true, search: true, contextWindow: 256000 } },
|
||||
|
||||
// ── Qwen (VL = vision; max/plus = vision+1M; coder/text last) ─────
|
||||
{ pattern: "*qwen*vl*", caps: { vision: true, reasoning: true, contextWindow: 262144 } },
|
||||
{ pattern: "*qwen*max*", caps: { vision: true, reasoning: true, contextWindow: 1000000, maxOutput: 65536 } },
|
||||
{ pattern: "*qwen*plus*", caps: { vision: true, reasoning: true, contextWindow: 1000000, maxOutput: 65536 } },
|
||||
{ pattern: "*qwen*235b*", caps: { reasoning: true, contextWindow: 262144 } },
|
||||
{ pattern: "*qwen*coder*", caps: { reasoning: true, contextWindow: 1000000 } },
|
||||
{ pattern: "*qwq*", caps: { reasoning: true, contextWindow: 131072 } },
|
||||
{ pattern: "*qwen*", caps: { reasoning: true, contextWindow: 262144 } },
|
||||
|
||||
// ── Kimi (K2.x = vision + thinking, 262K) ────────────────────────
|
||||
{ pattern: "*kimi*k2*", caps: { vision: true, reasoning: true, contextWindow: 262144, maxOutput: 262144 } },
|
||||
{ pattern: "*kimi*", caps: { reasoning: true, contextWindow: 262144 } },
|
||||
|
||||
// ── GLM (4.6V vision handled by exact id; text GLM = reasoning) ───
|
||||
{ pattern: "*glm-5*", caps: { reasoning: true, contextWindow: 200000, maxOutput: 128000 } },
|
||||
{ pattern: "*glm-4.7*", caps: { reasoning: true, contextWindow: 200000, maxOutput: 128000 } },
|
||||
{ pattern: "*glm-4*", caps: { reasoning: true, contextWindow: 200000 } },
|
||||
{ pattern: "*glm*", caps: { reasoning: true, contextWindow: 200000 } },
|
||||
|
||||
// ── DeepSeek (NO vision; v4 = 1M ctx; r1/reasoner = thinking) ─────
|
||||
{ pattern: "*deepseek-v4*", caps: { reasoning: true, contextWindow: 1000000, maxOutput: 384000 } },
|
||||
{ pattern: "*reasoner*", caps: { reasoning: true, contextWindow: 128000 } },
|
||||
{ pattern: "*deepseek-r*", caps: { reasoning: true, contextWindow: 128000 } },
|
||||
{ pattern: "*deepseek*", caps: { contextWindow: 128000 } },
|
||||
|
||||
// ── MiniMax (M3 = 1M/512K; M2.x = 200K) ──────────────────────────
|
||||
{ pattern: "*minimax*image*", caps: { imageOutput: true } },
|
||||
{ pattern: "*minimax-m3*", caps: { reasoning: true, contextWindow: 1048576, maxOutput: 512000 } },
|
||||
{ pattern: "*minimax-m2.7*", caps: { reasoning: true, contextWindow: 204800, maxOutput: 131072 } },
|
||||
{ pattern: "*minimax*", caps: { reasoning: true, contextWindow: 200000, maxOutput: 131072 } },
|
||||
|
||||
// ── Xiaomi MiMo (vision, 1M / 262K ctx) ──────────────────────────
|
||||
{ pattern: "*mimo*v2.5*", caps: { vision: true, contextWindow: 1048576, maxOutput: 131072 } },
|
||||
{ pattern: "*mimo*omni*", caps: { vision: true, audioInput: true, contextWindow: 262144, maxOutput: 131072 } },
|
||||
{ pattern: "*mimo*", caps: { vision: true, contextWindow: 262144, maxOutput: 131072 } },
|
||||
|
||||
// ── Llama (4 = vision/1M; 3.x = text-only/128K) ──────────────────
|
||||
{ pattern: "*llama-4*", caps: { vision: true, contextWindow: 1000000 } },
|
||||
{ pattern: "*llama*", caps: { contextWindow: 128000 } },
|
||||
|
||||
// ── Mistral (Large 3 = vision/256K; codestral text) ──────────────
|
||||
{ pattern: "*codestral*", caps: { contextWindow: 256000 } },
|
||||
{ pattern: "*mistral-large*", caps: { vision: true, contextWindow: 256000 } },
|
||||
{ pattern: "*mistral*", caps: { contextWindow: 128000 } },
|
||||
|
||||
// ── Cohere (Command A Vision = vision; others text) ──────────────
|
||||
{ pattern: "*command-a-vision*", caps: { vision: true, contextWindow: 128000 } },
|
||||
{ pattern: "*command*", caps: { contextWindow: 128000 } },
|
||||
|
||||
// ── Perplexity (web search native) ───────────────────────────────
|
||||
{ pattern: "*sonar*", caps: { search: true, contextWindow: 128000 } },
|
||||
{ pattern: "*pplx*", caps: { search: true, contextWindow: 128000 } },
|
||||
{ pattern: "*perplexity*", caps: { search: true, contextWindow: 128000 } },
|
||||
|
||||
// ── Others ───────────────────────────────────────────────────────
|
||||
{ pattern: "*hunyuan*", caps: { reasoning: true, contextWindow: 262144, maxOutput: 262144 } },
|
||||
{ pattern: "hy3*", caps: { reasoning: true, contextWindow: 262144, maxOutput: 262144 } },
|
||||
{ pattern: "*step-*", caps: { reasoning: true, contextWindow: 128000 } },
|
||||
{ pattern: "*nemotron*", caps: { reasoning: true, contextWindow: 128000 } },
|
||||
{ pattern: "*ling-*", caps: { reasoning: true, contextWindow: 128000 } },
|
||||
];
|
||||
|
||||
/**
|
||||
* Resolve capabilities for a model using the 4-step fallback chain,
|
||||
* merged over DEFAULT_CAPABILITIES so the result is always complete.
|
||||
*
|
||||
* @param {string} provider
|
||||
* @param {string} model
|
||||
* @returns {object} full capabilities object
|
||||
*/
|
||||
export function getCapabilitiesForModel(provider, model) {
|
||||
if (!model) return { ...DEFAULT_CAPABILITIES };
|
||||
|
||||
// 1. Provider-specific override
|
||||
if (provider && PROVIDER_CAPABILITIES[provider]?.[model]) {
|
||||
return { ...DEFAULT_CAPABILITIES, ...PROVIDER_CAPABILITIES[provider][model] };
|
||||
}
|
||||
|
||||
// 2. Canonical exact (strip vendor prefix: "anthropic/claude-opus-4.7" -> "claude-opus-4.7")
|
||||
const baseModel = model.includes("/") ? model.split("/").pop() : model;
|
||||
if (MODEL_CAPABILITIES[baseModel]) return { ...DEFAULT_CAPABILITIES, ...MODEL_CAPABILITIES[baseModel] };
|
||||
if (MODEL_CAPABILITIES[model]) return { ...DEFAULT_CAPABILITIES, ...MODEL_CAPABILITIES[model] };
|
||||
|
||||
// 3. Pattern match (first match wins)
|
||||
for (const { pattern, caps } of PATTERN_CAPABILITIES) {
|
||||
if (matchPattern(pattern, baseModel) || matchPattern(pattern, model)) {
|
||||
return { ...DEFAULT_CAPABILITIES, ...caps };
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Floor
|
||||
return { ...DEFAULT_CAPABILITIES };
|
||||
}
|
||||
Reference in New Issue
Block a user