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:
decolua
2026-06-15 11:38:43 +07:00
parent 24a2d19bd7
commit aba4c45da6
76 changed files with 2115 additions and 479 deletions

View File

@@ -3,7 +3,7 @@ import { BaseExecutor } from "./base.js";
import { PROVIDERS } from "../config/providers.js";
import { OAUTH_ENDPOINTS, ANTIGRAVITY_HEADERS, INTERNAL_REQUEST_HEADER, AG_DEFAULT_TOOLS, AG_TOOL_SUFFIX } from "../config/appConstants.js";
import { HTTP_STATUS } from "../config/runtimeConfig.js";
import { deriveSessionId } from "../utils/sessionManager.js";
import { resolveSessionId } from "../utils/sessionManager.js";
import { proxyAwareFetch } from "../utils/proxyFetch.js";
import { cleanJSONSchemaForAntigravity } from "../translator/formats/gemini.js";
@@ -94,7 +94,7 @@ export class AntigravityExecutor extends BaseExecutor {
generationConfig,
...(contents && { contents }),
...(tools && { tools }),
sessionId: body.request?.sessionId || deriveSessionId(credentials?.email || credentials?.connectionId),
sessionId: body.request?.sessionId || resolveSessionId({ headers: credentials?.rawHeaders, body, connectionId: credentials?.email || credentials?.connectionId, scope: "antigravity" }),
safetySettings: undefined,
...(tools?.length > 0 && { toolConfig: { functionCallingConfig: { mode: "VALIDATED" } } })
};

View File

