diff --git a/README.md b/README.md index 69fe17227e..9c59d8ab80 100644 --- a/README.md +++ b/README.md @@ -898,27 +898,44 @@ When minimized, OmniRoute lives in your system tray with quick actions: ## 💰 Pricing at a Glance -| Tier | Provider | Cost | Quota Reset | Best For | -| ------------------- | ----------------- | ---------------------- | ---------------- | ----------------------- | -| **💳 SUBSCRIPTION** | Claude Code (Pro) | $20/mo | 5h + weekly | Already subscribed | -| | Codex (Plus/Pro) | $20-200/mo | 5h + weekly | OpenAI users | -| | Gemini CLI | **FREE** | 180K/mo + 1K/day | Everyone! | -| | GitHub Copilot | $10-19/mo | Monthly | GitHub users | -| **🔑 API KEY** | NVIDIA NIM | **FREE** (dev forever) | ~40 RPM | 70+ open models | -| | Cerebras | **FREE** (1M tok/day) | 60K TPM / 30 RPM | World's fastest | -| | Groq | **FREE** (30 RPM) | 14.4K RPD | Ultra-fast Llama/Gemma | -| | DeepSeek | Pay-per-use | None | Best price/quality | -| | xAI (Grok) | Pay-per-use | None | Grok models | -| | Mistral | Free trial + paid | Rate limited | European AI | -| | OpenRouter | Pay-per-use | None | 100+ models aggr. | -| **💰 CHEAP** | GLM-4.7 | $0.6/1M | Daily 10AM | Budget backup | -| | MiniMax M2.1 | $0.2/1M | 5-hour rolling | Cheapest option | -| | Kimi K2 | $9/mo flat | 10M tokens/mo | Predictable cost | -| **🆓 FREE** | iFlow | **$0** | Unlimited | 5 models unlimited | -| | Qwen | **$0** | Unlimited | 4 models unlimited | -| | Kiro | **$0** | Unlimited | Claude (AWS Builder ID) | +| Tier | Provider | Cost | Quota Reset | Best For | +| ------------------- | --------------------------- | ------------------------- | ---------------- | --------------------------------- | +| **💳 SUBSCRIPTION** | Claude Code (Pro) | $20/mo | 5h + weekly | Already subscribed | +| | Codex (Plus/Pro) | $20-200/mo | 5h + weekly | OpenAI users | +| | Gemini CLI | **FREE** | 180K/mo + 1K/day | Everyone! | +| | GitHub Copilot | $10-19/mo | Monthly | GitHub users | +| **🔑 API KEY** | NVIDIA NIM | **FREE** (dev forever) | ~40 RPM | 70+ open models | +| | Cerebras | **FREE** (1M tok/day) | 60K TPM / 30 RPM | World's fastest | +| | Groq | **FREE** (30 RPM) | 14.4K RPD | Ultra-fast Llama/Gemma | +| | DeepSeek V3.2 | $0.27/$1.10 per 1M | None | Best price/quality reasoning | +| | xAI Grok-4 Fast | **$0.20/$0.50 per 1M** 🆕 | None | Fastest + tool calling, ultralow | +| | xAI Grok-4 (standard) | $0.20/$1.50 per 1M 🆕 | None | Reasoning flagship from xAI | +| | Mistral | Free trial + paid | Rate limited | European AI | +| | OpenRouter | Pay-per-use | None | 100+ models aggr. | +| **💰 CHEAP** | GLM-5 (via Z.AI) 🆕 | $0.5/1M | Daily 10AM | 128K output, newest flagship | +| | GLM-4.7 | $0.6/1M | Daily 10AM | Budget backup | +| | MiniMax M2.5 🆕 | $0.3/1M input | 5-hour rolling | Reasoning + agentic tasks | +| | MiniMax M2.1 | $0.2/1M | 5-hour rolling | Cheapest option | +| | Kimi K2.5 (Moonshot API) 🆕 | Pay-per-use | None | Direct Moonshot API access | +| | Kimi K2 | $9/mo flat | 10M tokens/mo | Predictable cost | +| **🆓 FREE** | iFlow | **$0** | Unlimited | 5 models unlimited | +| | Qwen | **$0** | Unlimited | 4 models unlimited | +| | Kiro | **$0** | Unlimited | Claude Sonnet/Haiku (AWS Builder) | -**💡 $0 Combo Stack:** Gemini CLI (180K/mo) → iFlow (unlimited: kimi-k2-thinking, qwen3-coder-plus, deepseek-r1) → Kiro (Claude for free) → Qwen (4 models, unlimited) — **Zero cost, never stops coding.** When Gemini quota runs out, OmniRoute auto-falls back to iFlow or Kiro with zero config. +> 🆕 **New models added (Mar 2026):** Grok-4 Fast family at $0.20/$0.50/M (benchmarked at 1143ms — 30% faster than Gemini 2.5 Flash), GLM-5 via Z.AI with 128K output, MiniMax M2.5 reasoning, DeepSeek V3.2 updated pricing, Kimi K2.5 via Moonshot direct API. + +**💡 $0 Combo Stack — The Complete Free Setup:** + +``` +Gemini CLI (180K/mo free) + → iFlow (unlimited: kimi-k2-thinking, qwen3-coder-plus, deepseek-r1) + → Kiro (Claude Sonnet 4.5 + Haiku — unlimited, via AWS Builder ID) + → Qwen (4 models — unlimited) + → Groq (14.4K req/day — ultra-fast) + → NVIDIA NIM (70+ models — 40 RPM forever) +``` + +**Zero cost. Never stops coding.** Configure this as one OmniRoute combo and all fallbacks happen automatically — no manual switching ever. --- diff --git a/open-sse/services/autoCombo/routerStrategy.ts b/open-sse/services/autoCombo/routerStrategy.ts new file mode 100644 index 0000000000..fc8812495b --- /dev/null +++ b/open-sse/services/autoCombo/routerStrategy.ts @@ -0,0 +1,124 @@ +/** + * RouterStrategy — Pluggable Routing Strategy System + * + * Inspired by ClawRouter commit 14c83c258 "refactor: extract routing into pluggable RouterStrategy system". + * Provides a RouterStrategy interface and two built-in implementations: + * - RulesStrategy (default): wraps the existing 6-factor scoring engine + * - CostStrategy: always picks cheapest available model + */ + +import type { ProviderCandidate, ScoredProvider } from "./scoring.js"; +import { scorePool } from "./scoring.js"; +import { getTaskFitness } from "./taskFitness.js"; + +export interface RoutingContext { + taskType: string; + requestHasTools?: boolean; + requestHasVision?: boolean; + estimatedInputTokens?: number; +} + +export interface RoutingDecision { + provider: string; + model: string; + strategy: string; + reason: string; + candidatesConsidered: number; + finalScore: number; +} + +export interface RouterStrategy { + readonly name: string; + readonly description: string; + select(pool: ProviderCandidate[], context: RoutingContext): RoutingDecision; +} + +// ── RulesStrategy: wraps 6-factor scoring engine ──────────────────────────── + +class RulesStrategyImpl implements RouterStrategy { + readonly name = "rules"; + readonly description = + "6-factor weighted scoring: quota, health, cost, latency, taskFit, stability"; + + select(pool: ProviderCandidate[], context: RoutingContext): RoutingDecision { + const eligible = pool.filter((c) => c.circuitBreakerState !== "OPEN"); + const ranked: ScoredProvider[] = scorePool( + eligible.length > 0 ? eligible : pool, + context.taskType, + undefined, + getTaskFitness + ); + const best = ranked[0]; + if (!best) throw new Error("[RulesStrategy] No candidates to score"); + return { + provider: best.provider, + model: best.model, + strategy: this.name, + reason: `RulesStrategy: score=${best.score.toFixed(3)} (quota=${best.factors.quota.toFixed(2)}, health=${best.factors.health.toFixed(2)}, cost=${best.factors.costInv.toFixed(2)}, taskFit=${best.factors.taskFit.toFixed(2)})`, + candidatesConsidered: ranked.length, + finalScore: best.score, + }; + } +} + +// ── CostStrategy: always picks cheapest healthy provider ───────────────────── + +class CostStrategyImpl implements RouterStrategy { + readonly name = "cost"; + readonly description = "Always selects cheapest available provider (by costPer1MTokens)"; + + select(pool: ProviderCandidate[], context: RoutingContext): RoutingDecision { + const healthy = pool.filter((c) => c.circuitBreakerState !== "OPEN"); + const candidates = healthy.length > 0 ? healthy : pool; + const sorted = [...candidates].sort((a, b) => a.costPer1MTokens - b.costPer1MTokens); + const best = sorted[0]; + if (!best) throw new Error("[CostStrategy] No candidates available"); + return { + provider: best.provider, + model: best.model, + strategy: this.name, + reason: `CostStrategy: cheapest at $${best.costPer1MTokens.toFixed(3)}/1M tokens`, + candidatesConsidered: candidates.length, + finalScore: best.costPer1MTokens === 0 ? 1.0 : 1 / best.costPer1MTokens, + }; + } +} + +// ── Registry ────────────────────────────────────────────────────────────────── + +const strategyRegistry = new Map(); + +const rulesStrategy = new RulesStrategyImpl(); +const costStrategy = new CostStrategyImpl(); + +strategyRegistry.set("rules", rulesStrategy); +strategyRegistry.set("cost", costStrategy); +strategyRegistry.set("eco", costStrategy); // alias + +export function getStrategy(name: string): RouterStrategy { + const strategy = strategyRegistry.get(name); + if (!strategy) { + console.warn(`[RouterStrategy] Strategy '${name}' not found, falling back to 'rules'`); + return rulesStrategy; + } + return strategy; +} + +export function registerStrategy(name: string, strategy: RouterStrategy): void { + if (strategyRegistry.has(name)) { + console.warn(`[RouterStrategy] Overwriting strategy '${name}'`); + } + strategyRegistry.set(name, strategy); +} + +export function listStrategies(): Array<{ name: string; description: string }> { + return [...strategyRegistry.entries()].map(([name, s]) => ({ name, description: s.description })); +} + +export function selectWithStrategy( + pool: ProviderCandidate[], + context: RoutingContext, + strategyName = "rules" +): RoutingDecision { + return getStrategy(strategyName).select(pool, context); +} diff --git a/open-sse/services/autoCombo/taskFitness.ts b/open-sse/services/autoCombo/taskFitness.ts index 15b1e0e285..e29b10ab17 100644 --- a/open-sse/services/autoCombo/taskFitness.ts +++ b/open-sse/services/autoCombo/taskFitness.ts @@ -24,10 +24,23 @@ const FITNESS_TABLE: Record> = { "deepseek-coder": 0.9, "deepseek-v3": 0.85, "deepseek-r1": 0.88, + "deepseek-chat": 0.84, // DeepSeek V3.2 Chat — strong code performance + "deepseek-v3.2": 0.86, // Explicit V3.2 alias qwen: 0.78, llama: 0.72, mistral: 0.75, mixtral: 0.77, + // Grok-4 fast — good code, ultra-low latency (1143ms P50) + "grok-4-fast": 0.8, + "grok-4": 0.82, + "grok-3": 0.8, + // Kimi K2.5 — agentic with tool calling, good at code tasks + "kimi-k2": 0.82, + // GLM-5 — Z.AI model with 128k output + "glm-5": 0.78, + // MiniMax M2.5 — reasoning support helps complex code + "minimax-m2.5": 0.75, + "minimax-m2": 0.72, }, review: { "claude-sonnet": 0.92, @@ -58,10 +71,15 @@ const FITNESS_TABLE: Record> = { "claude-sonnet": 0.92, "gemini-2.5-pro": 0.95, "gemini-pro": 0.88, + "gemini-3.1-pro": 0.95, // Gemini 3.1 Pro — 1M context, ideal for long analysis "gpt-4o": 0.85, o1: 0.9, o3: 0.93, "deepseek-r1": 0.88, + "deepseek-chat": 0.8, + "kimi-k2": 0.82, // Kimi K2.5 agentic — good for analysis + "glm-5": 0.78, // GLM-5 with 128k output for long analysis + "minimax-m2.5": 0.76, }, debugging: { "claude-sonnet": 0.93, @@ -87,8 +105,17 @@ const FITNESS_TABLE: Record> = { "claude-opus": 0.85, "gpt-4o": 0.85, "gemini-pro": 0.8, + "gemini-3.1-pro": 0.85, "deepseek-v3": 0.75, + "deepseek-chat": 0.74, "gemini-flash": 0.72, + // New models from ClawRouter analysis (2026-03-17): + "grok-4-fast": 0.72, // ultra-fast, suitable for all tasks + "grok-4": 0.74, + "grok-3": 0.73, + "kimi-k2": 0.76, // agentic multi-step tasks + "glm-5": 0.7, + "minimax-m2.5": 0.7, }, }; diff --git a/open-sse/services/emergencyFallback.ts b/open-sse/services/emergencyFallback.ts new file mode 100644 index 0000000000..6e6a0710e7 --- /dev/null +++ b/open-sse/services/emergencyFallback.ts @@ -0,0 +1,103 @@ +/** + * Emergency Fallback — Budget Exhaustion Redirect + * + * When a request fails due to budget exhaustion (HTTP 402 or budget keywords + * in the error body), optionally redirect to a free-tier model + * (default: nvidia/gpt-oss-120b at $0.00/M tokens). + * + * Inspired by ClawRouter: "gpt-oss-120b costs nothing and serves as + * automatic fallback when wallet is empty." + */ + +export interface EmergencyFallbackConfig { + enabled: boolean; + provider: string; + model: string; + triggerOn402: boolean; + triggerOnBudgetKeywords: boolean; + budgetKeywords: string[]; + /** Skip fallback for tool requests (gpt-oss-120b may not support structured tool calling) */ + skipForToolRequests: boolean; + maxOutputTokens: number; +} + +export const EMERGENCY_FALLBACK_CONFIG: EmergencyFallbackConfig = { + enabled: true, + provider: "nvidia", + model: "gpt-oss-120b", + triggerOn402: true, + triggerOnBudgetKeywords: true, + budgetKeywords: [ + "insufficient funds", + "insufficient_funds", + "budget exceeded", + "budget_exceeded", + "quota exceeded", + "quota_exceeded", + "billing", + "payment required", + "out of credits", + "no credits", + "credit limit", + "spending limit", + "saldo insuficiente", + "limite de gastos", + "cota excedida", + ], + skipForToolRequests: true, + maxOutputTokens: 4096, +}; + +export interface FallbackDecision { + shouldFallback: true; + reason: string; + provider: string; + model: string; + maxOutputTokens: number; +} + +export interface NoFallbackDecision { + shouldFallback: false; + reason: string; +} + +export type FallbackResult = FallbackDecision | NoFallbackDecision; + +export function shouldUseFallback( + status: number, + errorBody: string, + requestHasTools: boolean, + config: EmergencyFallbackConfig = EMERGENCY_FALLBACK_CONFIG +): FallbackResult { + if (!config.enabled) return { shouldFallback: false, reason: "emergency fallback disabled" }; + if (config.skipForToolRequests && requestHasTools) { + return { shouldFallback: false, reason: "skipped: request has tools" }; + } + if (config.triggerOn402 && status === 402) { + return { + shouldFallback: true, + reason: `HTTP 402 → emergency fallback to ${config.provider}/${config.model}`, + provider: config.provider, + model: config.model, + maxOutputTokens: config.maxOutputTokens, + }; + } + if (config.triggerOnBudgetKeywords && errorBody) { + const lowerBody = errorBody.toLowerCase(); + const matched = config.budgetKeywords.find((kw) => lowerBody.includes(kw.toLowerCase())); + if (matched) { + return { + shouldFallback: true, + reason: `Budget error detected ('${matched}') → emergency fallback to ${config.provider}/${config.model}`, + provider: config.provider, + model: config.model, + maxOutputTokens: config.maxOutputTokens, + }; + } + } + return { shouldFallback: false, reason: "no budget error detected" }; +} + +export function isFallbackDecision(result: FallbackResult): result is FallbackDecision { + return result.shouldFallback === true; +} diff --git a/open-sse/services/intentClassifier.ts b/open-sse/services/intentClassifier.ts new file mode 100644 index 0000000000..2dda9de825 --- /dev/null +++ b/open-sse/services/intentClassifier.ts @@ -0,0 +1,375 @@ +/** + * Multilingual Intent Detection for AutoCombo + * + * Classifies prompts as: code | reasoning | simple | medium + * using keywords in 9 languages (EN, PT-BR, ES, ZH, JA, RU, DE, KO, AR). + * + * Inspired by ClawRouter (BlockRunAI) multilingual routing system. + * Execution: purely synchronous, <1ms, no I/O. + */ + +export type IntentType = "code" | "reasoning" | "simple" | "medium"; + +export const CODE_KEYWORDS: readonly string[] = [ + // English + "function", + "class", + "import", + "def", + "SELECT", + "async", + "await", + "const", + "let", + "var", + "return", + "```", + "algorithm", + "compile", + "debug", + "refactor", + "typescript", + "python", + "javascript", + "code", + "implement", + "write a", + "create a component", + "endpoint", + "repository", + "deploy", + "install", + "script", + "api", + "database", + "query", + "schema", + "interface", + "generic", + "enum", + "module", + "package", + "dependency", + // Português (PT-BR) + "função", + "classe", + "importar", + "definir", + "consulta", + "assíncrono", + "aguardar", + "constante", + "variável", + "retornar", + "algoritmo", + "compilar", + "depurar", + "refatorar", + "código", + "implementar", + "criar um", + "componente", + "como fazer", + "repositório", + "configurar", + "instalar", + "banco de dados", + "escrever uma função", + "criar uma classe", + // Español + "función", + "clase", + "importar", + "definir", + "consulta", + "asíncrono", + "esperar", + "constante", + "variable", + "retornar", + "algoritmo", + "compilar", + "depurar", + "refactorizar", + "código", + "implementar", + // 中文 + "函数", + "类", + "导入", + "定义", + "查询", + "异步", + "等待", + "常量", + "变量", + "返回", + "算法", + "编译", + "调试", + "代码", + // 日本語 + "関数", + "クラス", + "インポート", + "非同期", + "定数", + "変数", + "コード", + "アルゴリズム", + // Русский + "функция", + "класс", + "импорт", + "запрос", + "асинхронный", + "константа", + "переменная", + "алгоритм", + "код", + // Deutsch + "funktion", + "klasse", + "importieren", + "abfrage", + "asynchron", + "konstante", + "variable", + "algorithmus", + "code", + // 한국어 + "함수", + "클래스", + "가져오기", + "정의", + "쿼리", + "비동기", + "대기", + "상수", + "변수", + "반환", + "코드", + // العربية + "دالة", + "فئة", + "استيراد", + "استعلام", + "غير متزامن", + "ثابت", + "متغير", + "كود", + "خوارزمية", +]; + +export const REASONING_KEYWORDS: readonly string[] = [ + // English + "prove", + "theorem", + "derive", + "step by step", + "chain of thought", + "formally", + "mathematical", + "proof", + "logically", + "analyze", + "reasoning", + "deduce", + "infer", + "hypothesis", + "convergence", + // Português (PT-BR) + "provar", + "teorema", + "derivar", + "passo a passo", + "cadeia de pensamento", + "formalmente", + "matemático", + "prova", + "logicamente", + "analisar", + "raciocínio", + "deduzir", + "inferir", + "hipótese", + "demonstrar", + "cálculo", + "equação diferencial", + "integral", + "otimização", + // Español + "demostrar", + "teorema", + "derivar", + "paso a paso", + "formalmente", + "matemático", + "lógicamente", + // 中文 + "证明", + "定理", + "推导", + "逐步", + "思维链", + "数学", + "逻辑", + "分析", + // 日本語 + "証明", + "定理", + "導出", + "論理的", + "分析", + // Русский + "доказать", + "теорема", + "шаг за шагом", + "математически", + "логически", + // Deutsch + "beweisen", + "theorem", + "schritt für schritt", + "mathematisch", + "logisch", + // 한국어 + "증명", + "정리", + "단계별", + "수학적", + "논리적", + // العربية + "إثبات", + "نظرية", + "خطوة بخطوة", + "رياضي", + "منطقياً", +]; + +export const SIMPLE_KEYWORDS: readonly string[] = [ + // English + "what is", + "define", + "translate", + "hello", + "yes or no", + "summarize", + "list", + "tell me", + "who is", + // Português (PT-BR) + "o que é", + "definir", + "traduzir", + "olá", + "oi", + "sim ou não", + "resumir", + "listar", + "me diga", + "quem é", + "quando foi", + "onde fica", + "explique brevemente", + "de forma simples", + // Español + "qué es", + "definir", + "traducir", + "hola", + "resumir", + "listar", + // 中文 + "什么是", + "定义", + "翻译", + "你好", + "总结", + "列出", + // Русский + "что такое", + "определить", + "перевести", + "привет", + "резюмировать", + // Deutsch + "was ist", + "definieren", + "übersetzen", + "hallo", + "zusammenfassen", + // 한국어 + "이란", + "정의", + "번역", + "안녕", + "요약", + // العربية + "ما هو", + "تعريف", + "ترجمة", + "مرحبا", + "ملخص", +]; + +/** + * Classify a prompt's intent using multilingual keyword matching. + * Priority: code > reasoning > simple > medium (default) + */ +export function classifyPromptIntent(prompt: string, systemPrompt?: string): IntentType { + const fullText = `${systemPrompt ?? ""} ${prompt}`.toLowerCase(); + const wordCount = prompt.trim().split(/\s+/).length; + + for (const kw of CODE_KEYWORDS) { + if (fullText.includes(kw.toLowerCase())) return "code"; + } + for (const kw of REASONING_KEYWORDS) { + if (fullText.includes(kw.toLowerCase())) return "reasoning"; + } + if (wordCount < 60) { + for (const kw of SIMPLE_KEYWORDS) { + if (fullText.includes(kw.toLowerCase())) return "simple"; + } + } + return "medium"; +} + +export interface IntentClassifierConfig { + enabled: boolean; + extraCodeKeywords?: string[]; + extraReasoningKeywords?: string[]; + extraSimpleKeywords?: string[]; + simpleMaxWords?: number; +} + +export const DEFAULT_INTENT_CONFIG: IntentClassifierConfig = { + enabled: true, + simpleMaxWords: 60, +}; + +export function classifyWithConfig( + prompt: string, + config: IntentClassifierConfig, + systemPrompt?: string +): IntentType { + if (!config.enabled) return "medium"; + const fullText = `${systemPrompt ?? ""} ${prompt}`.toLowerCase(); + const wordCount = prompt.trim().split(/\s+/).length; + const maxSimpleWords = config.simpleMaxWords ?? 60; + const codeKws = [...CODE_KEYWORDS, ...(config.extraCodeKeywords ?? [])]; + const reasoningKws = [...REASONING_KEYWORDS, ...(config.extraReasoningKeywords ?? [])]; + const simpleKws = [...SIMPLE_KEYWORDS, ...(config.extraSimpleKeywords ?? [])]; + for (const kw of codeKws) { + if (fullText.includes(kw.toLowerCase())) return "code"; + } + for (const kw of reasoningKws) { + if (fullText.includes(kw.toLowerCase())) return "reasoning"; + } + if (wordCount < maxSimpleWords) { + for (const kw of simpleKws) { + if (fullText.includes(kw.toLowerCase())) return "simple"; + } + } + return "medium"; +} diff --git a/open-sse/services/requestDedup.ts b/open-sse/services/requestDedup.ts new file mode 100644 index 0000000000..cb669fd309 --- /dev/null +++ b/open-sse/services/requestDedup.ts @@ -0,0 +1,120 @@ +/** + * Request Deduplication Service + * + * Deduplicates **concurrent** identical requests to the same upstream. + * Inspired by ClawRouter's dedup.ts (BlockRunAI / github.com/BlockRunAI/ClawRouter). + * + * IMPORTANT: In-memory only — does NOT persist across restarts and does NOT + * work across multiple process instances (no cross-instance dedup). + */ + +import { createHash } from "node:crypto"; + +export interface DedupConfig { + enabled: boolean; + maxTemperatureForDedup: number; + timeoutMs: number; +} + +export const DEFAULT_DEDUP_CONFIG: DedupConfig = { + enabled: true, + maxTemperatureForDedup: 0.1, + timeoutMs: 60_000, +}; + +export interface DedupResult { + result: T; + wasDeduplicated: boolean; + hash: string; +} + +const inflight = new Map>(); + +/** + * Compute a deterministic hash for a request body. + * Includes: model, messages, temperature, tools, tool_choice, max_tokens, response_format + * Excludes: stream, user, metadata (don't affect LLM output) + */ +export function computeRequestHash(requestBody: unknown): string { + const body = requestBody as Record; + const canonical = { + model: body.model ?? null, + messages: body.messages ?? null, + temperature: typeof body.temperature === "number" ? body.temperature : 1.0, + tools: body.tools ?? null, + tool_choice: body.tool_choice ?? null, + max_tokens: body.max_tokens ?? null, + response_format: body.response_format ?? null, + top_p: body.top_p ?? null, + frequency_penalty: body.frequency_penalty ?? null, + presence_penalty: body.presence_penalty ?? null, + }; + return createHash("sha256").update(JSON.stringify(canonical)).digest("hex").slice(0, 16); +} + +/** Determine whether a request should be deduplicated */ +export function shouldDeduplicate( + requestBody: unknown, + config: DedupConfig = DEFAULT_DEDUP_CONFIG +): boolean { + if (!config.enabled) return false; + const body = requestBody as Record; + if (body.stream === true) return false; + const temperature = typeof body.temperature === "number" ? body.temperature : 1.0; + if (temperature > config.maxTemperatureForDedup) return false; + return true; +} + +/** + * Execute a request with deduplication. + * Concurrent identical requests share one upstream call. + */ +export async function deduplicate( + hash: string, + fn: () => Promise, + config: DedupConfig = DEFAULT_DEDUP_CONFIG +): Promise> { + if (!config.enabled) { + return { result: await fn(), wasDeduplicated: false, hash }; + } + + const existing = inflight.get(hash); + if (existing) { + const result = (await existing) as T; + return { result, wasDeduplicated: true, hash }; + } + + let resolve!: (value: T) => void; + let reject!: (reason: unknown) => void; + const sharedPromise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + inflight.set(hash, sharedPromise as Promise); + + const timer = setTimeout(() => { + if (inflight.get(hash) === sharedPromise) inflight.delete(hash); + }, config.timeoutMs); + + try { + const result = await fn(); + resolve(result); + return { result, wasDeduplicated: false, hash }; + } catch (err) { + reject(err); + throw err; + } finally { + clearTimeout(timer); + if (inflight.get(hash) === sharedPromise) inflight.delete(hash); + } +} + +export function getInflightCount(): number { + return inflight.size; +} +export function getInflightHashes(): string[] { + return [...inflight.keys()]; +} +export function clearInflight(): void { + inflight.clear(); +} diff --git a/src/shared/constants/pricing.ts b/src/shared/constants/pricing.ts index 9ac0bd5cfd..7ae54873ee 100644 --- a/src/shared/constants/pricing.ts +++ b/src/shared/constants/pricing.ts @@ -129,12 +129,13 @@ export const DEFAULT_PRICING = { reasoning: 3.75, cache_creation: 0.3, }, + // Gemini 2.5 Flash Lite — preco corrigido via ClawRouter: $0.10/$0.40 (era $0.15/$1.25) "gemini-2.5-flash-lite": { - input: 0.15, - output: 1.25, - cached: 0.015, - reasoning: 1.875, - cache_creation: 0.15, + input: 0.1, + output: 0.4, + cached: 0.025, + reasoning: 0.6, + cache_creation: 0.1, }, }, @@ -451,10 +452,71 @@ export const DEFAULT_PRICING = { reasoning: 15.0, cache_creation: 3.0, }, + // Claude 4.5 Haiku — modelo eco mais recente da Anthropic (2025-10) + "claude-haiku-4-5-20251001": { + input: 1.0, + output: 5.0, + cached: 0.5, + reasoning: 7.5, + cache_creation: 1.0, + }, + "claude-haiku-4.5": { + input: 1.0, + output: 5.0, + cached: 0.5, + reasoning: 7.5, + cache_creation: 1.0, + }, + // Claude Sonnet 4.6 — maxOutput 64k tokens, $3/$15/M + "claude-sonnet-4-6-20251031": { + input: 3.0, + output: 15.0, + cached: 1.5, + reasoning: 22.5, + cache_creation: 3.0, + }, + "claude-sonnet-4.6": { + input: 3.0, + output: 15.0, + cached: 1.5, + reasoning: 22.5, + cache_creation: 3.0, + }, + // Claude Opus 4.6 — mais barato que Opus 4 ($5/$25 vs $15/$75) + "claude-opus-4-6-20251031": { + input: 5.0, + output: 25.0, + cached: 2.5, + reasoning: 37.5, + cache_creation: 5.0, + }, + "claude-opus-4.6": { + input: 5.0, + output: 25.0, + cached: 2.5, + reasoning: 37.5, + cache_creation: 5.0, + }, }, // Gemini gemini: { + // Gemini 3.1 Pro — novo flagship Google (2026-03-17) + // Context: 1.050.000 tokens | Max Output: 65.536 + "gemini-3.1-pro": { + input: 2.0, + output: 12.0, + cached: 0.25, + reasoning: 18.0, + cache_creation: 2.0, + }, + "gemini-3-1-pro": { + input: 2.0, + output: 12.0, + cached: 0.25, + reasoning: 18.0, + cache_creation: 2.0, + }, "gemini-3-pro-preview": { input: 2.0, output: 12.0, @@ -476,12 +538,53 @@ export const DEFAULT_PRICING = { reasoning: 3.75, cache_creation: 0.3, }, + // Gemini 2.5 Flash Lite — preco corrigido: $0.10/$0.40 (ClawRouter) "gemini-2.5-flash-lite": { - input: 0.15, - output: 1.25, - cached: 0.015, - reasoning: 1.875, - cache_creation: 0.15, + input: 0.1, + output: 0.4, + cached: 0.025, + reasoning: 0.6, + cache_creation: 0.1, + }, + }, + + // DeepSeek — API nativa (V3.2 Chat), separada de free providers + // Preco: $0.28/$0.42/M tokens (verificado via ClawRouter 2026-03-17) + deepseek: { + "deepseek-chat": { + input: 0.28, + output: 0.42, + cached: 0.014, + reasoning: 0.42, + cache_creation: 0.28, + }, + "deepseek-v3": { + input: 0.28, + output: 0.42, + cached: 0.014, + reasoning: 0.42, + cache_creation: 0.28, + }, + "deepseek-v3.2": { + input: 0.28, + output: 0.42, + cached: 0.014, + reasoning: 0.42, + cache_creation: 0.28, + }, + "deepseek-reasoner": { + input: 0.55, + output: 2.19, + cached: 0.14, + reasoning: 2.19, + cache_creation: 0.55, + }, + "deepseek-r1": { + input: 0.55, + output: 2.19, + cached: 0.14, + reasoning: 2.19, + cache_creation: 0.55, }, }, @@ -521,7 +624,7 @@ export const DEFAULT_PRICING = { }, }, - // Kimi + // Kimi (Moonshot) kimi: { "kimi-latest": { input: 1.0, @@ -530,6 +633,22 @@ export const DEFAULT_PRICING = { reasoning: 6.0, cache_creation: 1.0, }, + // Kimi K2.5 — acesso direto via Moonshot API + // Context: 262.144 tokens | Capabilities: reasoning, vision, agentic, tools + "kimi-k2.5": { + input: 0.6, + output: 3.0, + cached: 0.3, + reasoning: 4.5, + cache_creation: 0.6, + }, + "moonshot-kimi-k2.5": { + input: 0.6, + output: 3.0, + cached: 0.3, + reasoning: 4.5, + cache_creation: 0.6, + }, }, // MiniMax @@ -541,6 +660,22 @@ export const DEFAULT_PRICING = { reasoning: 3.0, cache_creation: 0.5, }, + // MiniMax M2.5 — mais barato que M2.1, reasoning + tools + // Context: 204.800 tokens | Max Output: 16.384 tokens + "minimax-m2.5": { + input: 0.3, + output: 1.2, + cached: 0.15, + reasoning: 1.8, + cache_creation: 0.3, + }, + "MiniMax-M2.5": { + input: 0.3, + output: 1.2, + cached: 0.15, + reasoning: 1.8, + cache_creation: 0.3, + }, }, // ─── Free-tier API Key Providers (nominal $0 pricing) ─── @@ -757,7 +892,85 @@ export const DEFAULT_PRICING = { }, }, - // Kiro (AWS) + // ───────────────────────────────────────────────────────────────────── + // xAI (Grok) — Grok-3 + Grok-4 Family + // Source: ClawRouter benchmarks 2026-03-17 + // Grok-4-fast-non-reasoning: 1143ms P50 (mais rapido do benchmark) + // ───────────────────────────────────────────────────────────────────── + xai: { + "grok-3": { + input: 3.0, + output: 15.0, + cached: 1.5, + reasoning: 22.5, + cache_creation: 3.0, + }, + "grok-3-mini": { + input: 0.3, + output: 0.5, + cached: 0.15, + reasoning: 0.75, + cache_creation: 0.3, + }, + // Grok-4 Fast Family — ultrabaratos ($0.20/$0.50/M) + "grok-4-fast-non-reasoning": { + input: 0.2, + output: 0.5, + cached: 0.1, + reasoning: 0.0, + cache_creation: 0.2, + }, + "grok-4-fast-reasoning": { + input: 0.2, + output: 0.5, + cached: 0.1, + reasoning: 0.75, + cache_creation: 0.2, + }, + "grok-4-1-fast-non-reasoning": { + input: 0.2, + output: 0.5, + cached: 0.1, + reasoning: 0.0, + cache_creation: 0.2, + }, + "grok-4-1-fast-reasoning": { + input: 0.2, + output: 0.5, + cached: 0.1, + reasoning: 0.75, + cache_creation: 0.2, + }, + "grok-4-0709": { + input: 0.2, + output: 1.5, + cached: 0.1, + reasoning: 2.25, + cache_creation: 0.2, + }, + }, + + // ───────────────────────────────────────────────────────────────────── + // Z.AI / ZhipuAI — GLM-5 Family + // Adicionados via ClawRouter 2026-03-17 | maxOutput: 128k tokens! + // ───────────────────────────────────────────────────────────────────── + zai: { + "glm-5": { + input: 1.0, + output: 3.2, + cached: 0.5, + reasoning: 4.8, + cache_creation: 1.0, + }, + "glm-5-turbo": { + input: 1.2, + output: 4.0, + cached: 0.6, + reasoning: 6.0, + cache_creation: 1.2, + }, + }, + kiro: { "claude-sonnet-4.5": { input: 3.0, diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index b35847f13b..8da4fa6bf7 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -390,6 +390,18 @@ export const APIKEY_PROVIDERS = { website: "https://cloud.google.com/vertex-ai", authHint: "Provide Service Account JSON or OAuth access_token", }, + // Z.AI (formerly ZhipuAI) — GLM-5 family with 128k output + // Added 2026-03-17 based on ClawRouter feature analysis + zai: { + id: "zai", + alias: "zai", + name: "Z.AI (GLM-5)", + icon: "psychology", + color: "#2563EB", + textIcon: "ZA", + website: "https://open.bigmodel.cn", + apiHint: "API key from https://open.bigmodel.cn/usercenter/apikeys", + }, }; export const OPENAI_COMPATIBLE_PREFIX = "openai-compatible-";