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:
@@ -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" } } })
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
218
open-sse/providers/capabilities.js
Normal file
218
open-sse/providers/capabilities.js
Normal 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 };
|
||||
}
|
||||
304
open-sse/providers/pricing.js
Normal file
304
open-sse/providers/pricing.js
Normal 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;
|
||||
}
|
||||
45
open-sse/providers/registry/aws-polly.js
Normal file
45
open-sse/providers/registry/aws-polly.js
Normal 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
|
||||
};
|
||||
35
open-sse/providers/registry/brave-search.js
Normal file
35
open-sse/providers/registry/brave-search.js
Normal 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
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
id: "byteplus",
|
||||
priority: 150,
|
||||
priority: 70,
|
||||
alias: "byteplus",
|
||||
aliases: [
|
||||
"bpm",
|
||||
|
||||
36
open-sse/providers/registry/cartesia.js
Normal file
36
open-sse/providers/registry/cartesia.js
Normal 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
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
id: "cline",
|
||||
priority: 70,
|
||||
priority: 80,
|
||||
alias: "cl",
|
||||
uiAlias: "cl",
|
||||
display: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
id: "cloudflare-ai",
|
||||
priority: 20,
|
||||
priority: 60,
|
||||
hasFree: true,
|
||||
alias: "cloudflare-ai",
|
||||
aliases: [
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export default {
|
||||
id: "codebuddy",
|
||||
priority: 80,
|
||||
hidden: true,
|
||||
priority: 90,
|
||||
display: {
|
||||
name: "CodeBuddy",
|
||||
icon: "smart_toy",
|
||||
|
||||
30
open-sse/providers/registry/coqui.js
Normal file
30
open-sse/providers/registry/coqui.js
Normal 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
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
id: "cursor",
|
||||
priority: 40,
|
||||
priority: 50,
|
||||
alias: "cu",
|
||||
uiAlias: "cu",
|
||||
display: {
|
||||
|
||||
24
open-sse/providers/registry/edge-tts.js
Normal file
24
open-sse/providers/registry/edge-tts.js
Normal 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: []
|
||||
}
|
||||
};
|
||||
35
open-sse/providers/registry/elevenlabs.js
Normal file
35
open-sse/providers/registry/elevenlabs.js
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
50
open-sse/providers/registry/exa.js
Normal file
50
open-sse/providers/registry/exa.js
Normal 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
|
||||
}
|
||||
};
|
||||
34
open-sse/providers/registry/firecrawl.js
Normal file
34
open-sse/providers/registry/firecrawl.js
Normal 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
|
||||
}
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GOOGLE_OAUTH_CLIENT } from "../shared.js";
|
||||
|
||||
export default {
|
||||
id: "gemini",
|
||||
priority: 10,
|
||||
priority: 50,
|
||||
hasFree: true,
|
||||
alias: "gemini",
|
||||
display: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
id: "github",
|
||||
priority: 50,
|
||||
priority: 40,
|
||||
alias: "gh",
|
||||
uiAlias: "gh",
|
||||
display: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export default {
|
||||
id: "gitlab",
|
||||
priority: 120,
|
||||
hidden: true,
|
||||
priority: 100,
|
||||
display: {
|
||||
name: "GitLab Duo",
|
||||
icon: "code",
|
||||
|
||||
35
open-sse/providers/registry/google-pse.js
Normal file
35
open-sse/providers/registry/google-pse.js
Normal 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
|
||||
}
|
||||
};
|
||||
24
open-sse/providers/registry/google-tts.js
Normal file
24
open-sse/providers/registry/google-tts.js
Normal 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: []
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
export default {
|
||||
id: "iflow",
|
||||
priority: 170,
|
||||
hidden: true,
|
||||
priority: 110,
|
||||
alias: "if",
|
||||
display: {
|
||||
name: "iFlow AI",
|
||||
|
||||
@@ -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
|
||||
];
|
||||
|
||||
36
open-sse/providers/registry/inworld.js
Normal file
36
open-sse/providers/registry/inworld.js
Normal 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)"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
42
open-sse/providers/registry/jina-ai.js
Normal file
42
open-sse/providers/registry/jina-ai.js
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
34
open-sse/providers/registry/jina-reader.js
Normal file
34
open-sse/providers/registry/jina-reader.js
Normal 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
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
id: "kilocode",
|
||||
priority: 60,
|
||||
priority: 70,
|
||||
alias: "kc",
|
||||
uiAlias: "kc",
|
||||
display: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
id: "kiro",
|
||||
priority: 80,
|
||||
priority: 10,
|
||||
alias: "kr",
|
||||
uiAlias: "kr",
|
||||
display: {
|
||||
|
||||
34
open-sse/providers/registry/linkup.js
Normal file
34
open-sse/providers/registry/linkup.js
Normal 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
|
||||
}
|
||||
};
|
||||
24
open-sse/providers/registry/local-device.js
Normal file
24
open-sse/providers/registry/local-device.js
Normal 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: []
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
id: "mimo-free",
|
||||
priority: 120,
|
||||
priority: 50,
|
||||
hasFree: true,
|
||||
alias: "mmf",
|
||||
uiAlias: "mmf",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "mmf",
|
||||
hidden: true,
|
||||
priority: 200,
|
||||
display: {
|
||||
name: "MMF",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
id: "nvidia",
|
||||
priority: 100,
|
||||
priority: 20,
|
||||
hasFree: true,
|
||||
alias: "nvidia",
|
||||
display: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
id: "ollama",
|
||||
priority: 40,
|
||||
priority: 30,
|
||||
hasFree: true,
|
||||
alias: "ollama",
|
||||
display: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
id: "opencode",
|
||||
priority: 110,
|
||||
priority: 40,
|
||||
hasFree: true,
|
||||
alias: "oc",
|
||||
uiAlias: "oc",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
id: "openrouter",
|
||||
priority: 30,
|
||||
priority: 10,
|
||||
hasFree: true,
|
||||
alias: "openrouter",
|
||||
display: {
|
||||
|
||||
36
open-sse/providers/registry/playht.js
Normal file
36
open-sse/providers/registry/playht.js
Normal 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
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
id: "qoder",
|
||||
priority: 230,
|
||||
priority: 30,
|
||||
alias: "qd",
|
||||
uiAlias: "qd",
|
||||
display: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export default {
|
||||
id: "qwen",
|
||||
priority: 240,
|
||||
hidden: true,
|
||||
priority: 130,
|
||||
alias: "qw",
|
||||
display: {
|
||||
name: "Qwen Code",
|
||||
|
||||
35
open-sse/providers/registry/searchapi.js
Normal file
35
open-sse/providers/registry/searchapi.js
Normal 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
|
||||
}
|
||||
};
|
||||
33
open-sse/providers/registry/searxng.js
Normal file
33
open-sse/providers/registry/searxng.js
Normal 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
|
||||
}
|
||||
};
|
||||
35
open-sse/providers/registry/serper.js
Normal file
35
open-sse/providers/registry/serper.js
Normal 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
|
||||
}
|
||||
};
|
||||
50
open-sse/providers/registry/tavily.js
Normal file
50
open-sse/providers/registry/tavily.js
Normal 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
|
||||
}
|
||||
};
|
||||
19
open-sse/providers/registry/topaz.js
Normal file
19
open-sse/providers/registry/topaz.js
Normal 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"
|
||||
]
|
||||
};
|
||||
30
open-sse/providers/registry/tortoise.js
Normal file
30
open-sse/providers/registry/tortoise.js
Normal 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
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
id: "vertex",
|
||||
priority: 140,
|
||||
priority: 40,
|
||||
alias: "vertex",
|
||||
aliases: [
|
||||
"vx",
|
||||
|
||||
@@ -12,7 +12,7 @@ export default {
|
||||
apiKeyUrl: "https://console.x.ai",
|
||||
},
|
||||
},
|
||||
category: "apikey",
|
||||
category: "oauth",
|
||||
authModes: [
|
||||
"oauth",
|
||||
"apikey",
|
||||
|
||||
35
open-sse/providers/registry/youcom.js
Normal file
35
open-sse/providers/registry/youcom.js
Normal 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
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 || {})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 || {})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user