mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
fix(translator): ESM-safe registry + tool-id pairing + responses max_tokens; add real-creds tests
- translator/index.js: replace require() with static side-effect imports (ESM-safe), lazy-init registry maps to survive circular import order - openai-responses->openai: map max_output_tokens -> max_tokens (avoid leaking field upstream) - gemini/antigravity -> openai: derive deterministic tool_call id from name so functionCall/functionResponse pair correctly (fixes provider tool-pairing 400s) - add offline unit tests (finish-reason, usage, session-manager, ollama malformed args, const guard) - add real-creds integration tests (provider-cases + all-formats matrix: 6 inbound formats x 4 scenarios) Includes co-located provider registry refactor (pricing/capabilities/media providers) and sessionManager updates. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -284,24 +284,26 @@ export default function ProvidersPage() {
|
||||
const freeEntries = Object.entries(FREE_PROVIDERS).filter(
|
||||
([, info]) => !info.hidden && matchSearch(info.name),
|
||||
);
|
||||
const freeTierEntries = Object.entries(FREE_TIER_PROVIDERS)
|
||||
.filter(([, info]) => !info.hidden && matchSearch(info.name))
|
||||
.sort(([, a], [, b]) => {
|
||||
// hasFree providers first, then by priority
|
||||
const fa = a.hasFree ? 0 : 1;
|
||||
const fb = b.hasFree ? 0 : 1;
|
||||
if (fa !== fb) return fa - fb;
|
||||
return (a.priority ?? 999) - (b.priority ?? 999);
|
||||
});
|
||||
const apikeyEntries = sortByPriority(
|
||||
Object.entries(APIKEY_PROVIDERS).filter(
|
||||
const freeTierEntries = sortByPriority(
|
||||
Object.entries(FREE_TIER_PROVIDERS).filter(
|
||||
([, info]) => !info.hidden && matchSearch(info.name),
|
||||
),
|
||||
"freeTier",
|
||||
);
|
||||
// API Key: connected providers first, then alphabetical by name
|
||||
const apikeyEntries = Object.entries(APIKEY_PROVIDERS)
|
||||
.filter(
|
||||
([, info]) =>
|
||||
!info.hidden &&
|
||||
(info.serviceKinds ?? ["llm"]).includes("llm") &&
|
||||
matchSearch(info.name),
|
||||
),
|
||||
"apikey",
|
||||
);
|
||||
)
|
||||
.sort(([ka, a], [kb, b]) => {
|
||||
const ca = getProviderStats(ka, "apikey").total > 0 ? 0 : 1;
|
||||
const cb = getProviderStats(kb, "apikey").total > 0 ? 0 : 1;
|
||||
if (ca !== cb) return ca - cb;
|
||||
return (a.name || "").localeCompare(b.name || "");
|
||||
});
|
||||
const isApikeySearching = !!searchQuery.trim();
|
||||
const visibleApikeyEntries =
|
||||
isApikeySearching || showAllApikey
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getPricing, updatePricing, resetPricing, resetAllPricing } from "@/lib/localDb.js";
|
||||
import { getDefaultPricing } from "@/shared/constants/pricing.js";
|
||||
import { getDefaultPricing } from "open-sse/providers/pricing.js";
|
||||
|
||||
/**
|
||||
* GET /api/pricing
|
||||
|
||||
@@ -20,7 +20,7 @@ export async function getPricing() {
|
||||
if (cache.value && cache.expiresAt > now) return cache.value;
|
||||
|
||||
const userPricing = await getUserPricing();
|
||||
const { PROVIDER_PRICING } = await import("@/shared/constants/pricing.js");
|
||||
const { PROVIDER_PRICING } = await import("open-sse/providers/pricing.js");
|
||||
const merged = {};
|
||||
|
||||
for (const [provider, models] of Object.entries(PROVIDER_PRICING)) {
|
||||
@@ -52,7 +52,7 @@ export async function getPricingForModel(provider, model) {
|
||||
if (!model) return null;
|
||||
const userPricing = await getUserPricing();
|
||||
if (provider && userPricing[provider]?.[model]) return userPricing[provider][model];
|
||||
const { getPricingForModel: resolveConst } = await import("@/shared/constants/pricing.js");
|
||||
const { getPricingForModel: resolveConst } = await import("open-sse/providers/pricing.js");
|
||||
return resolveConst(provider, model);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { getDefaultPricing, formatCost } from "@/shared/constants/pricing.js";
|
||||
import { getDefaultPricing, formatCost } from "open-sse/providers/pricing.js";
|
||||
|
||||
export default function PricingModal({ isOpen, onClose, onSave }) {
|
||||
const [pricingData, setPricingData] = useState({});
|
||||
|
||||
@@ -1,303 +0,0 @@
|
||||
// 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).
|
||||
*/
|
||||
function matchPattern(pattern, model) {
|
||||
const regex = new RegExp("^" + pattern.split("*").map(s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$");
|
||||
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;
|
||||
}
|
||||
@@ -19,6 +19,7 @@ function buildProviderEntry(r) {
|
||||
...(r.display || {}),
|
||||
id: r.id,
|
||||
alias: r.uiAlias || r.alias,
|
||||
...(r.hidden ? { hidden: true } : {}),
|
||||
...mediaFields,
|
||||
...(r.priority !== undefined ? { priority: r.priority } : {}),
|
||||
...(r.hasFree ? { hasFree: true } : {}),
|
||||
|
||||
@@ -1,238 +1,12 @@
|
||||
// UI display config — registry providers derive from registry.display.
|
||||
// Non-registry providers (media-only: tts, stt, search, fetch) kept hardcoded here.
|
||||
// UI display config — all providers derive from registry.display.
|
||||
import REGISTRY from "open-sse/providers/registry/index.js";
|
||||
|
||||
export const RISK_NOTICE = "⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.";
|
||||
|
||||
// Non-registry media-only providers display config
|
||||
const MEDIA_ONLY_DISPLAY = {
|
||||
"elevenlabs": {
|
||||
"name": "ElevenLabs",
|
||||
"icon": "record_voice_over",
|
||||
"color": "#6C47FF",
|
||||
"textIcon": "EL",
|
||||
"website": "https://elevenlabs.io",
|
||||
"notice": {
|
||||
"apiKeyUrl": "https://elevenlabs.io/app/settings/api-keys"
|
||||
}
|
||||
},
|
||||
"cartesia": {
|
||||
"name": "Cartesia",
|
||||
"icon": "spatial_audio",
|
||||
"color": "#FF4F8B",
|
||||
"textIcon": "CA",
|
||||
"website": "https://cartesia.ai",
|
||||
"notice": {
|
||||
"apiKeyUrl": "https://play.cartesia.ai/keys"
|
||||
},
|
||||
"hidden": true
|
||||
},
|
||||
"playht": {
|
||||
"name": "PlayHT",
|
||||
"icon": "play_circle",
|
||||
"color": "#00B4D8",
|
||||
"textIcon": "PH",
|
||||
"website": "https://play.ht",
|
||||
"notice": {
|
||||
"apiKeyUrl": "https://play.ht/studio/api-access"
|
||||
},
|
||||
"hidden": true
|
||||
},
|
||||
"local-device": {
|
||||
"name": "Local Device",
|
||||
"icon": "speaker",
|
||||
"color": "#64748B",
|
||||
"textIcon": "LD",
|
||||
"mediaPriority": 5
|
||||
},
|
||||
"google-tts": {
|
||||
"name": "Google TTS",
|
||||
"icon": "record_voice_over",
|
||||
"color": "#4285F4",
|
||||
"textIcon": "GT",
|
||||
"mediaPriority": 5
|
||||
},
|
||||
"edge-tts": {
|
||||
"name": "Edge TTS",
|
||||
"icon": "record_voice_over",
|
||||
"color": "#0078D4",
|
||||
"textIcon": "ET",
|
||||
"mediaPriority": 5
|
||||
},
|
||||
"coqui": {
|
||||
"name": "Coqui TTS",
|
||||
"icon": "record_voice_over",
|
||||
"color": "#10B981",
|
||||
"textIcon": "CQ",
|
||||
"website": "https://github.com/coqui-ai/TTS",
|
||||
"hidden": true
|
||||
},
|
||||
"tortoise": {
|
||||
"name": "Tortoise TTS",
|
||||
"icon": "record_voice_over",
|
||||
"color": "#7C3AED",
|
||||
"textIcon": "TT",
|
||||
"website": "https://github.com/neonbjb/tortoise-tts",
|
||||
"hidden": true
|
||||
},
|
||||
"inworld": {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"aws-polly": {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"jina-ai": {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"jina-reader": {
|
||||
"name": "Jina Reader",
|
||||
"icon": "menu_book",
|
||||
"color": "#000000",
|
||||
"textIcon": "JR",
|
||||
"website": "https://jina.ai/reader",
|
||||
"notice": {
|
||||
"apiKeyUrl": "https://jina.ai/?sui=apikey"
|
||||
}
|
||||
},
|
||||
"tavily": {
|
||||
"name": "Tavily",
|
||||
"icon": "search",
|
||||
"color": "#5B21B6",
|
||||
"textIcon": "TV",
|
||||
"website": "https://tavily.com",
|
||||
"notice": {
|
||||
"apiKeyUrl": "https://app.tavily.com/home"
|
||||
}
|
||||
},
|
||||
"brave-search": {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"serper": {
|
||||
"name": "Serper",
|
||||
"icon": "search",
|
||||
"color": "#4F46E5",
|
||||
"textIcon": "SP",
|
||||
"website": "https://serper.dev",
|
||||
"notice": {
|
||||
"apiKeyUrl": "https://serper.dev/api-key"
|
||||
}
|
||||
},
|
||||
"exa": {
|
||||
"name": "Exa",
|
||||
"icon": "manage_search",
|
||||
"color": "#2563EB",
|
||||
"textIcon": "EX",
|
||||
"website": "https://exa.ai",
|
||||
"notice": {
|
||||
"apiKeyUrl": "https://dashboard.exa.ai/api-keys"
|
||||
}
|
||||
},
|
||||
"searxng": {
|
||||
"name": "SearXNG",
|
||||
"icon": "saved_search",
|
||||
"color": "#3B82F6",
|
||||
"textIcon": "SX",
|
||||
"website": "https://docs.searxng.org"
|
||||
},
|
||||
"google-pse": {
|
||||
"name": "Google PSE",
|
||||
"icon": "search",
|
||||
"color": "#4285F4",
|
||||
"textIcon": "GP",
|
||||
"website": "https://programmablesearchengine.google.com",
|
||||
"notice": {
|
||||
"apiKeyUrl": "https://programmablesearchengine.google.com/controlpanel/create"
|
||||
}
|
||||
},
|
||||
"linkup": {
|
||||
"name": "Linkup",
|
||||
"icon": "link",
|
||||
"color": "#0EA5E9",
|
||||
"textIcon": "LK",
|
||||
"website": "https://linkup.so",
|
||||
"notice": {
|
||||
"apiKeyUrl": "https://app.linkup.so/api-keys"
|
||||
}
|
||||
},
|
||||
"searchapi": {
|
||||
"name": "SearchAPI",
|
||||
"icon": "search",
|
||||
"color": "#0EA5A4",
|
||||
"textIcon": "SA",
|
||||
"website": "https://www.searchapi.io",
|
||||
"notice": {
|
||||
"apiKeyUrl": "https://www.searchapi.io/dashboard"
|
||||
}
|
||||
},
|
||||
"youcom": {
|
||||
"name": "You.com Search",
|
||||
"icon": "search",
|
||||
"color": "#7C3AED",
|
||||
"textIcon": "YC",
|
||||
"website": "https://you.com",
|
||||
"notice": {
|
||||
"apiKeyUrl": "https://api.you.com"
|
||||
}
|
||||
},
|
||||
"firecrawl": {
|
||||
"name": "Firecrawl",
|
||||
"icon": "local_fire_department",
|
||||
"color": "#F59E0B",
|
||||
"textIcon": "FC",
|
||||
"website": "https://firecrawl.dev",
|
||||
"notice": {
|
||||
"apiKeyUrl": "https://www.firecrawl.dev/app/api-keys"
|
||||
}
|
||||
},
|
||||
"topaz": {
|
||||
"name": "Topaz",
|
||||
"icon": "image",
|
||||
"color": "#059669",
|
||||
"textIcon": "TP",
|
||||
"website": "https://topazlabs.com",
|
||||
"notice": {
|
||||
"apiKeyUrl": "https://topazlabs.com/account"
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Resolve "RISK_NOTICE" token → real notice text (registry stores token to avoid import cycle)
|
||||
const resolveDisplay = (d) =>
|
||||
d.deprecationNotice === "RISK_NOTICE" ? { ...d, deprecationNotice: RISK_NOTICE } : d;
|
||||
|
||||
// Merge: registry providers take precedence
|
||||
export const PROVIDER_DISPLAY = {
|
||||
...MEDIA_ONLY_DISPLAY,
|
||||
...Object.fromEntries(REGISTRY.filter(r => r.display).map(r => [r.id, resolveDisplay(r.display)])),
|
||||
};
|
||||
export const PROVIDER_DISPLAY = Object.fromEntries(
|
||||
REGISTRY.filter((r) => r.display).map((r) => [r.id, resolveDisplay(r.display)]),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user