@@ -1,4 +1,3 @@
import { createHash } from "crypto";
import { BaseExecutor } from "./base.js";
import { CODEX_DEFAULT_INSTRUCTIONS } from "../config/codexInstructions.js";
import { PROVIDERS } from "../config/providers.js";
@@ -9,18 +8,14 @@ import {
import { normalizeResponsesInput } from "../translator/formats/responsesApi.js";
import { fetchImageAsBase64 } from "../translator/concerns/image.js";
import { getModelUpstreamId } from "../config/providerModels.js";
import { getConsistentMachineId } from "../shared/machineId.js";
import { DEFAULT_RETRY_CONFIG, resolveRetryEntry } from "../config/runtimeConfig.js";
import { dbg } from "../utils/debugLog.js";
import { resolveSessionId } from "../utils/sessionManager.js";
// SSE error patterns inside 200-OK body that should trigger retry as if 503
const CODEX_SSE_OVERLOADED_PATTERNS = ["server_is_overloaded", "service_unavailable_error"];
const CODEX_SSE_PEEK_BYTES = 4096;
// In-memory map: hash(machineId + first assistant content) → { sessionId, lastUsed }
const SESSION_TTL_MS = 60 * 60 * 1000; // 1 hour
const assistantSessionMap = new Map();
// Server-generated item id prefixes that Codex /responses cannot resolve when store=false
const SERVER_ID_PATTERN = /^(rs|fc|resp|msg)_/;
@@ -104,86 +99,17 @@ function normalizeCodexTools(body) {
}
}
// Cache machine ID at module level (resolved once)
let cachedMachineId = null;
getConsistentMachineId().then(id => { cachedMachineId = id; });
function hashContent(text) {
return createHash("sha256").update(text).digest("hex").slice(0, 16);
// Resolve prompt-cache session id: client session → assistant-text-hash → workspaceId → connection
function resolveCacheSessionId(body, credentials) {
return resolveSessionId({
headers: credentials?.rawHeaders,
body,
connectionId: credentials?.connectionId,
workspaceId: credentials?.providerSpecificData?.workspaceId,
scope: "codex"
});
}
function generateSessionId() {
return `sess_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`;
}
// Extract text content from an input item
function extractItemText(item) {
if (!item) return "";
if (typeof item.content === "string") return item.content;
if (Array.isArray(item.content)) {
return item.content.map(c => c.text || c.output || "").filter(Boolean).join("");
}
return "";
}
// Normalize a session id candidate (trim, length cap)
function normalizeSessionId(value) {
if (typeof value !== "string") return null;
const v = value.trim();
if (!v || v.length > 256) return null;
return v;
}
// Resolve prompt-cache session id with priority: body → assistant-text-hash → workspaceId → machineId
function resolveCacheSessionId(body, credentials, machineId) {
// 1. Client-provided session/conversation id (highest priority — stable per conversation)
const fromBody =
normalizeSessionId(body?.prompt_cache_key) ||
normalizeSessionId(body?.session_id) ||
normalizeSessionId(body?.conversation_id);
if (fromBody) return fromBody;
// 2. Hash accumulated assistant text (≥50 chars) — sticky session across turns
if (Array.isArray(body?.input) && body.input.length > 0) {
let text = "";
const MIN_LEN = 50;
const CAP_LEN = 200;
for (const item of body.input) {
if (item?.role !== "assistant") continue;
const t = extractItemText(item);
if (!t) continue;
text += t;
if (text.length >= CAP_LEN) break;
}
if (text.length >= MIN_LEN) {
const hash = hashContent((machineId || "") + text.slice(0, CAP_LEN));
const entry = assistantSessionMap.get(hash);
if (entry) {
entry.lastUsed = Date.now();
return entry.sessionId;
}
const sessionId = generateSessionId();
assistantSessionMap.set(hash, { sessionId, lastUsed: Date.now() });
return sessionId;
}
}
// 3. Account-wide fallback (workspaceId from connection)
const workspaceId = normalizeSessionId(credentials?.providerSpecificData?.workspaceId);
if (workspaceId) return workspaceId;
// 4. Last resort — stable per-machine id
return machineId ? `sess_${hashContent(machineId)}` : generateSessionId();
}
// Cleanup expired entries periodically
setInterval(() => {
const now = Date.now();
for (const [key, entry] of assistantSessionMap) {
if (now - entry.lastUsed > SESSION_TTL_MS) assistantSessionMap.delete(key);
}
}, 10 * 60 * 1000);
/**
* Codex Executor - handles OpenAI Codex API (Responses API format)
* Automatically injects default instructions if missing
@@ -377,7 +303,7 @@ export class CodexExecutor extends BaseExecutor {
this._isCompact = !!body._compact;
delete body._compact;
// Resolve conversation-stable session_id (priority: body → assistant-text → workspace → machine)
this._currentSessionId = resolveCacheSessionId(body, credentials, cachedMachineId);
this._currentSessionId = resolveCacheSessionId(body, credentials);
// Convert string input to array format (Codex API requires input as array)
const normalized = normalizeResponsesInput(body.input);
if (normalized) body.input = normalized;

View File

@@ -88,6 +88,9 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
const clientTool = detectClientTool(clientRawRequest?.headers || {}, body);
const passthrough = isNativePassthrough(clientTool, provider);
// Expose raw client headers to translators/executors for session-id resolution
if (credentials) credentials.rawHeaders = clientRawRequest?.headers || {};
let translatedBody;
let toolNameMap;
if (passthrough) {

View File

@@ -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 };
}

View File

@@ -0,0 +1,304 @@
// Pricing rates for AI models — all rates in $/1M tokens
//
// Fallback order (first match wins):
// 1. PROVIDER_PRICING[provider][model] — provider-specific override
// 2. MODEL_PRICING[model] — canonical model price (provider-agnostic)
// 3. PATTERN_PRICING — glob pattern match (e.g. "codex-*")
/**
* Canonical model pricing — provider-agnostic.
* Cover all known models; deduplicated across providers.
*/
export const MODEL_PRICING = {
// === Anthropic / Claude ===
"claude-opus-4-6": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 25.00, cache_creation: 6.25 },
"claude-opus-4-5-20251101": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 25.00, cache_creation: 6.25 },
"claude-sonnet-4-6": { input: 3.00, output: 15.00, cached: 0.30, reasoning: 15.00, cache_creation: 3.75 },
"claude-sonnet-4-5-20250929": { input: 3.00, output: 15.00, cached: 0.30, reasoning: 15.00, cache_creation: 3.75 },
"claude-haiku-4-5-20251001": { input: 1.00, output: 5.00, cached: 0.10, reasoning: 5.00, cache_creation: 1.25 },
"claude-sonnet-4-20250514": { input: 3.00, output: 15.00, cached: 1.50, reasoning: 15.00, cache_creation: 3.00 },
"claude-opus-4-20250514": { input: 15.00, output: 25.00, cached: 7.50, reasoning: 112.50, cache_creation: 15.00 },
"claude-3-5-sonnet-20241022": { input: 3.00, output: 15.00, cached: 1.50, reasoning: 15.00, cache_creation: 3.00 },
"claude-haiku-4.5": { input: 0.50, output: 2.50, cached: 0.05, reasoning: 3.75, cache_creation: 0.50 },
"claude-opus-4.1": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 },
"claude-opus-4.5": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 },
"claude-opus-4.6": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 },
"claude-sonnet-4": { input: 3.00, output: 15.00, cached: 0.30, reasoning: 22.50, cache_creation: 3.00 },
"claude-sonnet-4.5": { input: 3.00, output: 15.00, cached: 0.30, reasoning: 22.50, cache_creation: 3.00 },
"claude-sonnet-4.6": { input: 3.00, output: 15.00, cached: 0.30, reasoning: 22.50, cache_creation: 3.00 },
"claude-opus-4-5-thinking": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 },
"claude-opus-4-6-thinking": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 },
// === OpenAI / GPT ===
"gpt-3.5-turbo": { input: 0.50, output: 1.50, cached: 0.25, reasoning: 2.25, cache_creation: 0.50 },
"gpt-4": { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 },
"gpt-4-turbo": { input: 10.00, output: 30.00, cached: 5.00, reasoning: 45.00, cache_creation: 10.00 },
"gpt-4o": { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 },
"gpt-4o-mini": { input: 0.15, output: 0.60, cached: 0.075, reasoning: 0.90, cache_creation: 0.15 },
"gpt-4.1": { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 },
"gpt-5": { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 },
"gpt-5-mini": { input: 0.75, output: 3.00, cached: 0.375, reasoning: 4.50, cache_creation: 0.75 },
"gpt-5-codex": { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 },
"gpt-5.1": { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 },
"gpt-5.1-codex": { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 },
"gpt-5.1-codex-mini": { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 },
"gpt-5.1-codex-mini-high": { input: 2.00, output: 8.00, cached: 1.00, reasoning: 12.00, cache_creation: 2.00 },
"gpt-5.1-codex-max": { input: 8.00, output: 32.00, cached: 4.00, reasoning: 48.00, cache_creation: 8.00 },
"gpt-5.2": { input: 5.00, output: 20.00, cached: 2.50, reasoning: 30.00, cache_creation: 5.00 },
"gpt-5.2-codex": { input: 5.00, output: 20.00, cached: 2.50, reasoning: 30.00, cache_creation: 5.00 },
"gpt-5.3-codex": { input: 6.00, output: 24.00, cached: 3.00, reasoning: 36.00, cache_creation: 6.00 },
"gpt-5.3-codex-xhigh": { input: 10.00, output: 40.00, cached: 5.00, reasoning: 60.00, cache_creation: 10.00 },
"gpt-5.3-codex-high": { input: 8.00, output: 32.00, cached: 4.00, reasoning: 48.00, cache_creation: 8.00 },
"gpt-5.3-codex-low": { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 },
"gpt-5.3-codex-none": { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 },
"gpt-5.3-codex-spark": { input: 3.00, output: 12.00, cached: 0.30, reasoning: 12.00, cache_creation: 3.00 },
"o1": { input: 15.00, output: 60.00, cached: 7.50, reasoning: 90.00, cache_creation: 15.00 },
"o1-mini": { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 },
// === Gemini ===
"gemini-3-flash-preview": { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 },
"gemini-3-pro-preview": { input: 2.00, output: 12.00, cached: 0.25, reasoning: 18.00, cache_creation: 2.00 },
"gemini-3.1-pro-low": { input: 2.00, output: 12.00, cached: 0.25, reasoning: 18.00, cache_creation: 2.00 },
"gemini-3.1-pro-high": { input: 4.00, output: 18.00, cached: 0.50, reasoning: 27.00, cache_creation: 4.00 },
"gemini-pro-agent": { input: 4.00, output: 18.00, cached: 0.50, reasoning: 27.00, cache_creation: 4.00 },
"gemini-3-flash-agent": { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 },
"gemini-3.5-flash-low": { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 },
"gemini-3.5-flash-extra-low": { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 },
"gemini-3-flash": { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 },
"gemini-2.5-pro": { input: 2.00, output: 12.00, cached: 0.25, reasoning: 18.00, cache_creation: 2.00 },
"gemini-2.5-flash": { input: 0.30, output: 2.50, cached: 0.03, reasoning: 3.75, cache_creation: 0.30 },
"gemini-2.5-flash-lite": { input: 0.15, output: 1.25, cached: 0.015, reasoning: 1.875, cache_creation: 0.15 },
// === Qwen ===
"qwen3-coder-plus": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 },
"qwen3-coder-flash": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
// === Kimi ===
"kimi-k2": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 },
"kimi-k2-thinking": { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 },
"kimi-k2.5": { input: 1.20, output: 4.80, cached: 0.60, reasoning: 7.20, cache_creation: 1.20 },
"kimi-k2.5-thinking": { input: 1.80, output: 7.20, cached: 0.90, reasoning: 10.80, cache_creation: 1.80 },
"kimi-latest": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 },
// === DeepSeek ===
"deepseek-chat": { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 },
"deepseek-reasoner": { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 },
"deepseek-r1": { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 },
"deepseek-v3.2-chat": { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 },
"deepseek-v3.2-reasoner": { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 },
"deepseek-v4-flash": { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 },
"deepseek-v4-pro": { input: 0.435, output: 0.87, cached: 0.003625, reasoning: 0.87, cache_creation: 0.435 },
// === GLM ===
"glm-4.6": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
"glm-4.6v": { input: 0.75, output: 3.00, cached: 0.375, reasoning: 4.50, cache_creation: 0.75 },
"glm-4.7": { input: 0.75, output: 3.00, cached: 0.375, reasoning: 4.50, cache_creation: 0.75 },
"glm-5": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 },
// === MiniMax ===
"MiniMax-M3": { input: 0.30, output: 1.20, cached: 0.06, reasoning: 1.80, cache_creation: 0.30 },
"MiniMax-M2.1": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
"MiniMax-M2.5": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
"MiniMax-M2.7": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
"minimax-m2.1": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
"minimax-m2.5": { input: 0.60, output: 2.40, cached: 0.30, reasoning: 3.60, cache_creation: 0.60 },
// === Grok ===
"grok-code-fast-1": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
// === OpenRouter fallback ===
"auto": { input: 2.00, output: 8.00, cached: 1.00, reasoning: 12.00, cache_creation: 2.00 },
// === Misc ===
"oswe-vscode-prime": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 },
"gpt-oss-120b-medium": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
"vision-model": { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 },
"coder-model": { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 },
};
/**
* Provider-specific pricing overrides.
* Only include entries where price DIFFERS from MODEL_PRICING.
* Keyed by provider alias (cc, cx, gc, gh, ...) or provider id (openai, anthropic, ...).
*/
export const PROVIDER_PRICING = {
// GitHub Copilot (gh) — gpt-5.3-codex has different rate than canonical
gh: {
"gpt-5.3-codex": { input: 1.75, output: 14.00, cached: 0.175, reasoning: 14.00, cache_creation: 1.75 },
},
};
/**
* Pattern-based pricing fallback — matched when no exact model entry found.
* Patterns use simple glob: "*" matches any substring.
* First match wins — order matters.
*/
export const PATTERN_PRICING = [
// --- Codex variants ---
{ pattern: "*-codex-xhigh", pricing: { input: 10.00, output: 40.00, cached: 5.00, reasoning: 60.00, cache_creation: 10.00 } },
{ pattern: "*-codex-high", pricing: { input: 8.00, output: 32.00, cached: 4.00, reasoning: 48.00, cache_creation: 8.00 } },
{ pattern: "*-codex-max", pricing: { input: 8.00, output: 32.00, cached: 4.00, reasoning: 48.00, cache_creation: 8.00 } },
{ pattern: "*-codex-mini-*", pricing: { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 } },
{ pattern: "*-codex-mini", pricing: { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 } },
{ pattern: "*-codex-low", pricing: { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 } },
{ pattern: "*-codex-none", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } },
{ pattern: "*-codex-spark", pricing: { input: 3.00, output: 12.00, cached: 0.30, reasoning: 12.00, cache_creation: 3.00 } },
{ pattern: "codex-*", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } },
{ pattern: "*-codex", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } },
// --- Claude ---
{ pattern: "claude-opus-*", pricing: { input: 5.00, output: 25.00, cached: 0.50, reasoning: 25.00, cache_creation: 6.25 } },
{ pattern: "claude-sonnet-*", pricing: { input: 3.00, output: 15.00, cached: 0.30, reasoning: 15.00, cache_creation: 3.75 } },
{ pattern: "claude-haiku-*", pricing: { input: 1.00, output: 5.00, cached: 0.10, reasoning: 5.00, cache_creation: 1.25 } },
{ pattern: "claude-*", pricing: { input: 3.00, output: 15.00, cached: 0.30, reasoning: 15.00, cache_creation: 3.75 } },
// --- Gemini (specific first, generic last) ---
{ pattern: "gemini-*-flash-lite", pricing: { input: 0.15, output: 1.25, cached: 0.015, reasoning: 1.875, cache_creation: 0.15 } },
{ pattern: "gemini-*-flash", pricing: { input: 0.30, output: 2.50, cached: 0.03, reasoning: 3.75, cache_creation: 0.30 } },
{ pattern: "gemini-*-pro", pricing: { input: 2.00, output: 12.00, cached: 0.25, reasoning: 18.00, cache_creation: 2.00 } },
{ pattern: "gemini-3-*", pricing: { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 } },
{ pattern: "gemini-2.5-*", pricing: { input: 0.30, output: 2.50, cached: 0.03, reasoning: 3.75, cache_creation: 0.30 } },
{ pattern: "gemini-*", pricing: { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 } },
// --- GPT (specific first, generic last) ---
{ pattern: "gpt-5.3-*", pricing: { input: 6.00, output: 24.00, cached: 3.00, reasoning: 36.00, cache_creation: 6.00 } },
{ pattern: "gpt-5.2-*", pricing: { input: 5.00, output: 20.00, cached: 2.50, reasoning: 30.00, cache_creation: 5.00 } },
{ pattern: "gpt-5.1-*", pricing: { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 } },
{ pattern: "gpt-5-*", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } },
{ pattern: "gpt-5*", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } },
{ pattern: "gpt-4o-*", pricing: { input: 0.15, output: 0.60, cached: 0.075, reasoning: 0.90, cache_creation: 0.15 } },
{ pattern: "gpt-4o", pricing: { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 } },
{ pattern: "gpt-4*", pricing: { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 } },
// --- o1 / o-series ---
{ pattern: "o1-*", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } },
{ pattern: "o1", pricing: { input: 15.00, output: 60.00, cached: 7.50, reasoning: 90.00, cache_creation: 15.00 } },
{ pattern: "o3-*", pricing: { input: 10.00, output: 40.00, cached: 5.00, reasoning: 60.00, cache_creation: 10.00 } },
{ pattern: "o4-*", pricing: { input: 2.00, output: 8.00, cached: 1.00, reasoning: 12.00, cache_creation: 2.00 } },
// --- Qwen ---
{ pattern: "qwen3-coder-*", pricing: { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 } },
{ pattern: "qwen*-coder-*", pricing: { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 } },
{ pattern: "qwen*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } },
// --- Kimi ---
{ pattern: "kimi-*-thinking", pricing: { input: 1.80, output: 7.20, cached: 0.90, reasoning: 10.80, cache_creation: 1.80 } },
{ pattern: "kimi-k2*", pricing: { input: 1.20, output: 4.80, cached: 0.60, reasoning: 7.20, cache_creation: 1.20 } },
{ pattern: "kimi-*", pricing: { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 } },
// --- DeepSeek ---
{ pattern: "deepseek-*reasoner*", pricing: { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 } },
{ pattern: "deepseek-r*", pricing: { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 } },
{ pattern: "deepseek-v*", pricing: { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 } },
{ pattern: "deepseek-*", pricing: { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 } },
// --- GLM ---
{ pattern: "glm-5*", pricing: { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 } },
{ pattern: "glm-4*", pricing: { input: 0.75, output: 3.00, cached: 0.375, reasoning: 4.50, cache_creation: 0.75 } },
{ pattern: "glm-*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } },
// --- MiniMax ---
{ pattern: "MiniMax-*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } },
{ pattern: "minimax-*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } },
// --- Grok ---
{ pattern: "grok-code-*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } },
{ pattern: "grok-*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } },
];
/**
* Match a model ID against a glob pattern (* = wildcard). Case-insensitive:
* registry ids mix casing (e.g. "MiniMax-M2.5" vs "minimax-m2.5").
*/
export function matchPattern(pattern, model) {
const regex = new RegExp("^" + pattern.split("*").map(s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$", "i");
return regex.test(model);
}
/**
* Resolve pricing for a model using the 3-step fallback chain:
* 1. PROVIDER_PRICING[provider][model]
* 2. MODEL_PRICING[model]
* 3. PATTERN_PRICING (glob match)
*
* @param {string} provider
* @param {string} model
* @returns {object|null}
*/
export function getPricingForModel(provider, model) {
if (!model) return null;
// 1. Provider-specific override
if (provider && PROVIDER_PRICING[provider]?.[model]) {
return PROVIDER_PRICING[provider][model];
}
// 2. Canonical model pricing (strip vendor prefix if needed: "deepseek/deepseek-chat" → "deepseek-chat")
const baseModel = model.includes("/") ? model.split("/").pop() : model;
if (MODEL_PRICING[baseModel]) return MODEL_PRICING[baseModel];
if (MODEL_PRICING[model]) return MODEL_PRICING[model];
// 3. Pattern match
for (const { pattern, pricing } of PATTERN_PRICING) {
if (matchPattern(pattern, baseModel) || matchPattern(pattern, model)) {
return pricing;
}
}
return null;
}
/**
* Get all provider pricing (for UI / API).
* Returns PROVIDER_PRICING — consumers should fall back to MODEL_PRICING for unlisted models.
*/
export function getDefaultPricing() {
return PROVIDER_PRICING;
}
/**
* Format cost for display
* @param {number} cost
* @returns {string}
*/
export function formatCost(cost) {
if (cost === null || cost === undefined || isNaN(cost)) return "$0.00";
return `$${cost.toFixed(2)}`;
}
/**
* Calculate cost from tokens and pricing
* @param {object} tokens
* @param {object} pricing
* @returns {number} cost in dollars
*/
export function calculateCostFromTokens(tokens, pricing) {
if (!tokens || !pricing) return 0;
let cost = 0;
const inputTokens = tokens.prompt_tokens || tokens.input_tokens || 0;
const cachedTokens = tokens.cached_tokens || tokens.cache_read_input_tokens || 0;
const nonCachedInput = Math.max(0, inputTokens - cachedTokens);
cost += nonCachedInput * (pricing.input / 1000000);
if (cachedTokens > 0) {
cost += cachedTokens * ((pricing.cached || pricing.input) / 1000000);
}
const outputTokens = tokens.completion_tokens || tokens.output_tokens || 0;
cost += outputTokens * (pricing.output / 1000000);
const reasoningTokens = tokens.reasoning_tokens || 0;
if (reasoningTokens > 0) {
cost += reasoningTokens * ((pricing.reasoning || pricing.output) / 1000000);
}
const cacheCreationTokens = tokens.cache_creation_input_tokens || 0;
if (cacheCreationTokens > 0) {
cost += cacheCreationTokens * ((pricing.cache_creation || pricing.input) / 1000000);
}
return cost;
}

View File

@@ -0,0 +1,45 @@
export default {
id: "aws-polly",
alias: "polly",
display: {
name: "AWS Polly",
icon: "record_voice_over",
color: "#FF9900",
textIcon: "PL",
website: "https://aws.amazon.com/polly/",
notice: {
text: "Use AWS Secret Access Key as API key; set providerSpecificData.accessKeyId and optional region.",
apiKeyUrl: "https://console.aws.amazon.com/iam/home#/security_credentials"
}
},
category: "apikey",
authType: "apikey",
serviceKinds: [
"tts"
],
ttsConfig: {
baseUrl: "https://polly.{region}.amazonaws.com/v1/speech",
authType: "apikey",
authHeader: "aws-sigv4",
format: "aws-polly",
models: [
{
id: "standard",
name: "Standard"
},
{
id: "neural",
name: "Neural"
},
{
id: "long-form",
name: "Long-form"
},
{
id: "generative",
name: "Generative"
}
]
},
hasProviderSpecificData: true
};

View File

@@ -0,0 +1,35 @@
export default {
id: "brave-search",
alias: "brave",
display: {
name: "Brave Search",
icon: "travel_explore",
color: "#FB542B",
textIcon: "BR",
website: "https://brave.com/search/api",
notice: {
apiKeyUrl: "https://api-dashboard.search.brave.com/app/keys"
}
},
category: "apikey",
authType: "apikey",
serviceKinds: [
"webSearch"
],
searchConfig: {
baseUrl: "https://api.search.brave.com/res/v1",
method: "GET",
authType: "apikey",
authHeader: "x-subscription-token",
costPerQuery: 0.005,
freeMonthlyQuota: 1000,
searchTypes: [
"web",
"news"
],
defaultMaxResults: 5,
maxMaxResults: 20,
timeoutMs: 10000,
cacheTTLMs: 300000
}
};

View File

@@ -1,6 +1,6 @@
export default {
id: "byteplus",
priority: 150,
priority: 70,
alias: "byteplus",
aliases: [
"bpm",

View File

@@ -0,0 +1,36 @@
export default {
id: "cartesia",
alias: "cartesia",
display: {
name: "Cartesia",
icon: "spatial_audio",
color: "#FF4F8B",
textIcon: "CA",
website: "https://cartesia.ai",
notice: {
apiKeyUrl: "https://play.cartesia.ai/keys"
}
},
category: "apikey",
authType: "apikey",
serviceKinds: [
"tts"
],
ttsConfig: {
baseUrl: "https://api.cartesia.ai/tts/bytes",
authType: "apikey",
authHeader: "x-api-key",
format: "cartesia",
models: [
{
id: "sonic-2",
name: "Sonic 2"
},
{
id: "sonic-3",
name: "Sonic 3"
}
]
},
hidden: true
};

View File

@@ -1,6 +1,6 @@
export default {
id: "cline",
priority: 70,
priority: 80,
alias: "cl",
uiAlias: "cl",
display: {

View File

@@ -1,6 +1,6 @@
export default {
id: "cloudflare-ai",
priority: 20,
priority: 60,
hasFree: true,
alias: "cloudflare-ai",
aliases: [

View File

@@ -1,6 +1,7 @@
export default {
id: "codebuddy",
priority: 80,
hidden: true,
priority: 90,
display: {
name: "CodeBuddy",
icon: "smart_toy",

View File

@@ -0,0 +1,30 @@
export default {
id: "coqui",
alias: "coqui",
display: {
name: "Coqui TTS",
icon: "record_voice_over",
color: "#10B981",
textIcon: "CQ",
website: "https://github.com/coqui-ai/TTS"
},
category: "freeTier",
authType: "none",
serviceKinds: [
"tts"
],
noAuth: true,
ttsConfig: {
baseUrl: "http://localhost:5002/api/tts",
authType: "none",
authHeader: "none",
format: "coqui",
models: [
{
id: "tts_models/en/ljspeech/tacotron2-DDC",
name: "Tacotron2 DDC (LJSpeech)"
}
]
},
hidden: true
};

View File

@@ -1,6 +1,6 @@
export default {
id: "cursor",
priority: 40,
priority: 50,
alias: "cu",
uiAlias: "cu",
display: {

View File

@@ -0,0 +1,24 @@
export default {
id: "edge-tts",
alias: "edge-tts",
display: {
name: "Edge TTS",
icon: "record_voice_over",
color: "#0078D4",
textIcon: "ET"
},
category: "freeTier",
authType: "none",
serviceKinds: [
"tts"
],
mediaPriority: 5,
noAuth: true,
ttsConfig: {
baseUrl: "edge-tts",
authType: "none",
authHeader: "none",
format: "edge-tts",
models: []
}
};

View File

@@ -0,0 +1,35 @@
export default {
id: "elevenlabs",
alias: "el",
display: {
name: "ElevenLabs",
icon: "record_voice_over",
color: "#6C47FF",
textIcon: "EL",
website: "https://elevenlabs.io",
notice: {
apiKeyUrl: "https://elevenlabs.io/app/settings/api-keys"
}
},
category: "apikey",
authType: "apikey",
serviceKinds: [
"tts"
],
ttsConfig: {
baseUrl: "https://api.elevenlabs.io/v1/text-to-speech",
authType: "apikey",
authHeader: "xi-api-key",
format: "elevenlabs",
models: [
{
id: "eleven_multilingual_v2",
name: "Eleven Multilingual v2"
},
{
id: "eleven_turbo_v2_5",
name: "Eleven Turbo v2.5"
}
]
}
};

View File

@@ -0,0 +1,50 @@
export default {
id: "exa",
alias: "exa",
display: {
name: "Exa",
icon: "manage_search",
color: "#2563EB",
textIcon: "EX",
website: "https://exa.ai",
notice: {
apiKeyUrl: "https://dashboard.exa.ai/api-keys"
}
},
category: "apikey",
authType: "apikey",
serviceKinds: [
"webSearch",
"webFetch"
],
searchConfig: {
baseUrl: "https://api.exa.ai/search",
method: "POST",
authType: "apikey",
authHeader: "x-api-key",
costPerQuery: 0.007,
freeMonthlyQuota: 1000,
searchTypes: [
"web",
"news"
],
defaultMaxResults: 5,
maxMaxResults: 100,
timeoutMs: 10000,
cacheTTLMs: 300000
},
fetchConfig: {
baseUrl: "https://api.exa.ai/contents",
method: "POST",
authType: "apikey",
authHeader: "x-api-key",
costPerQuery: 0.001,
freeMonthlyQuota: 1000,
formats: [
"text",
"markdown"
],
maxCharacters: 100000,
timeoutMs: 15000
}
};

View File

@@ -0,0 +1,34 @@
export default {
id: "firecrawl",
alias: "firecrawl",
display: {
name: "Firecrawl",
icon: "local_fire_department",
color: "#F59E0B",
textIcon: "FC",
website: "https://firecrawl.dev",
notice: {
apiKeyUrl: "https://www.firecrawl.dev/app/api-keys"
}
},
category: "apikey",
authType: "apikey",
serviceKinds: [
"webFetch"
],
fetchConfig: {
baseUrl: "https://api.firecrawl.dev/v1/scrape",
method: "POST",
authType: "apikey",
authHeader: "bearer",
costPerQuery: 0.002,
freeMonthlyQuota: 500,
formats: [
"markdown",
"html",
"text"
],
maxCharacters: 200000,
timeoutMs: 30000
}
};

View File

@@ -2,7 +2,7 @@ import { GOOGLE_OAUTH_CLIENT } from "../shared.js";
export default {
id: "gemini-cli",
priority: 130,
priority: 20,
hasFree: true,
alias: "gc",
uiAlias: "gc",

View File

@@ -2,7 +2,7 @@ import { GOOGLE_OAUTH_CLIENT } from "../shared.js";
export default {
id: "gemini",
priority: 10,
priority: 50,
hasFree: true,
alias: "gemini",
display: {

View File

@@ -1,6 +1,6 @@
export default {
id: "github",
priority: 50,
priority: 40,
alias: "gh",
uiAlias: "gh",
display: {

View File

@@ -1,6 +1,7 @@
export default {
id: "gitlab",
priority: 120,
hidden: true,
priority: 100,
display: {
name: "GitLab Duo",
icon: "code",

View File

@@ -0,0 +1,35 @@
export default {
id: "google-pse",
alias: "gpse",
display: {
name: "Google PSE",
icon: "search",
color: "#4285F4",
textIcon: "GP",
website: "https://programmablesearchengine.google.com",
notice: {
apiKeyUrl: "https://programmablesearchengine.google.com/controlpanel/create"
}
},
category: "apikey",
authType: "apikey",
serviceKinds: [
"webSearch"
],
searchConfig: {
baseUrl: "https://www.googleapis.com/customsearch/v1",
method: "GET",
authType: "apikey",
authHeader: "key",
costPerQuery: 0.005,
freeMonthlyQuota: 3000,
searchTypes: [
"web",
"news"
],
defaultMaxResults: 5,
maxMaxResults: 10,
timeoutMs: 10000,
cacheTTLMs: 300000
}
};

View File

@@ -0,0 +1,24 @@
export default {
id: "google-tts",
alias: "google-tts",
display: {
name: "Google TTS",
icon: "record_voice_over",
color: "#4285F4",
textIcon: "GT"
},
category: "freeTier",
authType: "none",
serviceKinds: [
"tts"
],
mediaPriority: 5,
noAuth: true,
ttsConfig: {
baseUrl: "google-tts",
authType: "none",
authHeader: "none",
format: "google-tts",
models: []
}
};

View File

@@ -1,6 +1,7 @@
export default {
id: "iflow",
priority: 170,
hidden: true,
priority: 110,
alias: "if",
display: {
name: "iFlow AI",

View File

@@ -1,75 +1,98 @@
// Auto-generated: static imports of all registry entries
import p0 from './alicode-intl.js';
import p1 from './alicode.js';
import p2 from './anthropic.js';
import p3 from './antigravity.js';
import p4 from './assemblyai.js';
import p5 from './azure.js';
import p6 from './black-forest-labs.js';
import p7 from './blackbox.js';
import p8 from './byteplus.js';
import p9 from './cerebras.js';
import p10 from './chutes.js';
import p11 from './claude.js';
import p12 from './cline.js';
import p13 from './cloudflare-ai.js';
import p14 from './codebuddy.js';
import p15 from './codex.js';
import p16 from './cohere.js';
import p17 from './comfyui.js';
import p18 from './commandcode.js';
import p19 from './cursor.js';
import p20 from './deepgram.js';
import p21 from './deepseek.js';
import p22 from './fal-ai.js';
import p23 from './fireworks.js';
import p24 from './gemini-cli.js';
import p25 from './gemini.js';
import p26 from './github.js';
import p27 from './gitlab.js';
import p28 from './glm-cn.js';
import p29 from './glm.js';
import p30 from './grok-web.js';
import p31 from './groq.js';
import p32 from './huggingface.js';
import p33 from './hyperbolic.js';
import p34 from './iflow.js';
import p35 from './kilocode.js';
import p36 from './kimi-coding.js';
import p37 from './kimi.js';
import p38 from './kiro.js';
import p39 from './mimo-free.js';
import p40 from './minimax-cn.js';
import p41 from './minimax.js';
import p42 from './mistral.js';
import p43 from './mmf.js';
import p44 from './nanobanana.js';
import p45 from './nebius.js';
import p46 from './nvidia.js';
import p47 from './ollama-local.js';
import p48 from './ollama.js';
import p49 from './openai.js';
import p50 from './opencode-go.js';
import p51 from './opencode.js';
import p52 from './openrouter.js';
import p53 from './perplexity-web.js';
import p54 from './perplexity.js';
import p55 from './qoder.js';
import p56 from './qwen.js';
import p57 from './recraft.js';
import p58 from './runwayml.js';
import p59 from './sdwebui.js';
import p60 from './siliconflow.js';
import p61 from './stability-ai.js';
import p62 from './together.js';
import p63 from './vercel-ai-gateway.js';
import p64 from './vertex-partner.js';
import p65 from './vertex.js';
import p66 from './volcengine-ark.js';
import p67 from './voyage-ai.js';
import p68 from './xai.js';
import p69 from './xiaomi-mimo.js';
import p70 from './xiaomi-tokenplan.js';
import p0 from "./alicode.js";
import p1 from "./alicode-intl.js";
import p2 from "./anthropic.js";
import p3 from "./antigravity.js";
import p4 from "./assemblyai.js";
import p5 from "./aws-polly.js";
import p6 from "./azure.js";
import p7 from "./black-forest-labs.js";
import p8 from "./blackbox.js";
import p9 from "./brave-search.js";
import p10 from "./byteplus.js";
import p11 from "./cartesia.js";
import p12 from "./cerebras.js";
import p13 from "./chutes.js";
import p14 from "./claude.js";
import p15 from "./cline.js";
import p16 from "./cloudflare-ai.js";
import p17 from "./codebuddy.js";
import p18 from "./codex.js";
import p19 from "./cohere.js";
import p20 from "./comfyui.js";
import p21 from "./commandcode.js";
import p22 from "./coqui.js";
import p23 from "./cursor.js";
import p24 from "./deepgram.js";
import p25 from "./deepseek.js";
import p26 from "./edge-tts.js";
import p27 from "./elevenlabs.js";
import p28 from "./exa.js";
import p29 from "./fal-ai.js";
import p30 from "./firecrawl.js";
import p31 from "./fireworks.js";
import p32 from "./gemini.js";
import p33 from "./gemini-cli.js";
import p34 from "./github.js";
import p35 from "./gitlab.js";
import p36 from "./glm.js";
import p37 from "./glm-cn.js";
import p38 from "./google-pse.js";
import p39 from "./google-tts.js";
import p40 from "./grok-web.js";
import p41 from "./groq.js";
import p42 from "./huggingface.js";
import p43 from "./hyperbolic.js";
import p44 from "./iflow.js";
import p45 from "./inworld.js";
import p46 from "./jina-ai.js";
import p47 from "./jina-reader.js";
import p48 from "./kilocode.js";
import p49 from "./kimi.js";
import p50 from "./kimi-coding.js";
import p51 from "./kiro.js";
import p52 from "./linkup.js";
import p53 from "./local-device.js";
import p54 from "./mimo-free.js";
import p55 from "./minimax.js";
import p56 from "./minimax-cn.js";
import p57 from "./mistral.js";
import p58 from "./mmf.js";
import p59 from "./nanobanana.js";
import p60 from "./nebius.js";
import p61 from "./nvidia.js";
import p62 from "./ollama.js";
import p63 from "./ollama-local.js";
import p64 from "./openai.js";
import p65 from "./opencode.js";
import p66 from "./opencode-go.js";
import p67 from "./openrouter.js";
import p68 from "./perplexity.js";
import p69 from "./perplexity-web.js";
import p70 from "./playht.js";
import p71 from "./qoder.js";
import p72 from "./qwen.js";
import p73 from "./recraft.js";
import p74 from "./runwayml.js";
import p75 from "./sdwebui.js";
import p76 from "./searchapi.js";
import p77 from "./searxng.js";
import p78 from "./serper.js";
import p79 from "./siliconflow.js";
import p80 from "./stability-ai.js";
import p81 from "./tavily.js";
import p82 from "./together.js";
import p83 from "./topaz.js";
import p84 from "./tortoise.js";
import p85 from "./vercel-ai-gateway.js";
import p86 from "./vertex.js";
import p87 from "./vertex-partner.js";
import p88 from "./volcengine-ark.js";
import p89 from "./voyage-ai.js";
import p90 from "./xai.js";
import p91 from "./xiaomi-mimo.js";
import p92 from "./xiaomi-tokenplan.js";
import p93 from "./youcom.js";
export default [
p0,
@@ -142,5 +165,28 @@ export default [
p67,
p68,
p69,
p70
p70,
p71,
p72,
p73,
p74,
p75,
p76,
p77,
p78,
p79,
p80,
p81,
p82,
p83,
p84,
p85,
p86,
p87,
p88,
p89,
p90,
p91,
p92,
p93
];

View File

@@ -0,0 +1,36 @@
export default {
id: "inworld",
alias: "inworld",
display: {
name: "Inworld TTS",
icon: "record_voice_over",
color: "#FF6B6B",
textIcon: "IW",
website: "https://inworld.ai",
notice: {
text: "Free tier: 40 minutes/month TTS. Paid: TTS-1.5 Mini $0.01/min ($15/1M chars), TTS-1.5 Max $0.025/min ($30/1M chars). 270+ voices, 15 languages.",
apiKeyUrl: "https://platform.inworld.ai/api-keys"
}
},
category: "apikey",
authType: "apikey",
serviceKinds: [
"tts"
],
ttsConfig: {
baseUrl: "https://api.inworld.ai/tts/v1/voice",
authType: "apikey",
authHeader: "basic",
format: "inworld",
models: [
{
id: "inworld-tts-1.5-mini",
name: "Inworld TTS 1.5 Mini ($0.01/min)"
},
{
id: "inworld-tts-1.5-max",
name: "Inworld TTS 1.5 Max ($0.025/min)"
}
]
}
};

View File

@@ -0,0 +1,42 @@
export default {
id: "jina-ai",
alias: "jina",
display: {
name: "Jina AI",
icon: "blur_on",
color: "#2563EB",
textIcon: "JA",
website: "https://jina.ai",
notice: {
text: "10M free tokens on signup (non-commercial), no credit card required.",
apiKeyUrl: "https://jina.ai/?sui=apikey"
}
},
category: "apikey",
authType: "apikey",
serviceKinds: [
"embedding"
],
embeddingConfig: {
baseUrl: "https://api.jina.ai/v1/embeddings",
authType: "apikey",
authHeader: "bearer",
models: [
{
id: "jina-embeddings-v3",
name: "Jina Embeddings v3",
dimensions: 1024
},
{
id: "jina-embeddings-v2-base-en",
name: "Jina Embeddings v2 Base EN",
dimensions: 768
},
{
id: "jina-embeddings-v2-base-code",
name: "Jina Embeddings v2 Base Code",
dimensions: 768
}
]
}
};

View File

@@ -0,0 +1,34 @@
export default {
id: "jina-reader",
alias: "jina-reader",
display: {
name: "Jina Reader",
icon: "menu_book",
color: "#000000",
textIcon: "JR",
website: "https://jina.ai/reader",
notice: {
apiKeyUrl: "https://jina.ai/?sui=apikey"
}
},
category: "apikey",
authType: "apikey",
serviceKinds: [
"webFetch"
],
fetchConfig: {
baseUrl: "https://r.jina.ai",
method: "GET",
authType: "apikey",
authHeader: "bearer",
costPerQuery: 0,
freeMonthlyQuota: 1000000,
formats: [
"markdown",
"text",
"html"
],
maxCharacters: 200000,
timeoutMs: 30000
}
};

View File

@@ -1,6 +1,6 @@
export default {
id: "kilocode",
priority: 60,
priority: 70,
alias: "kc",
uiAlias: "kc",
display: {

View File

@@ -2,7 +2,8 @@ import { CLAUDE_API_HEADERS, KIMI_CODING_BASE_URL } from "../shared.js";
export default {
id: "kimi-coding",
priority: 180,
hidden: true,
priority: 120,
alias: "kmc",
display: {
name: "Kimi Coding",

View File

@@ -1,6 +1,6 @@
export default {
id: "kiro",
priority: 80,
priority: 10,
alias: "kr",
uiAlias: "kr",
display: {

View File

@@ -0,0 +1,34 @@
export default {
id: "linkup",
alias: "linkup",
display: {
name: "Linkup",
icon: "link",
color: "#0EA5E9",
textIcon: "LK",
website: "https://linkup.so",
notice: {
apiKeyUrl: "https://app.linkup.so/api-keys"
}
},
category: "apikey",
authType: "apikey",
serviceKinds: [
"webSearch"
],
searchConfig: {
baseUrl: "https://api.linkup.so/v1/search",
method: "POST",
authType: "apikey",
authHeader: "bearer",
costPerQuery: 0.005,
freeMonthlyQuota: 1000,
searchTypes: [
"web"
],
defaultMaxResults: 5,
maxMaxResults: 50,
timeoutMs: 10000,
cacheTTLMs: 300000
}
};

View File

@@ -0,0 +1,24 @@
export default {
id: "local-device",
alias: "local-device",
display: {
name: "Local Device",
icon: "speaker",
color: "#64748B",
textIcon: "LD"
},
category: "freeTier",
authType: "none",
serviceKinds: [
"tts"
],
mediaPriority: 5,
noAuth: true,
ttsConfig: {
baseUrl: "local-device",
authType: "none",
authHeader: "none",
format: "local-device",
models: []
}
};

View File

@@ -1,6 +1,6 @@
export default {
id: "mimo-free",
priority: 120,
priority: 50,
hasFree: true,
alias: "mmf",
uiAlias: "mmf",

View File

@@ -1,5 +1,6 @@
export default {
id: "mmf",
hidden: true,
priority: 200,
display: {
name: "MMF",

View File

@@ -27,7 +27,7 @@ export default {
{ id: "nanobanana-flash", name: "NanoBanana Flash", params: ["n","size"], kind: "image" },
{ id: "nanobanana-pro", name: "NanoBanana Pro", params: ["n","size"], kind: "image" },
],
serviceKinds: ["llm","image"],
serviceKinds: ["image"],
imageConfig: {
baseUrl: "https://api.nanobananaapi.ai/api/v1/nanobanana/generate",
pollUrl: "https://api.nanobananaapi.ai/api/v1/nanobanana/record-info",

View File

@@ -1,6 +1,6 @@
export default {
id: "nvidia",
priority: 100,
priority: 20,
hasFree: true,
alias: "nvidia",
display: {

View File

@@ -1,6 +1,6 @@
export default {
id: "ollama",
priority: 40,
priority: 30,
hasFree: true,
alias: "ollama",
display: {

View File

@@ -1,6 +1,6 @@
export default {
id: "opencode",
priority: 110,
priority: 40,
hasFree: true,
alias: "oc",
uiAlias: "oc",

View File

@@ -1,6 +1,6 @@
export default {
id: "openrouter",
priority: 30,
priority: 10,
hasFree: true,
alias: "openrouter",
display: {

View File

@@ -0,0 +1,36 @@
export default {
id: "playht",
alias: "playht",
display: {
name: "PlayHT",
icon: "play_circle",
color: "#00B4D8",
textIcon: "PH",
website: "https://play.ht",
notice: {
apiKeyUrl: "https://play.ht/studio/api-access"
}
},
category: "apikey",
authType: "apikey",
serviceKinds: [
"tts"
],
ttsConfig: {
baseUrl: "https://api.play.ht/api/v2/tts/stream",
authType: "apikey",
authHeader: "playht",
format: "playht",
models: [
{
id: "PlayDialog",
name: "PlayDialog"
},
{
id: "Play3.0-mini",
name: "Play 3.0 Mini"
}
]
},
hidden: true
};

View File

@@ -1,6 +1,6 @@
export default {
id: "qoder",
priority: 230,
priority: 30,
alias: "qd",
uiAlias: "qd",
display: {

View File

@@ -1,6 +1,7 @@
export default {
id: "qwen",
priority: 240,
hidden: true,
priority: 130,
alias: "qw",
display: {
name: "Qwen Code",

View File

@@ -0,0 +1,35 @@
export default {
id: "searchapi",
alias: "searchapi",
display: {
name: "SearchAPI",
icon: "search",
color: "#0EA5A4",
textIcon: "SA",
website: "https://www.searchapi.io",
notice: {
apiKeyUrl: "https://www.searchapi.io/dashboard"
}
},
category: "apikey",
authType: "apikey",
serviceKinds: [
"webSearch"
],
searchConfig: {
baseUrl: "https://www.searchapi.io/api/v1/search",
method: "GET",
authType: "apikey",
authHeader: "api_key",
costPerQuery: 0.004,
freeMonthlyQuota: 100,
searchTypes: [
"web",
"news"
],
defaultMaxResults: 5,
maxMaxResults: 100,
timeoutMs: 10000,
cacheTTLMs: 300000
}
};

View File

@@ -0,0 +1,33 @@
export default {
id: "searxng",
alias: "searxng",
display: {
name: "SearXNG",
icon: "saved_search",
color: "#3B82F6",
textIcon: "SX",
website: "https://docs.searxng.org"
},
category: "freeTier",
authType: "none",
serviceKinds: [
"webSearch"
],
noAuth: true,
searchConfig: {
baseUrl: "http://localhost:8888/search",
method: "GET",
authType: "none",
authHeader: "none",
costPerQuery: 0,
freeMonthlyQuota: 999999,
searchTypes: [
"web",
"news"
],
defaultMaxResults: 5,
maxMaxResults: 50,
timeoutMs: 10000,
cacheTTLMs: 180000
}
};

View File

@@ -0,0 +1,35 @@
export default {
id: "serper",
alias: "serper",
display: {
name: "Serper",
icon: "search",
color: "#4F46E5",
textIcon: "SP",
website: "https://serper.dev",
notice: {
apiKeyUrl: "https://serper.dev/api-key"
}
},
category: "apikey",
authType: "apikey",
serviceKinds: [
"webSearch"
],
searchConfig: {
baseUrl: "https://google.serper.dev",
method: "POST",
authType: "apikey",
authHeader: "x-api-key",
costPerQuery: 0.001,
freeMonthlyQuota: 2500,
searchTypes: [
"web",
"news"
],
defaultMaxResults: 5,
maxMaxResults: 100,
timeoutMs: 10000,
cacheTTLMs: 300000
}
};

View File

@@ -0,0 +1,50 @@
export default {
id: "tavily",
alias: "tavily",
display: {
name: "Tavily",
icon: "search",
color: "#5B21B6",
textIcon: "TV",
website: "https://tavily.com",
notice: {
apiKeyUrl: "https://app.tavily.com/home"
}
},
category: "apikey",
authType: "apikey",
serviceKinds: [
"webSearch",
"webFetch"
],
searchConfig: {
baseUrl: "https://api.tavily.com/search",
method: "POST",
authType: "apikey",
authHeader: "bearer",
costPerQuery: 0.008,
freeMonthlyQuota: 1000,
searchTypes: [
"web",
"news"
],
defaultMaxResults: 5,
maxMaxResults: 20,
timeoutMs: 10000,
cacheTTLMs: 300000
},
fetchConfig: {
baseUrl: "https://api.tavily.com/extract",
method: "POST",
authType: "apikey",
authHeader: "bearer",
costPerQuery: 0.008,
freeMonthlyQuota: 1000,
formats: [
"markdown",
"text"
],
maxCharacters: 100000,
timeoutMs: 15000
}
};

View File

@@ -0,0 +1,19 @@
export default {
id: "topaz",
alias: "topaz",
display: {
name: "Topaz",
icon: "image",
color: "#059669",
textIcon: "TP",
website: "https://topazlabs.com",
notice: {
apiKeyUrl: "https://topazlabs.com/account"
}
},
category: "apikey",
authType: "apikey",
serviceKinds: [
"image"
]
};

View File

@@ -0,0 +1,30 @@
export default {
id: "tortoise",
alias: "tortoise",
display: {
name: "Tortoise TTS",
icon: "record_voice_over",
color: "#7C3AED",
textIcon: "TT",
website: "https://github.com/neonbjb/tortoise-tts"
},
category: "freeTier",
authType: "none",
serviceKinds: [
"tts"
],
noAuth: true,
ttsConfig: {
baseUrl: "http://localhost:5000/api/tts",
authType: "none",
authHeader: "none",
format: "tortoise",
models: [
{
id: "tortoise-v2",
name: "Tortoise v2"
}
]
},
hidden: true
};

View File

@@ -1,6 +1,6 @@
export default {
id: "vertex",
priority: 140,
priority: 40,
alias: "vertex",
aliases: [
"vx",

View File

@@ -12,7 +12,7 @@ export default {
apiKeyUrl: "https://console.x.ai",
},
},
category: "apikey",
category: "oauth",
authModes: [
"oauth",
"apikey",

View File

@@ -0,0 +1,35 @@
export default {
id: "youcom",
alias: "youcom",
display: {
name: "You.com Search",
icon: "search",
color: "#7C3AED",
textIcon: "YC",
website: "https://you.com",
notice: {
apiKeyUrl: "https://api.you.com"
}
},
category: "apikey",
authType: "apikey",
serviceKinds: [
"webSearch"
],
searchConfig: {
baseUrl: "https://ydc-index.io/v1/search",
method: "GET",
authType: "apikey",
authHeader: "x-api-key",
costPerQuery: 0.005,
freeMonthlyQuota: 0,
searchTypes: [
"web",
"news"
],
defaultMaxResults: 5,
maxMaxResults: 100,
timeoutMs: 10000,
cacheTTLMs: 300000
}
};

View File

@@ -3,7 +3,7 @@ import { DEFAULT_THINKING_CLAUDE_SIGNATURE } from "../../config/defaultThinkingS
import { ROLE, CLAUDE_BLOCK } from "../schema/index.js";
import { adjustMaxTokens } from "./maxTokens.js";
import { applyCloaking } from "../../utils/claudeCloaking.js";
import { deriveSessionId } from "../../utils/sessionManager.js";
import { resolveSessionId } from "../../utils/sessionManager.js";
import { PROVIDERS } from "../../providers/index.js";
// Check if message has valid non-empty content
@@ -252,7 +252,7 @@ export function prepareClaudeRequest(body, provider = null, apiKey = null, conne
// Apply cloaking for OAuth tokens (billing header + fake user ID)
// session_id in user_id must match X-Claude-Code-Session-Id for fingerprint consistency
if ((provider === "claude" || provider?.startsWith("anthropic-compatible")) && apiKey) {
const sessionId = connectionId ? deriveSessionId(connectionId) : null;
const sessionId = resolveSessionId({ body, connectionId, scope: "claude" });
body = applyCloaking(body, apiKey, sessionId);
}

View File

@@ -7,15 +7,16 @@ import { normalizeThinkingConfig } from "../services/provider.js";
import { AntigravityExecutor } from "../executors/antigravity.js";
import { PROVIDERS } from "../providers/index.js";
// Registry for translators
const requestRegistry = new Map();
const responseRegistry = new Map();
// Track initialization state
let initialized = false;
// Registry for translators. Lazy-init guards against circular-import order:
// translator modules call register() (side-effect) before this module's body runs.
// var (not let): hoisted as undefined so register() can run during circular import (no TDZ).
var requestRegistry;
var responseRegistry;
// Register translator
export function register(from, to, requestFn, responseFn) {
requestRegistry ??= new Map();
responseRegistry ??= new Map();
const key = `${from}:${to}`;
if (requestFn) {
requestRegistry.set(key, requestFn);
@@ -25,35 +26,8 @@ export function register(from, to, requestFn, responseFn) {
}
}
// Lazy load translators (called once on first use)
function ensureInitialized() {
if (initialized) return;
initialized = true;
// Request translators - sync require pattern for bundler
require("./request/claude-to-openai.js");
require("./request/openai-to-claude.js");
require("./request/gemini-to-openai.js");
require("./request/openai-to-gemini.js");
require("./request/openai-to-vertex.js");
require("./request/antigravity-to-openai.js");
require("./request/openai-responses.js");
require("./request/openai-to-kiro.js");
require("./request/openai-to-cursor.js");
require("./request/openai-to-ollama.js");
require("./request/openai-to-commandcode.js");
// Response translators
require("./response/claude-to-openai.js");
require("./response/openai-to-claude.js");
require("./response/gemini-to-openai.js");
require("./response/openai-to-antigravity.js");
require("./response/openai-responses.js");
require("./response/kiro-to-openai.js");
require("./response/cursor-to-openai.js");
require("./response/ollama-to-openai.js");
require("./response/commandcode-to-openai.js");
}
// No-op: translators self-register via the static imports at the bottom of this file.
function ensureInitialized() {}
// Strip specific content types from messages (explicit opt-in via strip[] in PROVIDER_MODELS)
function stripContentTypes(body, stripList = []) {
@@ -246,7 +220,29 @@ export function initState(sourceFormat) {
return base;
}
// Initialize all translators (kept for backward compatibility)
// Kept for backward compatibility; translators are already registered at import time.
export function initTranslators() {
ensureInitialized();
}
// Static side-effect imports: each module calls register() at load (works in ESM + bundler).
import "./request/claude-to-openai.js";
import "./request/openai-to-claude.js";
import "./request/gemini-to-openai.js";
import "./request/openai-to-gemini.js";
import "./request/openai-to-vertex.js";
import "./request/antigravity-to-openai.js";
import "./request/openai-responses.js";
import "./request/openai-to-kiro.js";
import "./request/openai-to-cursor.js";
import "./request/openai-to-ollama.js";
import "./request/openai-to-commandcode.js";
import "./response/claude-to-openai.js";
import "./response/openai-to-claude.js";
import "./response/gemini-to-openai.js";
import "./response/openai-to-antigravity.js";
import "./response/openai-responses.js";
import "./response/kiro-to-openai.js";
import "./response/cursor-to-openai.js";
import "./response/ollama-to-openai.js";
import "./response/commandcode-to-openai.js";

View File

@@ -160,7 +160,8 @@ function convertContent(content) {
// Function call
if (part.functionCall) {
toolCalls.push({
id: part.functionCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
// Deterministic id from name so the matching functionResponse pairs correctly.
id: part.functionCall.id || `call_${part.functionCall.name}`,
type: OPENAI_BLOCK.FUNCTION,
function: {
name: part.functionCall.name,
@@ -173,7 +174,7 @@ function convertContent(content) {
if (part.functionResponse) {
toolResults.push({
role: ROLE.TOOL,
tool_call_id: part.functionResponse.id || part.functionResponse.name,
tool_call_id: part.functionResponse.id || `call_${part.functionResponse.name}`,
content: JSON.stringify(part.functionResponse.response?.result || part.functionResponse.response || {})
});
}

View File

@@ -97,8 +97,10 @@ function convertGeminiContent(content) {
}
if (part.functionCall) {
// Gemini lacks a native call id; derive a deterministic one from the name so the
// matching functionResponse maps to the same tool_call_id (providers require pairing).
toolCalls.push({
id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
id: part.functionCall.id || `call_${part.functionCall.name}`,
type: OPENAI_BLOCK.FUNCTION,
function: {
name: part.functionCall.name,
@@ -110,7 +112,7 @@ function convertGeminiContent(content) {
if (part.functionResponse) {
return {
role: ROLE.TOOL,
tool_call_id: part.functionResponse.id || part.functionResponse.name,
tool_call_id: part.functionResponse.id || `call_${part.functionResponse.name}`,
content: JSON.stringify(part.functionResponse.response?.result || part.functionResponse.response || {})
};
}

View File

@@ -177,6 +177,12 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
}
// Cleanup Responses API specific fields
// Map Responses-only max_output_tokens to Chat max_tokens (avoid leaking unknown field upstream)
if (result.max_output_tokens !== undefined) {
if (result.max_tokens === undefined) result.max_tokens = result.max_output_tokens;
delete result.max_output_tokens;
}
delete result.input;
delete result.instructions;
delete result.include;

View File

@@ -5,6 +5,7 @@
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { v4 as uuidv4 } from "uuid";
import { resolveSessionId } from "../../utils/sessionManager.js";
import {
resolveKiroModel,
isThinkingEnabled,
@@ -546,7 +547,7 @@ export function openaiToKiroRequest(model, body, stream, credentials) {
const payload = {
conversationState: {
chatTriggerType: "MANUAL",
conversationId: uuidv4(),
conversationId: resolveSessionId({ headers: credentials?.rawHeaders, body, connectionId: credentials?.connectionId, scope: "kiro" }),
currentMessage: {
userInputMessage: {
content: finalContent,

View File

@@ -79,4 +79,126 @@ export function generateBinaryStyleId() {
*/
export function clearSessionStore() {
runtimeSessionStore.clear();
assistantSessionStore.clear();
}
// Conversation-stable session store: Key = hash(scope+assistant text), Value = { sessionId, lastUsed }
const assistantSessionStore = new Map();
const ASSISTANT_MIN_LEN = 50;
const ASSISTANT_CAP_LEN = 200;
const MAX_ASSISTANT_SESSIONS = 5000;
// Client headers/body fields that carry an upstream session id (priority order)
const SESSION_HEADER_KEYS = ["x-session-id", "session_id", "x-amp-thread-id", "x-client-request-id"];
const CLAUDE_CODE_SESSION_RE = /_session_([a-f0-9-]+)$/;
function sha16(text) {
return crypto.createHash("sha256").update(text).digest("hex").slice(0, 16);
}
// Normalize a session id candidate (trim, length cap)
function normalizeSessionId(value) {
if (typeof value !== "string") return null;
const v = value.trim();
if (!v || v.length > 256) return null;
return v;
}
// Extract Claude Code session id from metadata.user_id (_session_{uuid} | JSON {session_id})
function extractClaudeCodeSession(userId) {
if (typeof userId !== "string" || !userId) return null;
const m = userId.match(CLAUDE_CODE_SESSION_RE);
if (m) return m[1];
if (userId[0] === "{") {
try { return normalizeSessionId(JSON.parse(userId)?.session_id); } catch { /* noop */ }
}
return null;
}
// Lowercase-key lookup for raw client headers
function headerValue(headers, key) {
if (!headers || typeof headers !== "object") return null;
return normalizeSessionId(headers[key] ?? headers[key.toLowerCase()]);
}
// Read client-provided session id from headers/body (no generation)
function extractClientSessionId(headers, body) {
const claude = extractClaudeCodeSession(body?.metadata?.user_id);
if (claude) return `claude:${claude}`;
for (const key of SESSION_HEADER_KEYS) {
const v = headerValue(headers, key);
if (v) return v;
}
const fromBody =
normalizeSessionId(body?.prompt_cache_key) ||
normalizeSessionId(body?.session_id) ||
normalizeSessionId(body?.conversation_id) ||
normalizeSessionId(body?.metadata?.user_id);
return fromBody || null;
}
// Accumulate assistant text from OpenAI/Responses-style input/messages (cap-limited)
function accumulateAssistantText(body) {
const items = Array.isArray(body?.input) ? body.input
: Array.isArray(body?.messages) ? body.messages : null;
if (!items) return "";
let text = "";
for (const item of items) {
if (item?.role !== "assistant") continue;
if (typeof item.content === "string") text += item.content;
else if (Array.isArray(item.content)) {
for (const c of item.content) text += c?.text || c?.output || "";
}
if (text.length >= ASSISTANT_CAP_LEN) break;
}
return text;
}
// Stable session id keyed on accumulated assistant text (avoids collision on identical first user prompt)
function assistantTextSessionId(scope, body) {
const text = accumulateAssistantText(body);
if (text.length < ASSISTANT_MIN_LEN) return null;
const hash = sha16(`${scope}:${text.slice(0, ASSISTANT_CAP_LEN)}`);
const existing = assistantSessionStore.get(hash);
if (existing) {
existing.lastUsed = Date.now();
return existing.sessionId;
}
if (assistantSessionStore.size >= MAX_ASSISTANT_SESSIONS) {
assistantSessionStore.delete(assistantSessionStore.keys().next().value);
}
const sessionId = generateBinaryStyleId();
assistantSessionStore.set(hash, { sessionId, lastUsed: Date.now() });
return sessionId;
}
/**
* Resolve a conversation-stable session id (generalizes Codex resolveCacheSessionId).
* Priority: client session → accumulated-assistant-text hash → workspaceId → per-connection.
*
* @param {object} opts
* @param {object} [opts.headers] - Raw client request headers (lowercase keys)
* @param {object} [opts.body] - Parsed request body
* @param {string} [opts.connectionId] - Connection identifier (fallback scope)
* @param {string} [opts.workspaceId] - Provider workspace id (account-wide fallback)
* @param {string} [opts.scope] - Provider scope to isolate cache keys across providers
* @returns {string} A stable session id
*/
export function resolveSessionId({ headers, body, connectionId, workspaceId, scope = "" } = {}) {
const client = extractClientSessionId(headers, body);
if (client) return client;
const fromAssistant = assistantTextSessionId(`${scope}:${connectionId || ""}`, body);
if (fromAssistant) return fromAssistant;
const ws = normalizeSessionId(workspaceId);
if (ws) return ws;
return deriveSessionId(connectionId);
}
// Cleanup expired assistant-session entries
const assistantCleanup = setInterval(() => {
const now = Date.now();
for (const [key, entry] of assistantSessionStore) {
if (now - entry.lastUsed > MEMORY_CONFIG.sessionTtlMs) assistantSessionStore.delete(key);
}
}, MEMORY_CONFIG.sessionCleanupIntervalMs);
if (assistantCleanup.unref) assistantCleanup.unref();