merge: resolve conflicts with origin/master - keep both local and remote features

This commit is contained in:
2026-06-22 11:40:16 +07:00
424 changed files with 24520 additions and 11731 deletions

39
open-sse/AGENTS.md Normal file
View File

@@ -0,0 +1,39 @@
# open-sse
Provider-agnostic SSE engine: one OpenAI-style request → any provider (LLM chat, image, embedding, tts, stt, search), streamed back in the client's format.
## Request lifecycle (chat)
`handlers/chatCore.js` → `services/model.js` `parseModel` (resolve `provider/model`) → **pre-translate hooks** (`rtk/` tool_result compress, `rtk/headroom.js` proxy compress, `rtk/caveman.js` system inject — all fail-open) → `executors/index.js` `getExecutor(provider)` → `translator/index.js` `translateRequest` (client format → provider format) → `executor.execute()` (streams upstream) → `translateResponse` (provider chunks → client format) → SSE out.
## Directory map
- `config/` — ALL constants/config (no hardcode elsewhere). `providers.js`/`registry/` (provider defs), `providerModels.js` (alias→models matrix), `runtimeConfig.js` (timeouts, token limits), `*Constants.js`.
- `translator/` — format conversion. `request/<from>-to-<to>.js`, `response/<from>-to-<to>.js`, `schema/` (enums: ROLE, CLAUDE_BLOCK…), `concerns/` (shared logic), `formats.js`+`formats/` (per-format). `index.js` is the registry/entry.
- `executors/` — per-provider upstream call. `base.js` (BaseExecutor), one file per special provider, `index.js` map.
- `providers/` — registry build + `capabilities.js` + `pricing.js`. Entry: `index.js` (PROVIDERS).
- `handlers/` — per-modality cores (chat/image/embedding/tts/stt/search) + sub-provider folders. `chatCore/` has the streaming/non-streaming/sse-to-json handlers.
- `rtk/` — request token-killer. `index.js` compresses `tool_result` content in-place (OpenAI/Claude/Kiro shapes); `filters/` per-tool compressors + `autodetect.js`; `headroom.js` external compress proxy; `caveman.js` system-prompt injector.
- `transformer/` — `responsesTransformer.js` (Chat Completions SSE → Codex Responses API SSE), `streamToJsonConverter.js`.
- `shared/` — cross-provider auth/identity: `clineAuth.js`, `machineId.js`, `qoder/`.
- `services/` — `model.js`, `provider.js`, `accountFallback.js`, `combo.js`, `compact.js`, `tokenRefresh/`+`tokenRefresh.js`, `oauthCredentialManager.js`, `usage/`, `projectId.js`, `kiroModels.js`/`qoderModels.js`.
- `utils/` — streamHandler, stream, sse, error, sessionManager, claudeCloaking, clientDetector, proxyFetch (patches global fetch), cursorProtobuf/cursorChecksum, ollamaTransform.
## Conventions
- Config-driven, DRY, camelCase. NEVER hardcode values, models, or block/role strings — use `config/` + `schema/` constants.
- Translator pipeline pivots through OpenAI as the intermediate format. A translator registered on the exact `source:target` pair (e.g. `claude:kiro`) runs as a **direct route**, skipping the lossy double-hop.
- Translators self-register via `register(from, to, reqFn, resFn)` as an import side-effect — new files MUST be imported in `translator/index.js`.
## How to add
- **Provider**: copy `providers/REGISTRY_TEMPLATE.js` → `providers/registry/{id}.js`; add models to `config/providerModels.js`. Generic providers need no executor (DefaultExecutor handles OpenAI-compatible APIs).
- **Executor** (only for non-standard upstream): subclass `BaseExecutor` (override `getBaseUrls`/`buildHeaders`/`buildUrl`/`execute`), register in `executors/index.js` map. `getExecutor` falls back to `DefaultExecutor` when absent.
- **Translator**: add `request|response/<from>-to-<to>.js` calling `register(...)`, then import it in `translator/index.js`. Reuse `schema/` + `concerns/` — don't re-implement parsing.
## Pitfalls
- OpenAI bridge is lossy (thinking, non-base64 images, tool ids, is_error) — prefer a direct route for fragile pairs.
- `registry/index.js` is an auto-generated static import list; regenerate it (don't hand-edit) after adding a `registry/{id}.js`. REGISTRY_TEMPLATE is excluded by design.
- Special binary/protobuf formats (kiro EventStream, cursor protobuf, commandcode NDJSON) don't round-trip through OpenAI — handle in their executor.
- `rtk/` + `headroom.js` mutate the request body in-place and are **fail-open**: any error returns null and leaves the body untouched — never throw out of them. RTK skips `is_error`/`status:"error"` tool results to preserve traces.

View File

@@ -1,8 +1,9 @@
import { platform, arch } from "os";
import { PROVIDERS, PROVIDER_OAUTH } from "./providers.js";
// === Gemini CLI ===
export const GEMINI_CLI_VERSION = "0.34.0";
export const GEMINI_CLI_API_CLIENT = "google-genai-sdk/1.41.0 gl-node/v22.19.0";
// === Gemini CLI === derive từ registry gemini-cli.transport
export const GEMINI_CLI_VERSION = PROVIDERS["gemini-cli"]?.cliVersion;
export const GEMINI_CLI_API_CLIENT = PROVIDERS["gemini-cli"]?.apiClient;
// Map Node arch to Gemini CLI arch string (x64/x86/arm64/...)
function geminiCLIArch() {
@@ -16,11 +17,13 @@ export function geminiCLIUserAgent(model = "unknown") {
}
// === GitHub Copilot ===
// Derive từ registry github.transport.copilot
const _ghCopilot = PROVIDERS.github?.copilot || {};
export const GITHUB_COPILOT = {
VSCODE_VERSION: "1.110.0",
COPILOT_CHAT_VERSION: "0.38.0",
USER_AGENT: "GitHubCopilotChat/0.38.0",
API_VERSION: "2025-04-01",
VSCODE_VERSION: _ghCopilot.vscodeVersion,
COPILOT_CHAT_VERSION: _ghCopilot.chatVersion,
USER_AGENT: _ghCopilot.userAgent,
API_VERSION: _ghCopilot.apiVersion,
};
// === Antigravity enums ===
@@ -152,43 +155,19 @@ export const LOAD_CODE_ASSIST_METADATA = {
export const CLAUDE_SYSTEM_PROMPT = "You are Claude Code, Anthropic's official CLI for Claude.";
export const ANTIGRAVITY_DEFAULT_SYSTEM = "You are Antigravity, a powerful agentic AI coding assistant designed by the Google Deepmind team working on Advanced Agentic Coding.You are pair programming with a USER to solve their coding task. The task may require creating a new codebase, modifying or debugging an existing codebase, or simply answering a question.**Absolute paths only****Proactiveness**";
// Proactive token refresh lead times per provider (ms)
export const REFRESH_LEAD_MS = {
codex: 5 * 24 * 60 * 60 * 1000, // 5 days
claude: 4 * 60 * 60 * 1000, // 4 hours
iflow: 24 * 60 * 60 * 1000, // 24 hours
qwen: 20 * 60 * 1000, // 20 minutes
"kimi-coding": 5 * 60 * 1000, // 5 minutes
antigravity: 5 * 60 * 1000, // 5 minutes
};
// Derive từ registry oauth.refreshLeadMs
export const REFRESH_LEAD_MS = Object.fromEntries(
Object.entries(PROVIDER_OAUTH).filter(([, o]) => o.refreshLeadMs).map(([id, o]) => [id, o.refreshLeadMs])
);
// OAuth endpoints
export const OAUTH_ENDPOINTS = {
google: {
token: "https://oauth2.googleapis.com/token",
auth: "https://accounts.google.com/o/oauth2/auth"
},
openai: {
token: "https://auth.openai.com/oauth/token",
auth: "https://auth.openai.com/oauth/authorize"
},
anthropic: {
token: "https://api.anthropic.com/v1/oauth/token",
auth: "https://api.anthropic.com/v1/oauth/authorize"
},
qwen: {
token: "https://qwen.ai/api/v1/oauth2/token",
auth: "https://qwen.ai/api/v1/oauth2/device/code"
},
iflow: {
token: "https://iflow.cn/oauth/token",
auth: "https://iflow.cn/oauth"
},
github: {
token: "https://github.com/login/oauth/access_token",
auth: "https://github.com/login/oauth/authorize",
deviceCode: "https://github.com/login/device/code"
}
google: { token: "https://oauth2.googleapis.com/token", auth: "https://accounts.google.com/o/oauth2/auth" },
openai: { token: PROVIDER_OAUTH["codex"]?.tokenUrl, auth: PROVIDER_OAUTH["codex"]?.authorizeUrl },
anthropic: { token: PROVIDER_OAUTH["claude"]?.tokenUrl, auth: "https://api.anthropic.com/v1/oauth/authorize" }, // ≠ claude.authorizeUrl (claude.ai login) — keep
qwen: { token: PROVIDER_OAUTH["qwen"]?.tokenUrl, auth: PROVIDER_OAUTH["qwen"]?.deviceCodeUrl },
iflow: { token: PROVIDER_OAUTH["iflow"]?.tokenUrl, auth: PROVIDER_OAUTH["iflow"]?.authorizeUrl },
github: { token: PROVIDER_OAUTH["github"]?.tokenUrl, auth: PROVIDER_OAUTH["github"]?.authorizeUrl, deviceCode: PROVIDER_OAUTH["github"]?.deviceCodeUrl },
};
// Generate Kimi OAuth custom headers

View File

@@ -15,6 +15,9 @@
* fiction. The suffix is stripped before the request leaves this process.
*/
import { extractThinking } from "../translator/concerns/thinkingUnified.js";
import { effortToBudget } from "../translator/concerns/thinking.js";
export const KIRO_AGENTIC_SUFFIX = "-agentic";
export const KIRO_THINKING_SUFFIX = "-thinking";
@@ -89,16 +92,48 @@ REMEMBER: When in doubt, write LESS per operation. Multiple small operations > o
`.trim();
/**
* Detect whether an inbound request is asking for reasoning / thinking output.
* Resolve the Kiro thinking budget requested by a client.
*
* Sources of intent (any one is enough):
* - HTTP header `Anthropic-Beta: ...interleaved-thinking...`
* - JSON `thinking.type === "enabled"` (Claude Messages API)
* - JSON `reasoning_effort` in {low, medium, high, auto} (OpenAI o1/o3)
* - JSON `reasoning.effort` in {low, medium, high, auto} (OpenAI Responses)
* - System prompt contains `<thinking_mode>enabled</thinking_mode>` or
* `<thinking_mode>interleaved</thinking_mode>` (AMP / Cursor)
* - Model name contains `thinking` or `-reason`
* Reuses the shared thinkingUnified parser (extractThinking) so every client
* shape (Claude output_config.effort / thinking.budget_tokens, OpenAI
* reasoning_effort / reasoning.effort, Gemini, Qwen) maps consistently. Explicit
* `none`/`off`/disabled wins and returns null (no prefix injected).
* buildThinkingSystemPrefix performs Kiro's final 1..32000 clamp.
*
* @param {object} body OpenAI/Claude-shaped request body
* @param {object} [headers] Original inbound HTTP headers (case-insensitive)
* @param {string} [model] Model id the caller asked for
* @returns {number|null} budget to inject, or null when thinking is disabled
*/
export function resolveKiroThinkingBudget(body, headers, model) {
const cfg = extractThinking(body);
if (cfg) {
if (cfg.mode === "none") return null;
if (cfg.mode === "budget") return cfg.budget;
if (cfg.mode === "level") return effortToBudget(cfg.level) ?? KIRO_THINKING_BUDGET_DEFAULT;
return KIRO_THINKING_BUDGET_DEFAULT;
}
if (headers) {
const beta = pickHeader(headers, "anthropic-beta");
if (typeof beta === "string" && beta.toLowerCase().includes("interleaved-thinking")) {
return KIRO_THINKING_BUDGET_DEFAULT;
}
}
if (containsThinkingModeTag(body)) return KIRO_THINKING_BUDGET_DEFAULT;
if (typeof model === "string" && model) {
const m = model.toLowerCase();
if (m.includes("thinking") || m.includes("-reason")) return KIRO_THINKING_BUDGET_DEFAULT;
}
return null;
}
/**
* Detect whether an inbound request is asking for reasoning / thinking output.
* Thin wrapper over resolveKiroThinkingBudget (single source of truth).
*
* @param {object} body OpenAI-shaped request body (post-translation)
* @param {object} [headers] Original inbound HTTP headers (case-insensitive)
@@ -106,44 +141,7 @@ REMEMBER: When in doubt, write LESS per operation. Multiple small operations > o
* @returns {boolean}
*/
export function isThinkingEnabled(body, headers, model) {
if (headers) {
const beta = pickHeader(headers, "anthropic-beta");
if (typeof beta === "string" && beta.toLowerCase().includes("interleaved-thinking")) {
return true;
}
}
if (body && typeof body === "object") {
const thinking = body.thinking;
if (thinking && typeof thinking === "object" && thinking.type === "enabled") {
const budget = Number(thinking.budget_tokens);
if (!Number.isFinite(budget) || budget > 0) {
return true;
}
}
const effort = body.reasoning_effort
?? (body.reasoning && typeof body.reasoning === "object" ? body.reasoning.effort : null);
if (typeof effort === "string") {
const v = effort.toLowerCase();
if (v && v !== "none" && (v === "low" || v === "medium" || v === "high" || v === "auto")) {
return true;
}
}
if (containsThinkingModeTag(body)) {
return true;
}
}
if (typeof model === "string" && model) {
const m = model.toLowerCase();
if (m.includes("thinking") || m.includes("-reason")) {
return true;
}
}
return false;
return resolveKiroThinkingBudget(body, headers, model) !== null;
}
/**

View File

@@ -0,0 +1,27 @@
// Central config for remote-media fetching security limits.
// Max bytes accepted from a remote image fetch (reject larger to prevent memory DoS).
export const MAX_IMAGE_BYTES = 10 * 1024 * 1024; // 10MB
// Fetch timeout for remote media.
export const FETCH_TIMEOUT_MS = 10000;
// Magic-byte signatures -> mime. Each entry: { sig:[bytes], offset, mime }.
// offset>0 for containers where the signature is not at byte 0 (e.g. webp).
export const IMAGE_SIGNATURES = [
{ sig: [0x89, 0x50, 0x4e, 0x47], offset: 0, mime: "image/png" },
{ sig: [0xff, 0xd8, 0xff], offset: 0, mime: "image/jpeg" },
{ sig: [0x47, 0x49, 0x46, 0x38], offset: 0, mime: "image/gif" },
{ sig: [0x52, 0x49, 0x46, 0x46], offset: 0, mime: "image/webp", verifyWebp: true },
{ sig: [0x42, 0x4d], offset: 0, mime: "image/bmp" },
];
// Hostnames/IPs that must never be fetched (SSRF guard for loopback + cloud metadata).
export const BLOCKED_HOSTS = new Set([
"localhost",
"127.0.0.1",
"0.0.0.0",
"::1",
"169.254.169.254", // AWS/GCP/Azure IMDS
"metadata.google.internal",
]);

View File

@@ -1,845 +1,12 @@
import { PROVIDERS } from "./providers.js";
import { buildTtsProviderModels } from "./ttsModels.js";
import REGISTRY from "../providers/registry/index.js";
// PROVIDER_MODELS now built from providers/registry (transport + models co-located)
import { PROVIDER_MODELS } from "../providers/index.js";
import { modelQuotaFamily, modelStrip, modelTargetFormat } from "../providers/models/schema.js";
import { CODEX_REVIEW_SUFFIX } from "../providers/models/helpers.js";
// Provider models - Single source of truth
// Key = alias (cc, cx, gc, qw, if, ag, gh for OAuth; id for API Key)
// Field "provider" for special cases (e.g. AntiGravity models that call different backends)
export { PROVIDER_MODELS };
const CODEX_REVIEW_SUFFIX = "-review";
function withCodexReviewModels(models) {
return models.flatMap((model) => {
if ((model.type || "llm") !== "llm" || model.id.endsWith(CODEX_REVIEW_SUFFIX)) {
return [model];
}
return [
model,
{
...model,
id: `${model.id}${CODEX_REVIEW_SUFFIX}`,
name: `${model.name} Review`,
upstreamModelId: model.upstreamModelId || model.id,
quotaFamily: "review",
},
];
});
}
export const PROVIDER_MODELS = {
// OAuth Providers (using alias)
cc: [ // Claude Code
{ id: "claude-opus-4-8", name: "Claude Opus 4.8" },
{ id: "claude-opus-4-7", name: "Claude Opus 4.7" },
{ id: "claude-opus-4-6", name: "Claude Opus 4.6" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ id: "claude-opus-4-5-20251101", name: "Claude 4.5 Opus" },
{ id: "claude-sonnet-4-5-20250929", name: "Claude 4.5 Sonnet" },
{ id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" },
],
cx: withCodexReviewModels([ // OpenAI Codex
{ id: "gpt-5.5", name: "GPT 5.5" },
{ id: "gpt-5.4", name: "GPT 5.4" },
{ id: "gpt-5.4-mini", name: "GPT 5.4 Mini" },
// GPT 5.3 Codex - all thinking levels
{ id: "gpt-5.3-codex", name: "GPT 5.3 Codex" },
{ id: "gpt-5.3-codex-xhigh", name: "GPT 5.3 Codex (xHigh)" },
{ id: "gpt-5.3-codex-high", name: "GPT 5.3 Codex (High)" },
{ id: "gpt-5.3-codex-low", name: "GPT 5.3 Codex (Low)" },
{ id: "gpt-5.3-codex-none", name: "GPT 5.3 Codex (None)" },
{ id: "gpt-5.3-codex-spark", name: "GPT 5.3 Codex Spark" },
// Image models (uses image_generation tool, requires Plus/Pro plan)
{ id: "gpt-5.5-image", name: "GPT 5.5 Image", type: "image", capabilities: ["text2img", "edit"], params: ["size", "quality", "background", "image_detail", "output_format"] },
{ id: "gpt-5.4-image", name: "GPT 5.4 Image", type: "image", capabilities: ["text2img", "edit"], params: ["size", "quality", "background", "image_detail", "output_format"] },
{ id: "gpt-5.3-image", name: "GPT 5.3 Image", type: "image", capabilities: ["text2img", "edit"], params: ["size", "quality", "background", "image_detail", "output_format"] },
]),
gc: [ // Gemini CLI
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" },
{ id: "gemini-3-pro-preview", name: "Gemini 3 Pro Preview" },
],
qw: [ // Qwen Code
// { id: "qwen3-coder-next", name: "Qwen3 Coder Next" },
{ id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" },
{ id: "qwen3-coder-flash", name: "Qwen3 Coder Flash" },
{ id: "vision-model", name: "Qwen3 Vision Model" },
{ id: "coder-model", name: "Qwen3.6 Coder Model" },
],
if: [ // iFlow AI
{ id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" },
{ id: "qwen3-max", name: "Qwen3 Max" },
{ id: "qwen3-vl-plus", name: "Qwen3 VL Plus" },
{ id: "qwen3-max-preview", name: "Qwen3 Max Preview" },
{ id: "qwen3-235b", name: "Qwen3 235B A22B" },
{ id: "qwen3-235b-a22b-instruct", name: "Qwen3 235B A22B Instruct" },
{ id: "qwen3-235b-a22b-thinking-2507", name: "Qwen3 235B A22B Thinking" },
{ id: "qwen3-32b", name: "Qwen3 32B" },
{ id: "kimi-k2", name: "Kimi K2" },
{ id: "deepseek-v3.2", name: "DeepSeek V3.2 Exp" },
{ id: "deepseek-v3.1", name: "DeepSeek V3.1 Terminus" },
{ id: "deepseek-v3", name: "DeepSeek V3 671B" },
{ id: "deepseek-r1", name: "DeepSeek R1" },
{ id: "glm-4.7", name: "GLM 4.7" },
{ id: "iflow-rome-30ba3b", name: "iFlow ROME" },
],
ag: [ // Antigravity - special case: models call different backends
{ id: "gemini-3-flash-agent", name: "Gemini 3.5 Flash (High)" },
{ id: "gemini-3.5-flash-low", name: "Gemini 3.5 Flash (Medium)" },
{ id: "gemini-3.5-flash-extra-low", name: "Gemini 3.5 Flash (Low)" },
{ id: "gemini-pro-agent", name: "Gemini 3.1 Pro (High)" },
{ id: "gemini-3.1-pro-low", name: "Gemini 3.1 Pro (Low)" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 (Thinking)" },
{ id: "claude-opus-4-6-thinking", name: "Claude Opus 4.6 (Thinking)" },
{ id: "gpt-oss-120b-medium", name: "GPT-OSS 120B (Medium)" },
{ id: "gemini-3-flash", name: "Gemini 3 Flash", thinking: false }, // command model; AG strips thinking
],
gh: [ // GitHub Copilot - OpenAI models
{ id: "gpt-3.5-turbo", name: "GPT-3.5 Turbo" },
{ id: "gpt-4", name: "GPT-4" },
{ id: "gpt-4o", name: "GPT-4o" },
{ id: "gpt-4o-mini", name: "GPT-4o mini" },
{ id: "gpt-4.1", name: "GPT-4.1" },
{ id: "gpt-5-mini", name: "GPT-5 Mini" },
{ id: "gpt-5.2", name: "GPT-5.2" },
{ id: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
{ id: "gpt-5.4", name: "GPT-5.4" },
{ id: "gpt-5.4-mini", name: "GPT-5.4 Mini" },
// GitHub Copilot - Anthropic models
{ id: "claude-haiku-4.5", name: "Claude Haiku 4.5" },
{ id: "claude-opus-4.5", name: "Claude Opus 4.5" },
{ id: "claude-sonnet-4", name: "Claude Sonnet 4" },
{ id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5" },
{ id: "claude-sonnet-4.6", name: "Claude Sonnet 4.6" },
{ id: "claude-opus-4.6", name: "Claude Opus 4.6" },
{ id: "claude-opus-4.7", name: "Claude Opus 4.7" },
// GitHub Copilot - Google models
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro" },
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash" },
{ id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro" },
// GitHub Copilot - Other models
{ id: "grok-code-fast-1", name: "Grok Code Fast 1" },
{ id: "oswe-vscode-prime", name: "Raptor Mini" },
{ id: "goldeneye-free-auto", name: "GoldenEye" },
// GitHub Copilot - Embedding models
{ id: "text-embedding-3-small", name: "Text Embedding 3 Small (GitHub)", type: "embedding" },
{ id: "text-embedding-3-large", name: "Text Embedding 3 Large (GitHub)", type: "embedding" },
],
kr: [ // Kiro AI
// --- Base Claude variants ---
// { id: "claude-opus-4.5", name: "Claude Opus 4.5" },
{ id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5" },
{ id: "claude-haiku-4.5", name: "Claude Haiku 4.5" },
{ id: "deepseek-3.2", name: "DeepSeek 3.2", strip: ["image", "audio"] },
{ id: "qwen3-coder-next", name: "Qwen3 Coder Next", strip: ["image", "audio"] },
{ id: "glm-5", name: "GLM 5" },
{ id: "MiniMax-M2.5", name: "MiniMax M2.5" },
// --- Thinking variants (alias to base; thinking is enabled at request time
// via <thinking_mode>enabled</thinking_mode> system-prompt injection) ---
{ id: "claude-sonnet-4.5-thinking", name: "Claude Sonnet 4.5 (Thinking)" },
{ id: "claude-haiku-4.5-thinking", name: "Claude Haiku 4.5 (Thinking)" },
// --- Agentic variants (synthetic; same upstream model + chunked-write
// system prompt to dodge Kiro's 2-3 min server timeout on big writes) ---
{ id: "claude-sonnet-4.5-agentic", name: "Claude Sonnet 4.5 (Agentic)" },
{ id: "claude-haiku-4.5-agentic", name: "Claude Haiku 4.5 (Agentic)" },
{ id: "claude-sonnet-4.5-thinking-agentic", name: "Claude Sonnet 4.5 (Thinking + Agentic)" },
{ id: "claude-haiku-4.5-thinking-agentic", name: "Claude Haiku 4.5 (Thinking + Agentic)" },
],
qd: [ // Qoder - tier + frontier models (server-published catalog)
// Tier models — pick a quality/cost tradeoff
{ id: "auto", name: "Qoder Auto" },
{ id: "ultimate", name: "Qoder Ultimate" },
{ id: "performance", name: "Qoder Performance" },
{ id: "efficient", name: "Qoder Efficient" },
{ id: "lite", name: "Qoder Lite" },
// Frontier models — pin a specific backing model
{ id: "qmodel", name: "Qwen 3.6 Plus (Qoder)" },
{ id: "qmodel_latest", name: "Qoder Qwen 3.7 Max" },
{ id: "dmodel", name: "DeepSeek V4 Pro (Qoder)" },
{ id: "dfmodel", name: "DeepSeek V4 Flash (Qoder)" },
{ id: "gm51model", name: "GLM 5.1 (Qoder)" },
{ id: "kmodel", name: "Kimi K2.6 (Qoder)" },
{ id: "mmodel", name: "MiniMax M2.7 (Qoder)" },
],
cu: [ // Cursor IDE
{ id: "default", name: "Auto (Server Picks)" },
{ id: "claude-4.5-opus-high-thinking", name: "Claude 4.5 Opus High Thinking" },
{ id: "claude-4.5-opus-high", name: "Claude 4.5 Opus High" },
{ id: "claude-4.5-sonnet-thinking", name: "Claude 4.5 Sonnet Thinking" },
{ id: "claude-4.5-sonnet", name: "Claude 4.5 Sonnet" },
{ id: "claude-4.5-haiku", name: "Claude 4.5 Haiku" },
{ id: "claude-4.5-opus", name: "Claude 4.5 Opus" },
{ id: "gpt-5.2-codex", name: "GPT 5.2 Codex" },
{ id: "claude-4.6-opus-max", name: "Claude 4.6 Opus Max" },
{ id: "claude-4.6-sonnet-medium-thinking", name: "Claude 4.6 Sonnet Medium Thinking" },
{ id: "kimi-k2.5", name: "Kimi K2.5" },
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" },
{ id: "gpt-5.2", name: "GPT 5.2" },
{ id: "gpt-5.3-codex", name: "GPT 5.3 Codex" },
],
kmc: [ // Kimi Coding
{ id: "kimi-k2.6", name: "Kimi K2.6" },
{ id: "kimi-k2.5", name: "Kimi K2.5" },
{ id: "kimi-k2.5-thinking", name: "Kimi K2.5 Thinking" },
{ id: "kimi-latest", name: "Kimi Latest" },
],
kc: [ // KiloCode
{ id: "anthropic/claude-sonnet-4-20250514", name: "Claude Sonnet 4" },
{ id: "anthropic/claude-opus-4-20250514", name: "Claude Opus 4" },
{ id: "google/gemini-2.5-pro", name: "Gemini 2.5 Pro" },
{ id: "google/gemini-2.5-flash", name: "Gemini 2.5 Flash" },
{ id: "openai/gpt-4.1", name: "GPT-4.1" },
{ id: "openai/o3", name: "o3" },
{ id: "deepseek/deepseek-chat", name: "DeepSeek Chat" },
{ id: "deepseek/deepseek-reasoner", name: "DeepSeek Reasoner" },
],
"opencode-go": [ // OpenCode Go subscription (API key)
{ id: "kimi-k2.6", name: "Kimi K2.6" },
{ id: "kimi-k2.5", name: "Kimi K2.5" },
{ id: "glm-5.1", name: "GLM 5.1" },
{ id: "glm-5", name: "GLM 5" },
{ id: "qwen3.5-plus", name: "Qwen 3.5 Plus" },
{ id: "qwen3.6-plus", name: "Qwen 3.6 Plus" },
{ id: "mimo-v2-pro", name: "MiMo V2 Pro" },
{ id: "mimo-v2-omni", name: "MiMo V2 Omni" },
{ id: "minimax-m2.7", name: "MiniMax M2.7", targetFormat: "claude" },
{ id: "minimax-m2.5", name: "MiniMax M2.5", targetFormat: "claude" },
],
oc: [ // OpenCode
// { id: "nemotron-3-super-free", name: "Nemotron 3 Super" },
// { id: "qwen3.6-plus-free", name: "Qwen 3.6 Plus" },
// { id: "big-pickle", name: "Big Pickle", targetFormat: "claude" },
// { id: "minimax-m2.5-free", name: "MiniMax M2.5", targetFormat: "claude" },
// { id: "trinity-large-preview-free", name: "Trinity Large Preview" },
],
mmf: [ // MiMo Free — free channel only serves mimo-auto
{ id: "mimo-auto", name: "MiMo Auto" },
],
cl: [ // Cline
{ id: "anthropic/claude-opus-4.7", name: "Claude Opus 4.7" },
{ id: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6" },
{ id: "anthropic/claude-opus-4.6", name: "Claude Opus 4.6" },
{ id: "openai/gpt-5.3-codex", name: "GPT-5.3 Codex" },
{ id: "openai/gpt-5.4", name: "GPT-5.4" },
{ id: "google/gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview" },
{ id: "google/gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview" },
{ id: "kwaipilot/kat-coder-pro", name: "KAT Coder Pro" },
],
// API Key Providers (alias = id)
openai: [
// Flagship models
{ id: "gpt-5.4", name: "GPT-5.4" },
{ id: "gpt-5.4-mini", name: "GPT-5.4 Mini" },
{ id: "gpt-5.4-nano", name: "GPT-5.4 Nano" },
{ id: "gpt-5.2", name: "GPT-5.2" },
{ id: "gpt-5.1", name: "GPT-5.1" },
{ id: "gpt-5", name: "GPT-5" },
{ id: "gpt-5-mini", name: "GPT-5 Mini" },
{ id: "gpt-5-nano", name: "GPT-5 Nano" },
{ id: "gpt-4o", name: "GPT-4o" },
{ id: "gpt-4o-mini", name: "GPT-4o Mini" },
{ id: "gpt-4-turbo", name: "GPT-4 Turbo" },
{ id: "gpt-4.1", name: "GPT-4.1" },
{ id: "gpt-4.1-mini", name: "GPT-4.1 Mini" },
{ id: "gpt-4.1-nano", name: "GPT-4.1 Nano" },
// Reasoning models
{ id: "o3", name: "O3" },
{ id: "o3-mini", name: "O3 Mini" },
{ id: "o3-pro", name: "O3 Pro" },
{ id: "o4-mini", name: "O4 Mini" },
{ id: "o1", name: "O1" },
{ id: "o1-mini", name: "O1 Mini" },
// Embedding models
{ id: "text-embedding-3-large", name: "Text Embedding 3 Large", type: "embedding" },
{ id: "text-embedding-3-small", name: "Text Embedding 3 Small", type: "embedding" },
{ id: "text-embedding-ada-002", name: "Text Embedding Ada 002", type: "embedding" },
// TTS models
{ id: "tts-1", name: "TTS-1", type: "tts" },
{ id: "tts-1-hd", name: "TTS-1 HD", type: "tts" },
{ id: "gpt-4o-mini-tts", name: "GPT-4o Mini TTS", type: "tts" },
// STT models
{ id: "whisper-1", name: "Whisper 1", type: "stt", params: ["language", "response_format", "temperature", "prompt"] },
{ id: "gpt-4o-transcribe", name: "GPT-4o Transcribe", type: "stt", params: ["language", "response_format", "temperature", "prompt"] },
{ id: "gpt-4o-mini-transcribe", name: "GPT-4o Mini Transcribe", type: "stt", params: ["language", "response_format", "temperature", "prompt"] },
// Image models
{ id: "gpt-image-1", name: "GPT Image 1", type: "image", params: ["n", "size", "quality", "response_format"] },
{ id: "dall-e-3", name: "DALL-E 3", type: "image", params: ["size", "quality", "style", "response_format"] },
{ id: "dall-e-2", name: "DALL-E 2", type: "image", params: ["n", "size", "response_format"] },
],
anthropic: [
{ id: "claude-sonnet-4-20250514", name: "Claude Sonnet 4" },
{ id: "claude-opus-4-20250514", name: "Claude Opus 4" },
{ id: "claude-3-5-sonnet-20241022", name: "Claude 3.5 Sonnet" },
],
gemini: [
// Gemini 3.1 series
{ id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview" },
{ id: "gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview" },
// Gemini 3 series
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" },
// Gemini 2.5 series
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro" },
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash" },
{ id: "gemini-2.5-flash-lite", name: "Gemini 2.5 Flash Lite" },
// Gemini 2.0 series (retiring June 1, 2026)
{ id: "gemini-2.0-flash", name: "Gemini 2.0 Flash" },
{ id: "gemini-2.0-flash-lite", name: "Gemini 2.0 Flash Lite" },
{ id: "gemma-4-31b-it", name: "Gemma 4 31B IT" },
// Embedding models
{ id: "gemini-embedding-2-preview", name: "Gemini Embedding 2 Preview", type: "embedding" },
{ id: "gemini-embedding-001", name: "Gemini Embedding 001", type: "embedding" },
{ id: "text-embedding-005", name: "Text Embedding 005", type: "embedding" },
{ id: "text-embedding-004", name: "Text Embedding 004 (Legacy)", type: "embedding" },
// Image models (Nano Banana)
{ id: "gemini-3.1-flash-image-preview", name: "Gemini 3.1 Flash Image (Nano Banana 2)", type: "image", params: [] },
{ id: "gemini-3-pro-image-preview", name: "Gemini 3 Pro Image (Nano Banana Pro)", type: "image", params: [] },
{ id: "gemini-2.5-flash-image", name: "Gemini 2.5 Flash Image (Nano Banana)", type: "image", params: [] },
// STT models (multimodal generateContent)
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro (Best)", type: "stt", params: ["language", "prompt"] },
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", type: "stt", params: ["language", "prompt"] },
{ id: "gemini-2.5-flash-lite", name: "Gemini 2.5 Flash Lite (Cheapest)", type: "stt", params: ["language", "prompt"] },
{ id: "gemini-2.0-flash", name: "Gemini 2.0 Flash", type: "stt", params: ["language", "prompt"] },
],
openrouter: [
// Embedding models
{ id: "openai/text-embedding-3-large", name: "OpenAI Text Embedding 3 Large", type: "embedding" },
{ id: "openai/text-embedding-3-small", name: "OpenAI Text Embedding 3 Small", type: "embedding" },
{ id: "openai/text-embedding-ada-002", name: "OpenAI Text Embedding Ada 002", type: "embedding" },
{ id: "qwen/qwen3-embedding-8b", name: "Qwen3 Embedding 8B", type: "embedding" },
{ id: "perplexity/pplx-embed-v1-4b", name: "Perplexity Embed V1 4B", type: "embedding" },
{ id: "perplexity/pplx-embed-v1-0.6b", name: "Perplexity Embed V1 0.6B", type: "embedding" },
{ id: "nvidia/llama-nemotron-embed-vl-1b-v2:free", name: "NVIDIA Nemotron Embed VL 1B V2 (Free)", type: "embedding" },
// TTS models
{ id: "openai/gpt-4o-mini-tts", name: "GPT-4o Mini TTS", type: "tts" },
{ id: "openai/tts-1-hd", name: "TTS-1 HD", type: "tts" },
{ id: "openai/tts-1", name: "TTS-1", type: "tts" },
// Image models
{ id: "openai/dall-e-3", name: "DALL-E 3 (via OpenRouter)", type: "image", params: ["size", "quality", "style", "response_format"] },
{ id: "openai/gpt-image-1", name: "GPT Image 1 (via OpenRouter)", type: "image", params: ["n", "size", "quality", "response_format"] },
{ id: "google/imagen-3.0-generate-002", name: "Imagen 3 (via OpenRouter)", type: "image", params: ["n", "size"] },
{ id: "black-forest-labs/FLUX.1-schnell", name: "FLUX.1 Schnell (via OpenRouter)", type: "image", params: ["n", "size"] },
],
glm: [
{ id: "glm-5.1", name: "GLM 5.1" },
{ id: "glm-5", name: "GLM 5" },
{ id: "glm-4.7", name: "GLM 4.7" },
{ id: "glm-4.6v", name: "GLM 4.6V (Vision)" },
],
"glm-cn": [
{ id: "glm-5.1", name: "GLM 5.1" },
{ id: "glm-5", name: "GLM 5" },
{ id: "glm-4.7", name: "GLM-4.7" },
{ id: "glm-4.6", name: "GLM-4.6" },
{ id: "glm-4.5-air", name: "GLM-4.5-Air" },
],
kimi: [
{ id: "kimi-k2.6", name: "Kimi K2.6" },
{ id: "kimi-k2.5", name: "Kimi K2.5" },
{ id: "kimi-k2.5-thinking", name: "Kimi K2.5 Thinking" },
{ id: "kimi-latest", name: "Kimi Latest" },
],
minimax: [
{ id: "MiniMax-M3", name: "MiniMax M3", targetFormat: "claude" },
{ id: "MiniMax-M2.7", name: "MiniMax M2.7" },
{ id: "MiniMax-M2.5", name: "MiniMax M2.5" },
{ id: "MiniMax-M2.1", name: "MiniMax M2.1" },
// Image models
{ id: "minimax-image-01", name: "MiniMax Image 01", type: "image", params: ["n", "size", "response_format"] },
],
blackbox: [
{ id: "gpt-4o", name: "GPT-4o" },
{ id: "gpt-4o-mini", name: "GPT-4o mini" },
{ id: "claude-sonnet-4.6", name: "Claude Sonnet 4.6" },
{ id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5" },
{ id: "claude-opus-4.6", name: "Claude Opus 4.6" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 (Legacy)" },
{ id: "claude-opus-4-6", name: "Claude Opus 4.6 (Legacy)" },
{ id: "deepseek-chat", name: "DeepSeek Chat" },
{ id: "deepseek-v3-671b", name: "DeepSeek V3 671B" },
{ id: "deepseek-r1", name: "DeepSeek R1" },
{ id: "o1", name: "OpenAI o1" },
{ id: "o3-mini", name: "OpenAI o3-mini" },
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash" },
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" },
{ id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" },
{ id: "qwen3-max", name: "Qwen3 Max" },
{ id: "qwen3-vl-plus", name: "Qwen3 VL Plus" },
],
"minimax-cn": [
{ id: "MiniMax-M3", name: "MiniMax M3", targetFormat: "claude" },
{ id: "MiniMax-M2.7", name: "MiniMax M2.7" },
{ id: "MiniMax-M2.5", name: "MiniMax M2.5" },
{ id: "MiniMax-M2.1", name: "MiniMax M2.1" },
],
alicode: [
{ id: "qwen3.5-plus", name: "Qwen3.5 Plus" },
{ id: "kimi-k2.5", name: "Kimi K2.5" },
{ id: "glm-5", name: "GLM 5" },
{ id: "MiniMax-M2.5", name: "MiniMax M2.5" },
{ id: "qwen3-max-2026-01-23", name: "Qwen3 Max" },
{ id: "qwen3-coder-next", name: "Qwen3 Coder Next" },
{ id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" },
{ id: "glm-4.7", name: "GLM 4.7" },
],
"alicode-intl": [
{ id: "qwen3.5-plus", name: "Qwen3.5 Plus" },
{ id: "kimi-k2.5", name: "Kimi K2.5" },
{ id: "glm-5", name: "GLM 5" },
{ id: "MiniMax-M2.5", name: "MiniMax M2.5" },
{ id: "qwen3-coder-next", name: "Qwen3 Coder Next" },
{ id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" },
{ id: "glm-4.7", name: "GLM 4.7" },
],
"volcengine-ark": [
{ id: "Doubao-Seed-2.0-Code", name: "Doubao-Seed-2.0-Code" },
{ id: "Doubao-Seed-2.0-pro", name: "Doubao-Seed-2.0-pro" },
{ id: "Doubao-Seed-2.0-lite", name: "Doubao-Seed-2.0-lite" },
{ id: "Doubao-Seed-Code", name: "Doubao-Seed-Code" },
{ id: "DeepSeek-V4-Flash", name: "DeepSeek-V4-Flash" },
{ id: "DeepSeek-V4-Pro", name: "DeepSeek-V4-Pro" },
{ id: "GLM-5.1", name: "GLM-5.1" },
{ id: "MiniMax-M2.7", name: "MiniMax-M2.7" },
{ id: "Kimi-K2.6", name: "Kimi-K2.6" },
],
"cloudflare-ai": [
{ id: "@cf/meta/llama-3.2-1b-instruct", name: "Llama 3.2 1B Instruct" },
{ id: "@cf/meta/llama-3.2-3b-instruct", name: "Llama 3.2 3B Instruct" },
{ id: "@cf/meta/llama-3.1-8b-instruct-fp8-fast", name: "Llama 3.1 8B Instruct FP8 Fast" },
{ id: "@cf/meta/llama-3.1-8b-instruct-awq", name: "Llama 3.1 8B Instruct AWQ" },
{ id: "@cf/mistralai/mistral-small-3.1-24b-instruct", name: "Mistral Small 3.1 24B Instruct" },
{ id: "@cf/meta/llama-3.1-70b-instruct-fp8-fast", name: "Llama 3.1 70B Instruct FP8 Fast" },
{ id: "@cf/meta/llama-3.3-70b-instruct-fp8-fast", name: "Llama 3.3 70B Instruct FP8 Fast" },
{ id: "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b", name: "DeepSeek R1 Distill Qwen 32B" },
{ id: "@cf/moonshotai/kimi-k2.5", name: "Kimi K2.5" },
{ id: "@cf/moonshotai/kimi-k2.6", name: "Kimi K2.6" },
{ id: "@cf/zai-org/glm-4.7-flash", name: "GLM 4.7 Flash" },
{ id: "@cf/qwen/qwq-32b", name: "QwQ 32B" },
{ id: "@cf/qwen/qwen2.5-coder-32b-instruct", name: "Qwen 2.5 Coder 32B Instruct" },
{ id: "@cf/black-forest-labs/flux-2-klein-9b", name: "FLUX.2 Klein 9B", type: "image", params: ["size"] },
{ id: "@cf/black-forest-labs/flux-2-klein-4b", name: "FLUX.2 Klein 4B", type: "image", params: ["size"] },
{ id: "@cf/black-forest-labs/flux-2-dev", name: "FLUX.2 Dev", type: "image", params: ["size"] },
{ id: "@cf/leonardo/lucid-origin", name: "Lucid Origin", type: "image", params: ["size"] },
{ id: "@cf/leonardo/phoenix-1.0", name: "Phoenix 1.0", type: "image", params: ["size"] },
{ id: "@cf/black-forest-labs/flux-1-schnell", name: "FLUX.1 Schnell", type: "image", params: ["size"] },
{ id: "@cf/bytedance/stable-diffusion-xl-lightning", name: "SDXL Lightning", type: "image", params: ["size"] },
{ id: "@cf/lykon/dreamshaper-8-lcm", name: "DreamShaper 8 LCM", type: "image", params: ["size"] },
{ id: "@cf/runwayml/stable-diffusion-v1-5-img2img", name: "Stable Diffusion v1.5 Img2Img", type: "image", params: ["size"], capabilities: ["edit"] },
{ id: "@cf/runwayml/stable-diffusion-v1-5-inpainting", name: "Stable Diffusion v1.5 Inpainting", type: "image", params: ["size"], capabilities: ["edit", "mask"] },
{ id: "@cf/stabilityai/stable-diffusion-xl-base-1.0", name: "SDXL Base 1.0", type: "image", params: ["size"] },
],
byteplus: [
{ id: "seed-2-0-pro-260328", name: "Seed 2.0 Pro" },
{ id: "seed-2-0-code-preview-260328", name: "Seed 2.0 Code Preview" },
{ id: "seed-2-0-mini-260215", name: "Seed 2.0 Mini" },
{ id: "seed-2-0-lite-260228", name: "Seed 2.0 Lite" },
{ id: "kimi-k2-thinking-251104", name: "Kimi K2 Thinking" },
{ id: "glm-4-7-251222", name: "GLM 4.7" },
{ id: "gpt-oss-120b-250805", name: "GPT-OSS-120B" },
],
deepseek: [
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro" },
{ id: "deepseek-v4-pro-max", name: "DeepSeek V4 Pro Max", upstreamModelId: "deepseek-v4-pro" },
{ id: "deepseek-v4-pro-none", name: "DeepSeek V4 Pro No Thinking", upstreamModelId: "deepseek-v4-pro" },
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash" },
{ id: "deepseek-chat", name: "DeepSeek V3.2 Chat" },
{ id: "deepseek-reasoner", name: "DeepSeek V3.2 Reasoner" },
],
commandcode: [
{ id: "deepseek/deepseek-v4-pro", name: "DeepSeek V4 Pro" },
{ id: "deepseek/deepseek-v4-flash", name: "DeepSeek V4 Flash" },
{ id: "moonshotai/Kimi-K2.6", name: "Kimi K2.6" },
{ id: "moonshotai/Kimi-K2.5", name: "Kimi K2.5" },
{ id: "zai-org/GLM-5.1", name: "GLM 5.1" },
{ id: "zai-org/GLM-5", name: "GLM 5" },
{ id: "MiniMaxAI/MiniMax-M2.7", name: "MiniMax M2.7" },
{ id: "MiniMaxAI/MiniMax-M2.5", name: "MiniMax M2.5" },
{ id: "Qwen/Qwen3.6-Max-Preview", name: "Qwen 3.6 Max Preview" },
{ id: "Qwen/Qwen3.6-Plus", name: "Qwen 3.6 Plus" },
{ id: "stepfun/Step-3.5-Flash", name: "Step 3.5 Flash" },
],
groq: [
{ id: "llama-3.3-70b-versatile", name: "Llama 3.3 70B" },
{ id: "meta-llama/llama-4-maverick-17b-128e-instruct", name: "Llama 4 Maverick" },
{ id: "qwen/qwen3-32b", name: "Qwen3 32B" },
{ id: "openai/gpt-oss-120b", name: "GPT-OSS 120B" },
// STT models
{ id: "whisper-large-v3", name: "Whisper Large v3", type: "stt", params: ["language", "response_format", "temperature", "prompt"] },
{ id: "whisper-large-v3-turbo", name: "Whisper Large v3 Turbo", type: "stt", params: ["language", "response_format", "temperature", "prompt"] },
{ id: "distil-whisper-large-v3-en", name: "Distil Whisper Large v3 EN", type: "stt", params: ["language", "response_format", "temperature", "prompt"] },
],
xai: [
{ id: "grok-4", name: "Grok 4" },
{ id: "grok-4-fast-reasoning", name: "Grok 4 Fast Reasoning" },
{ id: "grok-code-fast-1", name: "Grok Code Fast" },
{ id: "grok-3", name: "Grok 3" },
{ id: "grok-2-image-1212", name: "Grok 2 Image", type: "image", params: ["n", "response_format"] },
],
mistral: [
{ id: "mistral-large-latest", name: "Mistral Large 3" },
{ id: "codestral-latest", name: "Codestral" },
{ id: "mistral-medium-latest", name: "Mistral Medium 3" },
{ id: "mistral-embed", name: "Mistral Embed", type: "embedding" },
],
perplexity: [
{ id: "sonar-pro", name: "Sonar Pro" },
{ id: "sonar", name: "Sonar" },
],
together: [
{ id: "meta-llama/Llama-3.3-70B-Instruct-Turbo", name: "Llama 3.3 70B Turbo" },
{ id: "deepseek-ai/DeepSeek-R1", name: "DeepSeek R1" },
{ id: "Qwen/Qwen3-235B-A22B", name: "Qwen3 235B" },
{ id: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", name: "Llama 4 Maverick" },
{ id: "BAAI/bge-large-en-v1.5", name: "BGE Large EN v1.5", type: "embedding" },
{ id: "togethercomputer/m2-bert-80M-8k-retrieval", name: "M2 BERT 80M 8K", type: "embedding" },
],
fireworks: [
{ id: "accounts/fireworks/models/deepseek-v3p1", name: "DeepSeek V3.1" },
{ id: "accounts/fireworks/models/llama-v3p3-70b-instruct", name: "Llama 3.3 70B" },
{ id: "accounts/fireworks/models/qwen3-235b-a22b", name: "Qwen3 235B" },
{ id: "nomic-ai/nomic-embed-text-v1.5", name: "Nomic Embed Text v1.5", type: "embedding" },
],
cerebras: [
{ id: "gpt-oss-120b", name: "GPT OSS 120B" },
{ id: "zai-glm-4.7", name: "ZAI GLM 4.7" },
{ id: "llama-3.3-70b", name: "Llama 3.3 70B" },
{ id: "llama-4-scout-17b-16e-instruct", name: "Llama 4 Scout" },
{ id: "qwen-3-235b-a22b-instruct-2507", name: "Qwen3 235B A22B" },
{ id: "qwen-3-32b", name: "Qwen3 32B" },
],
cohere: [
{ id: "command-r-plus-08-2024", name: "Command R+ (Aug 2024)" },
{ id: "command-r-08-2024", name: "Command R (Aug 2024)" },
{ id: "command-a-03-2025", name: "Command A (Mar 2025)" },
],
nvidia: [
{ id: "minimaxai/minimax-m2.7", name: "Minimax M2.7" },
{ id: "z-ai/glm4.7", name: "GLM 4.7" },
{ id: "nvidia/nv-embedqa-e5-v5", name: "NV EmbedQA E5 v5", type: "embedding" },
// STT models
{ id: "nvidia/parakeet-ctc-1.1b-asr", name: "Parakeet CTC 1.1B", type: "stt", params: ["language"] },
],
nebius: [
{ id: "meta-llama/Llama-3.3-70B-Instruct", name: "Llama 3.3 70B Instruct" },
{ id: "Qwen/Qwen3-Embedding-8B", name: "Qwen3 Embedding 8B", type: "embedding" },
],
"voyage-ai": [
{ id: "voyage-3-large", name: "Voyage 3 Large", type: "embedding" },
{ id: "voyage-3.5", name: "Voyage 3.5", type: "embedding" },
{ id: "voyage-3.5-lite", name: "Voyage 3.5 Lite", type: "embedding" },
{ id: "voyage-code-3", name: "Voyage Code 3", type: "embedding" },
{ id: "voyage-finance-2", name: "Voyage Finance 2", type: "embedding" },
{ id: "voyage-law-2", name: "Voyage Law 2", type: "embedding" },
{ id: "voyage-multilingual-2", name: "Voyage Multilingual 2", type: "embedding" },
],
siliconflow: [
// DeepSeek models
{ id: "deepseek-ai/DeepSeek-V4-Pro", name: "DeepSeek V4 Pro" },
{ id: "deepseek-ai/DeepSeek-V4-Flash", name: "DeepSeek V4 Flash" },
{ id: "deepseek-ai/DeepSeek-V3.2", name: "DeepSeek V3.2" },
{ id: "deepseek-ai/DeepSeek-V3.2-Exp", name: "DeepSeek V3.2 Exp" },
{ id: "deepseek-ai/DeepSeek-V3.1", name: "DeepSeek V3.1" },
{ id: "deepseek-ai/DeepSeek-V3.1-Terminus", name: "DeepSeek V3.1 Terminus" },
{ id: "deepseek-ai/DeepSeek-R1", name: "DeepSeek R1" },
// Qwen models
{ id: "Qwen/Qwen3.5-397B-A17B", name: "Qwen 3.5 397B A17B" },
{ id: "Qwen/Qwen3.5-122B-A10B", name: "Qwen 3.5 122B A10B" },
// GLM models
{ id: "zai-org/GLM-5.1", name: "GLM 5.1" },
{ id: "zai-org/GLM-5", name: "GLM 5" },
// Kimi models
{ id: "moonshotai/Kimi-K2.6", name: "Kimi K2.6" },
{ id: "moonshotai/Kimi-K2.5", name: "Kimi K2.5" },
// Other models
{ id: "openai/gpt-oss-120b", name: "GPT OSS 120B" },
{ id: "MiniMaxAI/MiniMax-M2.5", name: "MiniMax M2.5" },
{ id: "inclusionAI/Ling-flash-2.0", name: "Ling Flash 2.0" },
],
"xiaomi-mimo": [
{ id: "mimo-v2.5-pro", name: "MiMo V2.5 Pro" },
{ id: "mimo-v2.5", name: "MiMo V2.5" },
{ id: "mimo-v2-omni", name: "MiMo V2 Omni" },
{ id: "mimo-v2-flash", name: "MiMo V2 Flash" },
],
"xiaomi-tokenplan": [
{ id: "mimo-v2.5-pro", name: "MiMo V2.5 Pro" },
{ id: "mimo-v2.5-pro-claude", name: "MiMo V2.5 Pro (Claude Native)", targetFormat: "claude", upstreamModelId: "mimo-v2.5-pro" },
{ id: "mimo-v2.5", name: "MiMo V2.5" },
{ id: "mimo-v2-pro", name: "MiMo V2 Pro" },
{ id: "mimo-v2-omni", name: "MiMo V2 Omni" },
{ id: "mimo-v2-tts", name: "MiMo V2 TTS" },
{ id: "mimo-v2.5-tts", name: "MiMo V2.5 TTS" },
{ id: "mimo-v2.5-tts-voiceclone", name: "MiMo V2.5 TTS Voice Clone" },
{ id: "mimo-v2.5-tts-voicedesign", name: "MiMo V2.5 TTS Voice Design" },
],
hyperbolic: [
{ id: "Qwen/QwQ-32B", name: "QwQ 32B" },
{ id: "deepseek-ai/DeepSeek-R1", name: "DeepSeek R1" },
{ id: "deepseek-ai/DeepSeek-V3", name: "DeepSeek V3" },
{ id: "meta-llama/Llama-3.3-70B-Instruct", name: "Llama 3.3 70B" },
{ id: "meta-llama/Llama-3.2-3B-Instruct", name: "Llama 3.2 3B" },
{ id: "Qwen/Qwen2.5-72B-Instruct", name: "Qwen 2.5 72B" },
{ id: "Qwen/Qwen2.5-Coder-32B-Instruct", name: "Qwen 2.5 Coder 32B" },
{ id: "NousResearch/Hermes-3-Llama-3.1-70B", name: "Hermes 3 70B" },
],
ollama: [
{ id: "gpt-oss:120b", name: "GPT OSS 120B" },
{ id: "kimi-k2.5", name: "Kimi K2.5" },
{ id: "glm-5", name: "GLM 5" },
{ id: "minimax-m2.5", name: "MiniMax M2.5" },
{ id: "glm-4.7-flash", name: "GLM 4.7 Flash" },
{ id: "qwen3.5", name: "Qwen3.5" },
],
vertex: [
{ id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview" },
{ id: "gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview" },
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" },
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash" },
],
"vertex-partner": [
{ id: "deepseek-ai/deepseek-v3.2-maas", name: "DeepSeek V3.2 (Vertex)" },
{ id: "qwen/qwen3-next-80b-a3b-thinking-maas", name: "Qwen3 Next 80B Thinking (Vertex)" },
{ id: "qwen/qwen3-next-80b-a3b-instruct-maas", name: "Qwen3 Next 80B Instruct (Vertex)" },
{ id: "zai-org/glm-5-maas", name: "GLM-5 (Vertex)" },
],
"grok-web": [
{ id: "grok-3", name: "Grok 3" },
{ id: "grok-3-mini", name: "Grok 3 Mini (Thinking)" },
{ id: "grok-3-thinking", name: "Grok 3 Thinking" },
{ id: "grok-4", name: "Grok 4" },
{ id: "grok-4-mini", name: "Grok 4 Mini (Thinking)" },
{ id: "grok-4-thinking", name: "Grok 4 Thinking" },
{ id: "grok-4-heavy", name: "Grok 4 Heavy (SuperGrok)" },
{ id: "grok-4.1-mini", name: "Grok 4.1 Mini (Thinking)" },
{ id: "grok-4.1-fast", name: "Grok 4.1 Fast" },
{ id: "grok-4.1-expert", name: "Grok 4.1 Expert" },
{ id: "grok-4.1-thinking", name: "Grok 4.1 Thinking" },
{ id: "grok-4.2", name: "Grok 4.2 (4.20 Beta)" },
],
"perplexity-web": [
{ id: "pplx-auto", name: "Perplexity Auto (Free)" },
{ id: "pplx-sonar", name: "Perplexity Sonar" },
{ id: "pplx-gpt", name: "GPT-5.4 (via Perplexity)" },
{ id: "pplx-gemini", name: "Gemini 3.1 Pro (via Perplexity)" },
{ id: "pplx-sonnet", name: "Claude Sonnet 4.6 (via Perplexity)" },
{ id: "pplx-opus", name: "Claude Opus 4.6 (via Perplexity)" },
{ id: "pplx-nemotron", name: "Nemotron 3 Super (via Perplexity)" },
],
// TTS entries are loaded from ttsModels.js via buildTtsProviderModels()
...buildTtsProviderModels(),
// Image providers
nanobanana: [
{ id: "nanobanana-flash", name: "NanoBanana Flash", type: "image", params: ["n", "size"] },
{ id: "nanobanana-pro", name: "NanoBanana Pro", type: "image", params: ["n", "size"] },
],
sdwebui: [
{ id: "stable-diffusion-v1-5", name: "Stable Diffusion v1.5", type: "image", params: ["n", "size"] },
{ id: "sdxl-base-1.0", name: "SDXL Base 1.0", type: "image", params: ["n", "size"] },
],
comfyui: [
{ id: "flux-dev", name: "FLUX Dev", type: "image", params: ["n", "size"] },
{ id: "sdxl", name: "SDXL", type: "image", params: ["n", "size"] },
],
huggingface: [
{ id: "black-forest-labs/FLUX.1-schnell", name: "FLUX.1 Schnell", type: "image", params: [] },
{ id: "stabilityai/stable-diffusion-xl-base-1.0", name: "SDXL Base 1.0", type: "image", params: [] },
// STT models
{ id: "openai/whisper-large-v3", name: "Whisper Large v3 (HF)", type: "stt", params: ["language"] },
{ id: "openai/whisper-small", name: "Whisper Small (HF)", type: "stt", params: ["language"] },
],
// === Free-tier providers (synced from OmniRoute) ===
agentrouter: [
{ id: "claude-opus-4-6", name: "Claude 4.6 Opus" },
{ id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" },
{ id: "glm-5.1", name: "GLM 5.1" },
{ id: "deepseek-v3.2", name: "DeepSeek V3.2" },
],
aimlapi: [
{ id: "gpt-4o", name: "GPT-4o" },
{ id: "gpt-4o-mini", name: "GPT-4o Mini" },
{ id: "claude-3-5-sonnet-20241022", name: "Claude 3.5 Sonnet" },
{ id: "gemini-2.0-flash-exp", name: "Gemini 2.0 Flash" },
{ id: "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo", name: "Llama 3.1 70B" },
],
novita: [
{ id: "deepseek/deepseek-r1", name: "DeepSeek R1" },
{ id: "deepseek/deepseek-v3", name: "DeepSeek V3" },
{ id: "meta-llama/llama-3.3-70b-instruct", name: "Llama 3.3 70B" },
{ id: "qwen/qwen-2.5-72b-instruct", name: "Qwen 2.5 72B" },
],
modal: [
{ id: "auto", name: "Auto (User-hosted)" },
],
reka: [
{ id: "reka-flash-3", name: "Reka Flash 3" },
{ id: "reka-edge-2603", name: "Reka Edge 2603" },
],
nlpcloud: [
{ id: "chatdolphin", name: "ChatDolphin" },
{ id: "dolphin", name: "Dolphin" },
{ id: "finetuned-llama-3-70b", name: "Llama 3 70B (Finetuned)" },
],
bazaarlink: [
{ id: "auto:free", name: "Auto Free (Zero Cost)" },
{ id: "auto", name: "Auto (Best Model)" },
],
completions: [
{ id: "claude-opus-4", name: "Claude Opus 4" },
{ id: "claude-sonnet-4", name: "Claude Sonnet 4" },
{ id: "gpt-4o", name: "GPT-4o" },
{ id: "gemini-2.0-flash", name: "Gemini 2.0 Flash" },
],
enally: [
{ id: "gpt-4o", name: "GPT-4o" },
{ id: "gpt-4o-mini", name: "GPT-4o Mini" },
{ id: "claude-3-5-sonnet", name: "Claude 3.5 Sonnet" },
],
freetheai: [
{ id: "gpt-4o", name: "GPT-4o" },
{ id: "claude-3-5-sonnet", name: "Claude 3.5 Sonnet" },
{ id: "gemini-1.5-pro", name: "Gemini 1.5 Pro" },
{ id: "deepseek-chat", name: "DeepSeek Chat" },
],
llm7: [
{ id: "gpt-4o-mini", name: "GPT-4o Mini" },
{ id: "gpt-4.1-mini", name: "GPT-4.1 Mini" },
{ id: "gemini-1.5-flash", name: "Gemini 1.5 Flash" },
],
lepton: [
{ id: "llama3-1-405b", name: "Llama 3.1 405B" },
{ id: "llama3-1-70b", name: "Llama 3.1 70B" },
{ id: "llama3-1-8b", name: "Llama 3.1 8B" },
{ id: "mixtral-8x7b", name: "Mixtral 8x7B" },
],
kluster: [
{ id: "deepseek-ai/DeepSeek-R1", name: "DeepSeek R1" },
{ id: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", name: "Llama 4 Maverick" },
{ id: "meta-llama/Llama-4-Scout-17B-16E-Instruct", name: "Llama 4 Scout" },
{ id: "Qwen/Qwen3-235B-A22B-Instruct", name: "Qwen3 235B" },
],
ai21: [
{ id: "jamba-large", name: "Jamba 1.5 Large" },
{ id: "jamba-mini", name: "Jamba 1.5 Mini" },
],
"inference-net": [
{ id: "meta-llama/llama-3.3-70b-instruct/fp-16", name: "Llama 3.3 70B" },
{ id: "deepseek/deepseek-v3-0324", name: "DeepSeek V3" },
{ id: "mistralai/mistral-nemo-12b-instruct/fp-16", name: "Mistral Nemo 12B" },
],
predibase: [
{ id: "llama-3-2-3b-instruct", name: "Llama 3.2 3B" },
{ id: "llama-3-1-8b-instruct", name: "Llama 3.1 8B" },
{ id: "qwen2-5-7b-instruct", name: "Qwen 2.5 7B" },
],
bytez: [
{ id: "meta-llama/Llama-3.3-70B-Instruct", name: "Llama 3.3 70B" },
{ id: "mistralai/Mistral-7B-Instruct-v0.3", name: "Mistral 7B v0.3" },
{ id: "Qwen/Qwen2.5-72B-Instruct", name: "Qwen 2.5 72B" },
],
morph: [
{ id: "morph-v3-large", name: "Morph V3 Large" },
{ id: "morph-v3-fast", name: "Morph V3 Fast" },
],
longcat: [
{ id: "LongCat-Flash-Chat", name: "LongCat Flash Chat" },
{ id: "LongCat-Flash-Thinking", name: "LongCat Flash Thinking" },
{ id: "LongCat-Flash-Lite", name: "LongCat Flash Lite" },
],
puter: [
{ id: "gpt-5", name: "GPT-5" },
{ id: "claude-opus-4", name: "Claude Opus 4" },
{ id: "gemini-3-pro-preview", name: "Gemini 3 Pro" },
{ id: "grok-4", name: "Grok 4" },
{ id: "deepseek-chat", name: "DeepSeek V3" },
],
uncloseai: [
{ id: "auto", name: "Auto (Free)" },
{ id: "gpt-4o-mini", name: "GPT-4o Mini" },
],
scaleway: [
{ id: "qwen3-235b-a22b-instruct-2507", name: "Qwen3 235B" },
{ id: "llama-3.3-70b-instruct", name: "Llama 3.3 70B" },
{ id: "mistral-small-3.1-24b-instruct-2503", name: "Mistral Small 3.1" },
],
deepinfra: [
{ id: "meta-llama/Meta-Llama-3.1-70B-Instruct", name: "Llama 3.1 70B" },
{ id: "deepseek-ai/DeepSeek-V3", name: "DeepSeek V3" },
{ id: "Qwen/Qwen2.5-72B-Instruct", name: "Qwen 2.5 72B" },
],
sambanova: [
{ id: "Meta-Llama-3.1-405B-Instruct", name: "Llama 3.1 405B" },
{ id: "Meta-Llama-3.1-70B-Instruct", name: "Llama 3.1 70B" },
{ id: "Meta-Llama-3.1-8B-Instruct", name: "Llama 3.1 8B" },
],
nscale: [
{ id: "meta-llama/Llama-3.3-70B-Instruct", name: "Llama 3.3 70B" },
{ id: "Qwen/Qwen2.5-Coder-32B-Instruct", name: "Qwen 2.5 Coder 32B" },
],
baseten: [
{ id: "deepseek-ai/DeepSeek-R1", name: "DeepSeek R1" },
{ id: "meta-llama/Llama-3.3-70B-Instruct", name: "Llama 3.3 70B" },
],
publicai: [
{ id: "auto", name: "Auto (Community)" },
],
"nous-research": [
{ id: "Hermes-4-405B", name: "Hermes 4 405B" },
{ id: "Hermes-4-70B", name: "Hermes 4 70B" },
],
glhf: [
{ id: "hf:meta-llama/Meta-Llama-3.1-405B-Instruct", name: "Llama 3.1 405B" },
{ id: "hf:meta-llama/Meta-Llama-3.1-70B-Instruct", name: "Llama 3.1 70B" },
{ id: "hf:Qwen/Qwen2.5-72B-Instruct", name: "Qwen 2.5 72B" },
],
deepgram: [
{ id: "nova-3", name: "Nova 3", type: "stt", params: ["language"] },
{ id: "nova-2", name: "Nova 2", type: "stt", params: ["language"] },
{ id: "whisper-large", name: "Whisper Large", type: "stt", params: ["language"] },
],
assemblyai: [
{ id: "universal-3-pro", name: "Universal 3 Pro", type: "stt", params: ["language"] },
{ id: "universal-2", name: "Universal 2", type: "stt", params: ["language"] },
],
"fal-ai": [
{ id: "fal-ai/flux/schnell", name: "FLUX Schnell", type: "image", params: ["n", "size"] },
{ id: "fal-ai/flux/dev", name: "FLUX Dev", type: "image", params: ["n", "size"] },
{ id: "fal-ai/flux-pro/v1.1", name: "FLUX Pro v1.1", type: "image", params: ["n", "size"] },
{ id: "fal-ai/flux-pro/v1.1-ultra", name: "FLUX Pro v1.1 Ultra", type: "image", params: ["n", "size"] },
{ id: "fal-ai/recraft-v3", name: "Recraft V3", type: "image", params: ["n", "size", "style"] },
{ id: "fal-ai/ideogram/v2", name: "Ideogram V2", type: "image", params: ["n", "size", "style"] },
{ id: "fal-ai/stable-diffusion-v35-large", name: "SD 3.5 Large", type: "image", params: ["n", "size"] },
],
"stability-ai": [
{ id: "stable-image-ultra", name: "Stable Image Ultra", type: "image", params: ["size"] },
{ id: "stable-image-core", name: "Stable Image Core", type: "image", params: ["size", "style"] },
{ id: "sd3.5-large", name: "Stable Diffusion 3.5 Large", type: "image", params: ["size"] },
{ id: "sd3.5-large-turbo", name: "Stable Diffusion 3.5 Large Turbo", type: "image", params: ["size"] },
{ id: "sd3.5-medium", name: "Stable Diffusion 3.5 Medium", type: "image", params: ["size"] },
],
"black-forest-labs": [
{ id: "flux-pro-1.1", name: "FLUX Pro 1.1", type: "image", params: ["n", "size"] },
{ id: "flux-pro-1.1-ultra", name: "FLUX Pro 1.1 Ultra", type: "image", params: ["size"] },
{ id: "flux-pro", name: "FLUX Pro", type: "image", params: ["n", "size"] },
{ id: "flux-dev", name: "FLUX Dev", type: "image", params: ["n", "size"] },
{ id: "flux-kontext-pro", name: "FLUX Kontext Pro (Edit)", type: "image", params: ["size"], capabilities: ["edit"] },
{ id: "flux-kontext-max", name: "FLUX Kontext Max (Edit)", type: "image", params: ["size"], capabilities: ["edit"] },
],
recraft: [
{ id: "recraftv3", name: "Recraft V3", type: "image", params: ["n", "size", "style"] },
{ id: "recraftv2", name: "Recraft V2", type: "image", params: ["n", "size", "style"] },
],
runwayml: [
{ id: "gen4_image", name: "Gen-4 Image", type: "image", params: ["size"] },
{ id: "gen4_image_turbo", name: "Gen-4 Image Turbo", type: "image", params: ["size"] },
{ id: "gen4_turbo", name: "Gen-4 Turbo", type: "video", params: [] },
{ id: "gen3a_turbo", name: "Gen-3 Alpha Turbo", type: "video", params: [] },
],
};
// Helper functions
export function getProviderModels(aliasOrId) {
@@ -868,15 +35,14 @@ export function findModelName(aliasOrId, modelId) {
export function getModelTargetFormat(aliasOrId, modelId) {
const models = PROVIDER_MODELS[aliasOrId];
if (!models) return null;
const found = models.find(m => m.id === modelId);
return found?.targetFormat || null;
return modelTargetFormat(models.find(m => m.id === modelId));
}
export function getModelType(aliasOrId, modelId) {
const models = PROVIDER_MODELS[aliasOrId];
if (!models) return null;
const found = models.find(m => m.id === modelId);
return found?.type || null;
return found?.kind || found?.type || null;
}
export function getModelUpstreamId(aliasOrId, modelId) {
@@ -891,30 +57,14 @@ export function getModelUpstreamId(aliasOrId, modelId) {
export function getModelQuotaFamily(aliasOrId, modelId) {
const models = PROVIDER_MODELS[aliasOrId];
const found = models?.find(m => m.id === modelId);
return found?.quotaFamily || "normal";
return modelQuotaFamily(models?.find(m => m.id === modelId));
}
// OAuth providers that use short aliases (everything else: alias = id)
const OAUTH_ALIASES = {
claude: "cc",
codex: "cx",
"gemini-cli": "gc",
qwen: "qw",
iflow: "if",
antigravity: "ag",
github: "gh",
kiro: "kr",
cursor: "cu",
"kimi-coding": "kmc",
kilocode: "kc",
cline: "cl",
opencode: "oc",
qoder: "qd",
"mimo-free": "mmf",
vertex: "vertex",
"vertex-partner": "vertex-partner",
};
// OAuth short aliases — derived from registry `alias` (single source). everything else: alias = id.
// vertex/vertex-partner keep alias=id (kept via the `|| id` fallback in consumers).
export const OAUTH_ALIASES = Object.fromEntries(
REGISTRY.filter(r => r.alias && r.alias !== r.id).map(r => [r.id, r.alias])
);
// Derived from PROVIDERS — no need to maintain manually
export const PROVIDER_ID_TO_ALIAS = Object.fromEntries(
@@ -929,6 +79,5 @@ export function getModelsByProviderId(providerId) {
// Get strip list for a model entry (explicit opt-in only)
// Returns array of content types to strip, e.g. ["image", "audio"]
export function getModelStrip(alias, modelId) {
const entry = PROVIDER_MODELS[alias]?.find(m => m.id === modelId);
return entry?.strip || [];
return modelStrip(PROVIDER_MODELS[alias]?.find(m => m.id === modelId));
}

View File

@@ -1,461 +1,6 @@
import { platform, arch } from "os";
// === OS/Arch helpers ===
function mapStainlessOs() {
switch (platform()) {
case "darwin": return "MacOS";
case "win32": return "Windows";
case "linux": return "Linux";
case "freebsd": return "FreeBSD";
default: return `Other::${platform()}`;
}
}
function mapStainlessArch() {
switch (arch()) {
case "x64": return "x64";
case "arm64": return "arm64";
case "ia32": return "x86";
default: return `other::${arch()}`;
}
}
// Shared Claude-compatible API headers (reused across claude-format providers)
const CLAUDE_API_HEADERS = {
"Anthropic-Version": "2023-06-01",
"Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14"
};
// Full Claude CLI fingerprint — required by providers that gate on client identity (e.g. agentrouter)
const CLAUDE_CLI_SPOOF_HEADERS = {
"Anthropic-Version": "2023-06-01",
"Anthropic-Beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28",
"Anthropic-Dangerous-Direct-Browser-Access": "true",
"User-Agent": "claude-cli/2.1.92 (external, sdk-cli)",
"X-App": "cli",
"X-Stainless-Helper-Method": "stream",
"X-Stainless-Retry-Count": "0",
"X-Stainless-Runtime-Version": "v24.14.0",
"X-Stainless-Package-Version": "0.80.0",
"X-Stainless-Runtime": "node",
"X-Stainless-Lang": "js",
"X-Stainless-Arch": mapStainlessArch(),
"X-Stainless-Os": mapStainlessOs(),
"X-Stainless-Timeout": "600"
};
// Shared baseUrls
const KIMI_CODING_BASE_URL = "https://api.kimi.com/coding/v1/messages";
export const PROVIDERS = {
claude: {
baseUrl: "https://api.anthropic.com/v1/messages",
format: "claude",
headers: { ...CLAUDE_CLI_SPOOF_HEADERS },
clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
tokenUrl: "https://api.anthropic.com/v1/oauth/token"
},
gemini: {
baseUrl: "https://generativelanguage.googleapis.com/v1beta/models",
format: "gemini",
clientId: "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com",
clientSecret: "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"
},
"gemini-cli": {
baseUrl: "https://cloudcode-pa.googleapis.com/v1internal",
format: "gemini-cli",
clientId: "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com",
clientSecret: "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"
},
codex: {
baseUrl: "https://chatgpt.com/backend-api/codex/responses",
format: "openai-responses",
headers: {
"originator": "codex_cli_rs",
"User-Agent": "codex_cli_rs/0.136.0"
},
clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
tokenUrl: "https://auth.openai.com/oauth/token"
},
qwen: {
baseUrl: "https://portal.qwen.ai/v1/chat/completions",
format: "openai",
clientId: "f0304373b74a44d2b584a3fb70ca9e56",
tokenUrl: "https://chat.qwen.ai/api/v1/oauth2/token",
authUrl: "https://chat.qwen.ai/api/v1/oauth2/device/code"
},
iflow: {
baseUrl: "https://apis.iflow.cn/v1/chat/completions",
format: "openai",
headers: { "User-Agent": "iFlow-Cli" },
clientId: "10009311001",
clientSecret: "4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW",
tokenUrl: "https://iflow.cn/oauth/token",
authUrl: "https://iflow.cn/oauth"
},
qoder: {
// The qoder executor builds the full URL itself (it has to append
// ?Encode=1 + sigPath query params and bypass any provider-level URL
// rewriting). baseUrl is kept for compatibility with introspection
// helpers but the executor ignores it.
baseUrl: "https://api3.qoder.sh/algo/api/v2/service/pro/sse/agent_chat_generation",
format: "openai",
headers: {},
// Reasoning models think long before first byte; raise both timeouts.
timeoutMs: 120000,
stallTimeoutMs: 120000,
},
antigravity: {
baseUrls: [
"https://daily-cloudcode-pa.googleapis.com",
"https://daily-cloudcode-pa.sandbox.googleapis.com",
],
format: "antigravity",
headers: { "User-Agent": `antigravity/1.107.0 ${platform()}/${arch()}` },
clientId: "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com",
clientSecret: "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"
},
openrouter: {
baseUrl: "https://openrouter.ai/api/v1/chat/completions",
format: "openai",
headers: {
"HTTP-Referer": "https://endpoint-proxy.local",
"X-Title": "Endpoint Proxy"
}
},
openai: {
baseUrl: "https://api.openai.com/v1/chat/completions",
format: "openai"
},
"vercel-ai-gateway": {
baseUrl: "https://ai-gateway.vercel.sh/v1/chat/completions",
format: "openai",
retry: { 429: 2 }
},
glm: {
baseUrl: "https://api.z.ai/api/anthropic/v1/messages",
format: "claude",
headers: { ...CLAUDE_API_HEADERS }
},
"glm-cn": {
baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4/chat/completions",
format: "openai",
headers: {}
},
kimi: {
baseUrl: KIMI_CODING_BASE_URL,
format: "claude",
headers: { ...CLAUDE_API_HEADERS }
},
minimax: {
baseUrl: "https://api.minimax.io/anthropic/v1/messages",
format: "claude",
headers: { ...CLAUDE_API_HEADERS }
},
"minimax-cn": {
baseUrl: "https://api.minimaxi.com/anthropic/v1/messages",
format: "claude",
headers: { ...CLAUDE_API_HEADERS }
},
alicode: {
baseUrl: "https://coding.dashscope.aliyuncs.com/v1/chat/completions",
format: "openai",
headers: {}
},
"alicode-intl": {
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1/chat/completions",
format: "openai",
headers: {}
},
"volcengine-ark": {
baseUrl: "https://ark.cn-beijing.volces.com/api/coding/v3/chat/completions",
format: "openai",
headers: {}
},
byteplus: {
baseUrl: "https://ark.ap-southeast.bytepluses.com/api/coding/v3/chat/completions",
format: "openai",
headers: {}
},
github: {
baseUrl: "https://api.githubcopilot.com/chat/completions",
responsesUrl: "https://api.githubcopilot.com/responses",
format: "openai",
headers: {
"copilot-integration-id": "vscode-chat",
"editor-version": "vscode/1.110.0",
"editor-plugin-version": "copilot-chat/0.38.0",
"user-agent": "GitHubCopilotChat/0.38.0",
"openai-intent": "conversation-panel",
"x-github-api-version": "2025-04-01",
"x-vscode-user-agent-library-version": "electron-fetch",
"X-Initiator": "user",
"Accept": "application/json",
"Content-Type": "application/json"
},
clientId: "Iv1.b507a08c87ecfe98"
},
kiro: {
// All three hosts resolve to the same regional CodeWhisperer streaming service
// (GenerateAssistantResponse). They are alternate DNS surfaces, NOT separate quota
// buckets — AWS throttles per authenticated identity (token + profileArn), not per
// hostname. Listing them enables edge-level failover (5xx / connect timeout / a
// degraded surface); it does NOT multiply 429 headroom. To actually spread 429 load,
// add multiple Kiro accounts — account rotation in sse/handlers/chat.js handles that.
// Order: newest Kiro IDE endpoint first, legacy AWS domains as fallback.
baseUrl: "https://runtime.us-east-1.kiro.dev/generateAssistantResponse",
baseUrls: [
"https://runtime.us-east-1.kiro.dev/generateAssistantResponse",
"https://codewhisperer.us-east-1.amazonaws.com/generateAssistantResponse",
"https://q.us-east-1.amazonaws.com/generateAssistantResponse",
],
format: "kiro",
// 429 = identity-level throttle; retrying the same identity only spams AWS.
// Rotate across the 3 host surfaces once each (shouldRetry) without per-host retries.
retry: { 429: 0 },
headers: {
"Content-Type": "application/json",
"Accept": "application/vnd.amazon.eventstream",
"X-Amz-Target": "AmazonCodeWhispererStreamingService.GenerateAssistantResponse",
"User-Agent": "AWS-SDK-JS/3.0.0 kiro-ide/1.0.0",
"X-Amz-User-Agent": "aws-sdk-js/3.0.0 kiro-ide/1.0.0"
},
tokenUrl: "https://prod.us-east-1.auth.desktop.kiro.dev/refreshToken",
authUrl: "https://prod.us-east-1.auth.desktop.kiro.dev"
},
cursor: {
baseUrl: "https://api2.cursor.sh",
chatPath: "/aiserver.v1.ChatService/StreamUnifiedChatWithTools",
format: "cursor",
headers: {
"connect-accept-encoding": "gzip",
"connect-protocol-version": "1",
"Content-Type": "application/connect+proto",
"User-Agent": "connect-es/1.6.1"
},
clientVersion: "3.1.0"
},
"kimi-coding": {
baseUrl: KIMI_CODING_BASE_URL,
format: "claude",
headers: { ...CLAUDE_API_HEADERS },
clientId: "17e5f671-d194-4dfb-9706-5516cb48c098",
tokenUrl: "https://auth.kimi.com/api/oauth/token",
refreshUrl: "https://auth.kimi.com/api/oauth/token"
},
kilocode: {
baseUrl: "https://api.kilo.ai/api/openrouter/chat/completions",
format: "openai",
headers: {}
},
opencode: {
baseUrl: "http://localhost:4096/v1/chat/completions",
format: "openai",
headers: {}
},
cline: {
baseUrl: "https://api.cline.bot/api/v1/chat/completions",
format: "openai",
headers: {
"HTTP-Referer": "https://cline.bot",
"X-Title": "Cline"
},
tokenUrl: "https://api.cline.bot/api/v1/auth/token",
refreshUrl: "https://api.cline.bot/api/v1/auth/refresh"
},
nvidia: {
baseUrl: "https://integrate.api.nvidia.com/v1/chat/completions",
format: "openai"
},
anthropic: {
baseUrl: "https://api.anthropic.com/v1/messages",
format: "claude",
headers: { ...CLAUDE_API_HEADERS }
},
deepseek: {
baseUrl: "https://api.deepseek.com/chat/completions",
format: "openai"
},
commandcode: {
baseUrl: "https://api.commandcode.ai/alpha/generate",
format: "commandcode",
headers: {
"x-command-code-version": "0.25.7",
"x-cli-environment": "cli"
}
},
groq: {
baseUrl: "https://api.groq.com/openai/v1/chat/completions",
format: "openai"
},
xai: {
baseUrl: "https://api.x.ai/v1/chat/completions",
responsesUrl: "https://api.x.ai/v1/responses",
format: "openai",
clientId: "b1a00492-073a-47ea-816f-4c329264a828",
tokenUrl: "https://auth.x.ai/oauth2/token",
refreshUrl: "https://auth.x.ai/oauth2/token"
},
mistral: {
baseUrl: "https://api.mistral.ai/v1/chat/completions",
format: "openai"
},
perplexity: {
baseUrl: "https://api.perplexity.ai/chat/completions",
format: "openai"
},
together: {
baseUrl: "https://api.together.xyz/v1/chat/completions",
format: "openai"
},
fireworks: {
baseUrl: "https://api.fireworks.ai/inference/v1/chat/completions",
format: "openai"
},
cerebras: {
baseUrl: "https://api.cerebras.ai/v1/chat/completions",
format: "openai"
},
cohere: {
baseUrl: "https://api.cohere.ai/v1/chat/completions",
format: "openai"
},
nebius: {
baseUrl: "https://api.studio.nebius.ai/v1/chat/completions",
format: "openai"
},
siliconflow: {
baseUrl: "https://api.siliconflow.com/v1/chat/completions",
format: "openai"
},
hyperbolic: {
baseUrl: "https://api.hyperbolic.xyz/v1/chat/completions",
format: "openai"
},
deepgram: {
baseUrl: "https://api.deepgram.com/v1/listen",
format: "openai"
},
assemblyai: {
baseUrl: "https://api.assemblyai.com/v1/audio/transcriptions",
format: "openai"
},
nanobanana: {
baseUrl: "https://api.nanobananaapi.ai/v1/chat/completions",
format: "openai"
},
chutes: {
baseUrl: "https://llm.chutes.ai/v1/chat/completions",
format: "openai"
},
ollama: {
baseUrl: "https://ollama.com/api/chat",
format: "ollama"
},
"ollama-local": {
baseUrl: "http://localhost:11434/api/chat",
format: "ollama"
},
// Vertex AI - Gemini models via Service Account JSON
// baseUrl is not used; VertexExecutor.buildUrl() constructs it dynamically
vertex: {
baseUrl: "https://aiplatform.googleapis.com",
format: "vertex"
},
// Vertex AI - Partner models (Claude, Llama, Mistral, GLM) via SA JSON
// Uses OpenAI-compatible global endpoint (or rawPredict for Anthropic)
"vertex-partner": {
baseUrl: "https://aiplatform.googleapis.com",
format: "openai"
},
// GitLab Duo - OpenAI-compatible chat endpoint
gitlab: {
baseUrl: "https://gitlab.com/api/v4/chat/completions",
format: "openai",
},
// CodeBuddy (Tencent) - uses device_code polling auth, no chat completions baseUrl needed
codebuddy: {
baseUrl: "https://copilot.tencent.com/v1/chat/completions",
format: "openai",
},
opencode: {
baseUrl: "https://opencode.ai",
format: "openai",
headers: { "x-opencode-client": "desktop" },
noAuth: true
},
"opencode-go": {
baseUrl: "https://opencode.ai/zen/go/v1/chat/completions",
format: "openai",
headers: {}
},
"grok-web": {
baseUrl: "https://grok.com/rest/app-chat/conversations/new",
format: "grok-web",
authType: "cookie"
},
"perplexity-web": {
baseUrl: "https://www.perplexity.ai/rest/sse/perplexity_ask",
format: "perplexity-web",
authType: "cookie"
},
azure: {
baseUrl: "",
format: "openai",
headers: {}
},
// Cloudflare Workers AI - {accountId} resolved from credentials.providerSpecificData.accountId
"cloudflare-ai": {
baseUrl: "https://api.cloudflare.com/client/v4/accounts/{accountId}/ai/v1/chat/completions",
format: "openai"
},
"xiaomi-mimo": {
baseUrl: "https://api.xiaomimimo.com/v1/chat/completions",
format: "openai"
},
"mimo-free": { baseUrl: "https://api.xiaomimimo.com/api/free-ai/openai/chat", format: "openai", noAuth: true },
mmf: { baseUrl: "https://api.xiaomimimo.com/api/free-ai/openai/chat", format: "openai", noAuth: true },
"xiaomi-tokenplan": {
baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1/chat/completions",
format: "openai"
},
// Region map for Xiaomi MiMo Token Plan (keys are cluster-specific)
// Used by resolveXiaomiTokenplanBaseUrl below
// === Free-tier providers (synced from OmniRoute) ===
// Claude-format with Claude CLI header spoofing (auth: x-api-key)
agentrouter: { baseUrl: "https://agentrouter.org/v1/messages", format: "claude", headers: { ...CLAUDE_CLI_SPOOF_HEADERS } },
// OpenAI-compatible (auth: bearer)
aimlapi: { baseUrl: "https://api.aimlapi.com/v1/chat/completions", format: "openai" },
novita: { baseUrl: "https://api.novita.ai/v3/openai/chat/completions", format: "openai" },
modal: { baseUrl: "https://api.modal.com/v1/chat/completions", format: "openai" },
reka: { baseUrl: "https://api.reka.ai/v1/chat/completions", format: "openai" },
nlpcloud: { baseUrl: "https://api.nlpcloud.io/v1/gpu/chatbot", format: "openai" },
bazaarlink: { baseUrl: "https://bazaarlink.ai/api/v1/chat/completions", format: "openai" },
completions: { baseUrl: "https://completions.me/api/v1/chat/completions", format: "openai" },
// enally uses X-API-Key header (not bearer); handled in validate route
enally: { baseUrl: "https://ai.enally.in/v1/chat/completions", format: "openai", authHeader: "x-api-key" },
freetheai: { baseUrl: "https://api.freetheai.xyz/v1/chat/completions", format: "openai" },
llm7: { baseUrl: "https://api.llm7.io/v1/chat/completions", format: "openai" },
lepton: { baseUrl: "https://api.lepton.ai/api/v1/chat/completions", format: "openai" },
kluster: { baseUrl: "https://api.kluster.ai/v1/chat/completions", format: "openai" },
ai21: { baseUrl: "https://api.ai21.com/studio/v1/chat/completions", format: "openai" },
"inference-net": { baseUrl: "https://api.inference.net/v1/chat/completions", format: "openai" },
predibase: { baseUrl: "https://serving.app.predibase.com/v1/chat/completions", format: "openai" },
bytez: { baseUrl: "https://api.bytez.com/models/v2", format: "openai" },
morph: { baseUrl: "https://api.morphllm.com/v1/chat/completions", format: "openai" },
longcat: { baseUrl: "https://api.longcat.chat/openai/v1/chat/completions", format: "openai" },
puter: { baseUrl: "https://api.puter.com/puterai/openai/v1/chat/completions", format: "openai" },
uncloseai: { baseUrl: "https://hermes.ai.unturf.com/v1/chat/completions", format: "openai", noAuth: true },
scaleway: { baseUrl: "https://api.scaleway.ai/v1/chat/completions", format: "openai" },
deepinfra: { baseUrl: "https://api.deepinfra.com/v1/openai/chat/completions", format: "openai" },
sambanova: { baseUrl: "https://api.sambanova.ai/v1/chat/completions", format: "openai" },
nscale: { baseUrl: "https://inference.api.nscale.com/v1/chat/completions", format: "openai" },
baseten: { baseUrl: "https://inference.baseten.co/v1/chat/completions", format: "openai" },
publicai: { baseUrl: "https://api.publicai.co/v1/chat/completions", format: "openai" },
"nous-research": { baseUrl: "https://inference-api.nousresearch.com/v1/chat/completions", format: "openai" },
glhf: { baseUrl: "https://glhf.chat/api/openai/v1/chat/completions", format: "openai" },
blackbox: { baseUrl: "https://api.blackbox.ai/chat/completions", format: "openai" },
};
// Barrel: PROVIDERS now built from providers/registry (transport co-located with models)
import { PROVIDERS } from "../providers/index.js";
export { PROVIDERS, PROVIDER_OAUTH } from "../providers/index.js";
export const OLLAMA_LOCAL_DEFAULT_HOST = "http://localhost:11434";
@@ -464,12 +9,9 @@ export function resolveOllamaLocalHost(credentials) {
return (raw || OLLAMA_LOCAL_DEFAULT_HOST).replace(/\/$/, "");
}
export const XIAOMI_TOKENPLAN_REGIONS = {
sgp: "https://token-plan-sgp.xiaomimimo.com/v1",
cn: "https://token-plan-cn.xiaomimimo.com/v1",
ams: "https://token-plan-ams.xiaomimimo.com/v1"
};
export const XIAOMI_TOKENPLAN_DEFAULT_REGION = "sgp";
// Region URLs single-source from registry xiaomi-tokenplan.transport
export const XIAOMI_TOKENPLAN_REGIONS = PROVIDERS["xiaomi-tokenplan"]?.regions || {};
export const XIAOMI_TOKENPLAN_DEFAULT_REGION = PROVIDERS["xiaomi-tokenplan"]?.defaultRegion;
export function resolveXiaomiTokenplanBaseUrl(credentials) {
const region = credentials?.providerSpecificData?.region;

View File

@@ -31,11 +31,23 @@ export const MEMORY_CONFIG = {
proxyDispatchersMaxSize: 20,
};
// Stream stall timeout: abort if no chunk received within this duration
export const STREAM_STALL_TIMEOUT_MS = 60 * 1000;
// Parse a positive integer env override, falling back to a default.
function envMs(name, def) {
const raw = process.env[name];
if (raw == null || raw === "") return def;
const n = parseInt(raw, 10);
return Number.isFinite(n) && n > 0 ? n : def;
}
// Inter-chunk stall timeout (once tokens are flowing). Generous headroom so
// slow reasoning models aren't aborted mid-stream. Env: STREAM_STALL_TIMEOUT_MS.
export const STREAM_STALL_TIMEOUT_MS = envMs("STREAM_STALL_TIMEOUT_MS", 360 * 1000);
// Time-to-first-token timeout (prompt prefill). Env: STREAM_FIRST_CHUNK_TIMEOUT_MS.
export const STREAM_FIRST_CHUNK_TIMEOUT_MS = envMs("STREAM_FIRST_CHUNK_TIMEOUT_MS", 200 * 1000);
// Fetch connect timeout: abort if upstream doesn't return response headers within this duration
export const FETCH_CONNECT_TIMEOUT_MS = 60 * 1000;
export const FETCH_CONNECT_TIMEOUT_MS = envMs("FETCH_CONNECT_TIMEOUT_MS", 60 * 1000);
// Default token limits
export const DEFAULT_MAX_TOKENS = 64000;

View File

@@ -3,9 +3,9 @@ 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/helpers/geminiHelper.js";
import { cleanJSONSchemaForAntigravity } from "../translator/formats/gemini.js";
// Sanitize function name: Gemini requires [a-zA-Z_][a-zA-Z0-9_.:\-]{0,63}
function sanitizeFunctionName(name) {
@@ -18,6 +18,56 @@ function sanitizeFunctionName(name) {
const MAX_RETRY_AFTER_MS = 10000;
const MAX_ANTIGRAVITY_OUTPUT_TOKENS = 16384;
// Fields Google generateContent rejects (Claude/OpenAI/Qwen thinking fields set at body root by thinkingUnified.js)
const ANTIGRAVITY_REQUEST_BLACKLIST = [
"output_config",
"thinking",
"reasoning_effort",
"reasoning",
"enable_thinking",
"thinking_budget",
"thinkingConfig",
];
// Strip blacklisted fields from an object (used for both body.request and top-level body)
const stripBlacklisted = obj => {
for (const key of ANTIGRAVITY_REQUEST_BLACKLIST) delete obj[key];
};
// Image generation model name patterns
const IMAGE_MODEL_PATTERNS = [
/image/i,
/imagen/i,
/image-generation/i,
];
// Detect if a model is an image generation model
function isImageModel(model) {
if (!model) return false;
return IMAGE_MODEL_PATTERNS.some(p => p.test(model));
}
// Parse aspect ratio / resolution from model name suffixes
// e.g. "gemini-3.1-flash-image-16x9" -> { aspectRatio: "16:9" }
// e.g. "gemini-3.1-flash-image-1024x768" -> { aspectRatio: "4:3" }
function parseImageConfig(model) {
const config = { aspectRatio: "1:1" };
const resMatch = model.match(/(\d+)x(\d+)$/);
if (resMatch) {
const w = parseInt(resMatch[1]);
const h = parseInt(resMatch[2]);
if (w <= 16 && h <= 16) {
config.aspectRatio = `${w}:${h}`;
} else {
// Resolution like 1024x768 — derive aspect ratio
const gcd = (a, b) => b ? gcd(b, a % b) : a;
const d = gcd(w, h);
config.aspectRatio = `${w/d}:${h/d}`;
}
}
return config;
}
export class AntigravityExecutor extends BaseExecutor {
constructor() {
super("antigravity", PROVIDERS.antigravity);
@@ -26,17 +76,22 @@ export class AntigravityExecutor extends BaseExecutor {
buildUrl(model, stream, urlIndex = 0) {
const baseUrls = this.getBaseUrls();
const baseUrl = baseUrls[urlIndex] || baseUrls[0];
const action = stream ? "streamGenerateContent?alt=sse" : "generateContent";
// Image generation MUST use non-streaming generateContent
const forceNonStream = isImageModel(model);
const action = (stream && !forceNonStream) ? "streamGenerateContent?alt=sse" : "generateContent";
return `${baseUrl}/v1internal:${action}`;
}
// sessionId comes from transformRequest output; base.execute runs transformRequest before
// buildHeaders, so we read it from instance state cached there (fallback: explicit arg).
buildHeaders(credentials, stream = true, sessionId = null) {
const sid = sessionId || this._lastSessionId;
return {
"Content-Type": "application/json",
"Authorization": `Bearer ${credentials.accessToken}`,
"User-Agent": this.config.headers?.["User-Agent"] || ANTIGRAVITY_HEADERS["User-Agent"],
[INTERNAL_REQUEST_HEADER.name]: INTERNAL_REQUEST_HEADER.value,
...(sessionId && { "X-Machine-Session-Id": sessionId }),
...(sid && { "X-Machine-Session-Id": sid }),
"Accept": stream ? "text/event-stream" : "application/json"
};
}
@@ -44,6 +99,53 @@ export class AntigravityExecutor extends BaseExecutor {
transformRequest(model, body, stream, credentials) {
const projectId = credentials?.projectId || this.generateProjectId();
// ─── Image generation: completely different request structure ───
if (isImageModel(model)) {
const imageConfig = parseImageConfig(model);
// Strip model name suffixes for the actual API model name
const cleanModel = model.replace(/-(\d+)x(\d+)$/, "");
// Build simplified contents — text-only, merge all user messages
const contents = [];
const srcContents = body.request?.contents || body.contents || [];
for (const c of srcContents) {
const textParts = (c.parts || []).filter(p => p.text !== undefined).map(p => ({ text: p.text }));
if (textParts.length > 0) {
contents.push({ role: c.role || "user", parts: textParts });
}
}
const sessionId = resolveSessionId({
headers: credentials?.rawHeaders,
body,
connectionId: credentials?.email || credentials?.connectionId,
scope: "antigravity",
});
this._lastSessionId = sessionId;
return {
project: projectId,
model: cleanModel,
userAgent: "antigravity",
requestType: "image_gen",
requestId: `agent-${crypto.randomUUID()}`,
request: {
contents,
generationConfig: {
temperature: 1.0,
topP: 0.95,
topK: 40,
maxOutputTokens: 8192,
imageConfig,
},
sessionId,
// No tools, no systemInstruction, no safetySettings for image gen
},
};
}
// ─── Standard (non-image) request ───
// Fix contents for Claude models via Antigravity
const contents = body.request?.contents?.map(c => {
let role = c.role;
@@ -80,7 +182,9 @@ export class AntigravityExecutor extends BaseExecutor {
tools = allDeclarations.length > 0 ? [{ functionDeclarations: allDeclarations }] : [];
}
// Strip tools/toolConfig (handled separately) and blacklisted fields that Google rejects
const { tools: _originalTools, toolConfig: _originalToolConfig, ...requestWithoutTools } = body.request || {};
stripBlacklisted(requestWithoutTools);
const generationConfig = { ...(requestWithoutTools.generationConfig || {}) };
if (generationConfig.maxOutputTokens > MAX_ANTIGRAVITY_OUTPUT_TOKENS) {
generationConfig.maxOutputTokens = MAX_ANTIGRAVITY_OUTPUT_TOKENS;
@@ -91,11 +195,16 @@ 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" } } })
};
// Strip blacklisted thinking fields from top-level body (set by thinkingUnified.js at root, not body.request)
stripBlacklisted(body);
this._lastSessionId = transformedRequest.sessionId; // cached for buildHeaders (base.execute order)
return {
...body,
project: projectId,
@@ -196,98 +305,23 @@ export class AntigravityExecutor extends BaseExecutor {
return totalMs > 0 ? totalMs : null;
}
async execute({ model, body, stream, credentials, signal, log, proxyOptions = null }) {
const fallbackCount = this.getFallbackCount();
let lastError = null;
let lastStatus = 0;
const MAX_AUTO_RETRIES = 3;
const MAX_RETRY_AFTER_RETRIES = 3;
const retryAttemptsByUrl = {}; // Track retry attempts per URL
const retryAfterAttemptsByUrl = {}; // Track Retry-After retries per URL
for (let urlIndex = 0; urlIndex < fallbackCount; urlIndex++) {
const url = this.buildUrl(model, stream, urlIndex);
const transformedBody = this.transformRequest(model, body, stream, credentials);
const sessionId = transformedBody.request?.sessionId;
const headers = this.buildHeaders(credentials, stream, sessionId);
// Initialize retry counters for this URL
if (!retryAttemptsByUrl[urlIndex]) {
retryAttemptsByUrl[urlIndex] = 0;
}
if (!retryAfterAttemptsByUrl[urlIndex]) {
retryAfterAttemptsByUrl[urlIndex] = 0;
}
// Hook called by BaseExecutor.tryRetry: derive delay from Retry-After (header → body),
// cap at MAX_RETRY_AFTER_MS, else exponential backoff for 429. Return false to veto (fallback URL).
async computeRetryDelay(response, attempt) {
let retryMs = this.parseRetryHeaders(response.headers);
if (!retryMs) {
try {
const response = await proxyAwareFetch(url, {
method: "POST",
headers,
body: JSON.stringify(transformedBody),
signal
}, proxyOptions);
if (response.status === HTTP_STATUS.RATE_LIMITED || response.status === HTTP_STATUS.SERVICE_UNAVAILABLE) {
// Try to get retry time from headers first
let retryMs = this.parseRetryHeaders(response.headers);
// If no retry time in headers, try to parse from error message body
if (!retryMs) {
try {
const errorBody = await response.clone().text();
const errorJson = JSON.parse(errorBody);
const errorMessage = errorJson?.error?.message || errorJson?.message || "";
retryMs = this.parseRetryFromErrorMessage(errorMessage);
} catch (e) {
// Ignore parse errors, will fall back to exponential backoff
}
}
if (retryMs && retryMs <= MAX_RETRY_AFTER_MS && retryAfterAttemptsByUrl[urlIndex] < MAX_RETRY_AFTER_RETRIES) {
retryAfterAttemptsByUrl[urlIndex]++;
log?.debug?.("RETRY", `${response.status} with Retry-After: ${Math.ceil(retryMs / 1000)}s, waiting... (${retryAfterAttemptsByUrl[urlIndex]}/${MAX_RETRY_AFTER_RETRIES})`);
await new Promise(resolve => setTimeout(resolve, retryMs));
urlIndex--;
continue;
}
// Auto retry only for 429 when retryMs is 0 or undefined
if (response.status === HTTP_STATUS.RATE_LIMITED && (!retryMs || retryMs === 0) && retryAttemptsByUrl[urlIndex] < MAX_AUTO_RETRIES) {
retryAttemptsByUrl[urlIndex]++;
// Exponential backoff: 2s, 4s, 8s...
const backoffMs = Math.min(1000 * (2 ** retryAttemptsByUrl[urlIndex]), MAX_RETRY_AFTER_MS);
log?.debug?.("RETRY", `429 auto retry ${retryAttemptsByUrl[urlIndex]}/${MAX_AUTO_RETRIES} after ${backoffMs / 1000}s`);
await new Promise(resolve => setTimeout(resolve, backoffMs));
urlIndex--;
continue;
}
log?.debug?.("RETRY", `${response.status}, Retry-After ${retryMs ? `too long (${Math.ceil(retryMs / 1000)}s)` : 'missing'}, trying fallback`);
lastStatus = response.status;
if (urlIndex + 1 < fallbackCount) {
continue;
}
}
if (this.shouldRetry(response.status, urlIndex)) {
log?.debug?.("RETRY", `${response.status} on ${url}, trying fallback ${urlIndex + 1}`);
lastStatus = response.status;
continue;
}
return { response, url, headers, transformedBody };
} catch (error) {
lastError = error;
if (urlIndex + 1 < fallbackCount) {
log?.debug?.("RETRY", `Error on ${url}, trying fallback ${urlIndex + 1}`);
continue;
}
throw error;
const errorJson = JSON.parse(await response.clone().text());
retryMs = this.parseRetryFromErrorMessage(errorJson?.error?.message || errorJson?.message || "");
} catch {
// ignore parse errors → fall through to backoff
}
}
throw lastError || new Error(`All ${fallbackCount} URLs failed with status ${lastStatus}`);
if (retryMs) return retryMs <= MAX_RETRY_AFTER_MS ? retryMs : false;
if (response.status === HTTP_STATUS.RATE_LIMITED) {
return Math.min(1000 * (2 ** attempt), MAX_RETRY_AFTER_MS); // exponential backoff
}
return false;
}
/**

View File

@@ -2,6 +2,7 @@ import { HTTP_STATUS, RETRY_CONFIG, DEFAULT_RETRY_CONFIG, resolveRetryEntry, FET
import { shouldRefreshCredentials } from "../services/oauthCredentialManager.js";
import { proxyAwareFetch } from "../utils/proxyFetch.js";
import { dbg } from "../utils/debugLog.js";
import { ANTHROPIC_API_VERSION, OPENAI_COMPAT_BASE, ANTHROPIC_COMPAT_BASE } from "../providers/shared.js";
/**
* BaseExecutor - Base class for provider executors
@@ -27,13 +28,13 @@ export class BaseExecutor {
buildUrl(model, stream, urlIndex = 0, credentials = null) {
if (this.provider?.startsWith?.("openai-compatible-")) {
const baseUrl = credentials?.providerSpecificData?.baseUrl || "https://api.openai.com/v1";
const baseUrl = credentials?.providerSpecificData?.baseUrl || OPENAI_COMPAT_BASE;
const normalized = baseUrl.replace(/\/$/, "");
const path = this.provider.includes("responses") ? "/responses" : "/chat/completions";
return `${normalized}${path}`;
}
if (this.provider?.startsWith?.("anthropic-compatible-")) {
const baseUrl = credentials?.providerSpecificData?.baseUrl || "https://api.anthropic.com/v1";
const baseUrl = credentials?.providerSpecificData?.baseUrl || ANTHROPIC_COMPAT_BASE;
const normalized = baseUrl.replace(/\/$/, "");
return `${normalized}/messages`;
}
@@ -55,7 +56,7 @@ export class BaseExecutor {
headers["Authorization"] = `Bearer ${credentials.accessToken}`;
}
if (!headers["anthropic-version"]) {
headers["anthropic-version"] = "2023-06-01";
headers["anthropic-version"] = ANTHROPIC_API_VERSION;
}
} else {
// Standard Bearer token auth for other providers
@@ -105,12 +106,20 @@ export class BaseExecutor {
const retryConfig = { ...DEFAULT_RETRY_CONFIG, ...this.config.retry };
// Schedule retry via retryConfig[statusKey]. Returns true when caller should `urlIndex--; continue`
const tryRetry = async (urlIndex, statusKey, reason) => {
// response (optional) lets a subclass hook compute a dynamic delay (e.g. antigravity Retry-After).
const tryRetry = async (urlIndex, statusKey, reason, response = null) => {
const { attempts, delayMs } = resolveRetryEntry(retryConfig[statusKey]);
if (attempts <= 0 || retryAttemptsByUrl[urlIndex] >= attempts) return false;
// Hook: subclass may derive delay from the response (headers/body). null → skip retry, use fallback.
let waitMs = delayMs;
if (response && this.computeRetryDelay) {
const dynamic = await this.computeRetryDelay(response, retryAttemptsByUrl[urlIndex] + 1, delayMs);
if (dynamic === false) return false; // hook vetoes retry (e.g. Retry-After too long)
if (dynamic != null) waitMs = dynamic;
}
retryAttemptsByUrl[urlIndex]++;
log?.debug?.("RETRY", `${reason} retry ${retryAttemptsByUrl[urlIndex]}/${attempts} after ${delayMs / 1000}s`);
await new Promise(resolve => setTimeout(resolve, delayMs));
log?.debug?.("RETRY", `${reason} retry ${retryAttemptsByUrl[urlIndex]}/${attempts} after ${waitMs / 1000}s`);
await new Promise(resolve => setTimeout(resolve, waitMs));
return true;
};
@@ -154,7 +163,7 @@ export class BaseExecutor {
const cl = response.headers?.get?.("content-length") || "?";
dbg("FETCH", `${this.provider.toUpperCase()} ← ${response.status} | ttft=${Date.now() - fetchT0}ms | ct=${ct} | cl=${cl}`);
if (await tryRetry(urlIndex, response.status, `status ${response.status}`)) { urlIndex--; continue; }
if (await tryRetry(urlIndex, response.status, `status ${response.status}`, response)) { urlIndex--; continue; }
if (this.shouldRetry(response.status, urlIndex)) {
log?.debug?.("RETRY", `${response.status} on ${url}, trying fallback ${urlIndex + 1}`);

View File

@@ -0,0 +1,36 @@
import { DefaultExecutor } from "./default.js";
/**
* CodeBuddyExecutor — talks to https://copilot.tencent.com/v2/chat/completions
*
* CodeBuddy is OpenAI-compatible but rejects non-stream chat requests
* (HTTP 400, code 11101 "Non-stream chat request is currently not supported").
* The same-format (openai→openai) translator path leaves body.stream as the
* client sent it, so we force it true here — 9router still re-aggregates the
* SSE into a JSON response for non-streaming clients.
*/
export class CodeBuddyExecutor extends DefaultExecutor {
constructor() {
super("codebuddy-cn");
}
transformRequest(model, body, stream, credentials) {
const transformed = super.transformRequest(model, body, stream, credentials);
transformed.stream = true;
// CodeBuddy only surfaces model reasoning when the request carries the CLI's
// OpenAI-style params: reasoning_effort + reasoning_summary:"auto". 9router's
// thinking pipeline sets reasoning_effort only when the client asks, and never
// sets reasoning_summary — so reasoning never shows. Mirror the CLI here.
const eff = transformed.reasoning_effort;
if (eff === "none" || eff === "off") {
delete transformed.reasoning_effort; // gateway has no "none" — just omit
} else {
if (!eff) transformed.reasoning_effort = "medium";
transformed.reasoning_summary = "auto";
}
return transformed;
}
}
export default CodeBuddyExecutor;

View File

@@ -1,4 +1,3 @@
import { createHash } from "crypto";
import { BaseExecutor } from "./base.js";
import { CODEX_DEFAULT_INSTRUCTIONS } from "../config/codexInstructions.js";
import { PROVIDERS } from "../config/providers.js";
@@ -6,30 +5,30 @@ import {
refreshProviderCredentials,
shouldRefreshCredentials,
} from "../services/oauthCredentialManager.js";
import { normalizeResponsesInput } from "../translator/helpers/responsesApiHelper.js";
import { fetchImageAsBase64 } from "../translator/helpers/imageHelper.js";
import { normalizeResponsesInput } from "../translator/formats/responsesApi.js";
import { fetchImageAsBase64 } from "../translator/concerns/image.js";
import { getModelUpstreamId } from "../config/providerModels.js";
import { getConsistentMachineId } from "../../src/shared/utils/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)_/;
// Hosted tool types that Codex/OpenAI Responses executes server-side
const CODEX_HOSTED_TOOL_TYPES = new Set([
"image_generation", "web_search", "web_search_preview", "file_search",
"computer", "computer_use_preview", "code_interpreter", "mcp", "local_shell"
"computer", "computer_use_preview", "code_interpreter", "mcp", "local_shell",
"tool_search"
]);
// Responses-native freeform tools carry a name plus format payload and must pass through intact.
const CODEX_PASSTHROUGH_TOOL_TYPES = new Set(["custom"]);
// Allowlist of fields accepted by Codex Responses API — anything else is stripped
const RESPONSES_API_ALLOWLIST = new Set([
"model", "input", "instructions", "tools", "tool_choice", "stream", "store",
@@ -76,6 +75,7 @@ function normalizeCodexTools(body) {
return true;
}
if (type !== "function") {
if (CODEX_PASSTHROUGH_TOOL_TYPES.has(type)) return true;
if (!type || tool.function || typeof tool.name === "string") return false;
return CODEX_HOSTED_TOOL_TYPES.has(type);
}
@@ -104,86 +104,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 +308,7 @@ export class CodexExecutor extends BaseExecutor {
this._isCompact = !!body._compact;
delete body._compact;
// Resolve conversation-stable session_id (priority: body → assistant-text → workspace → machine)
this._currentSessionId = resolveCacheSessionId(body, credentials, cachedMachineId);
this._currentSessionId = resolveCacheSessionId(body, credentials);
// Convert string input to array format (Codex API requires input as array)
const normalized = normalizeResponsesInput(body.input);
if (normalized) body.input = normalized;

View File

@@ -1,7 +1,8 @@
import { randomUUID } from "crypto";
import { BaseExecutor } from "./base.js";
import { PROVIDERS } from "../config/providers.js";
import { convertCommandCodeToOpenAI } from "../translator/response/commandcode-to-openai.js";
import { commandCodeToOpenAIResponse } from "../translator/response/commandcode-to-openai.js";
import { SSE_DONE } from "../utils/sseConstants.js";
/**
* CommandCodeExecutor — talks to https://api.commandcode.ai/alpha/generate
@@ -70,15 +71,15 @@ function wrapNdjsonAsOpenAISse(originalResponse, model) {
const trimmed = line.trim();
if (!trimmed) continue;
// Translate AI SDK v5 NDJSON line to one or more OpenAI chunks
emitChunks(convertCommandCodeToOpenAI(trimmed, state), controller);
emitChunks(commandCodeToOpenAIResponse(trimmed, state), controller);
}
},
flush(controller) {
const trimmed = buffer.trim();
if (trimmed) {
emitChunks(convertCommandCodeToOpenAI(trimmed, state), controller);
emitChunks(commandCodeToOpenAIResponse(trimmed, state), controller);
}
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.enqueue(encoder.encode(SSE_DONE));
},
});

View File

@@ -8,6 +8,8 @@ import {
} from "../utils/cursorProtobuf.js";
import { buildCursorHeaders } from "../utils/cursorChecksum.js";
import { estimateUsage } from "../utils/usageTracking.js";
import { SSE_DONE, SSE_HEADERS } from "../utils/sseConstants.js";
import { chatChunkSse } from "../utils/sse.js";
import { FORMATS } from "../translator/formats.js";
import { proxyAwareFetch } from "../utils/proxyFetch.js";
import zlib from "zlib";
@@ -98,6 +100,32 @@ function decompressPayload(payload, flags) {
return payload;
}
// Read one cursor protobuf frame: header + bounds + decompress. Returns status + payload + new offset.
function readCursorFrame(buffer, offset, frameNum, tag) {
if (offset + 5 > buffer.length) {
debugLog(`[CURSOR BUFFER${tag}] Reached end, offset=${offset}, remaining=${buffer.length - offset}`);
return { status: "done" };
}
const flags = buffer[offset];
const length = buffer.readUInt32BE(offset + 1);
debugLog(`[CURSOR BUFFER${tag}] Frame ${frameNum + 1}: flags=0x${flags.toString(16).padStart(2, "0")}, length=${length}`);
if (offset + 5 + length > buffer.length) {
debugLog(`[CURSOR BUFFER${tag}] Incomplete frame, offset=${offset}, length=${length}, buffer.length=${buffer.length}`);
return { status: "done" };
}
let payload = buffer.slice(offset + 5, offset + 5 + length);
const newOffset = offset + 5 + length;
payload = decompressPayload(payload, flags);
if (!payload) {
debugLog(`[CURSOR BUFFER${tag}] Frame ${frameNum + 1}: decompression failed, skipping`);
return { status: "skip", offset: newOffset };
}
return { status: "ok", payload, offset: newOffset };
}
function createErrorResponse(jsonError) {
const errorMsg = jsonError?.error?.details?.[0]?.debug?.details?.title
|| jsonError?.error?.details?.[0]?.debug?.details?.detail
@@ -141,7 +169,7 @@ export class CursorExecutor extends BaseExecutor {
transformRequest(model, body, stream, credentials) {
// Messages are already translated by chatCore (claude→openai→cursor)
// Do NOT call buildCursorRequest again — double-translation drops tool_results
// Do NOT call openaiToCursorRequest again — double-translation drops tool_results
const messages = body.messages || [];
const tools = body.tools || [];
const reasoningEffort = body.reasoning_effort || null;
@@ -286,36 +314,12 @@ export class CursorExecutor extends BaseExecutor {
debugLog(`[CURSOR BUFFER] Total length: ${buffer.length} bytes`);
while (offset < buffer.length) {
if (offset + 5 > buffer.length) {
debugLog(
`[CURSOR BUFFER] Reached end, offset=${offset}, remaining=${buffer.length - offset}`
);
break;
}
const flags = buffer[offset];
const length = buffer.readUInt32BE(offset + 1);
debugLog(
`[CURSOR BUFFER] Frame ${frameCount + 1}: flags=0x${flags.toString(16).padStart(2, "0")}, length=${length}`
);
if (offset + 5 + length > buffer.length) {
debugLog(
`[CURSOR BUFFER] Incomplete frame, offset=${offset}, length=${length}, buffer.length=${buffer.length}`
);
break;
}
let payload = buffer.slice(offset + 5, offset + 5 + length);
offset += 5 + length;
const frame = readCursorFrame(buffer, offset, frameCount, "");
if (frame.status === "done") break;
offset = frame.offset;
frameCount++;
payload = decompressPayload(payload, flags);
if (!payload) {
debugLog(`[CURSOR BUFFER] Frame ${frameCount}: decompression failed, skipping`);
continue;
}
if (frame.status === "skip") continue;
const payload = frame.payload;
// Check for JSON error frames (byte guard: skip toString on non-JSON frames)
if (payload.length > 0 && payload[0] === 0x7b) {
@@ -466,36 +470,12 @@ export class CursorExecutor extends BaseExecutor {
debugLog(`[CURSOR BUFFER SSE] Total length: ${buffer.length} bytes`);
while (offset < buffer.length) {
if (offset + 5 > buffer.length) {
debugLog(
`[CURSOR BUFFER SSE] Reached end, offset=${offset}, remaining=${buffer.length - offset}`
);
break;
}
const flags = buffer[offset];
const length = buffer.readUInt32BE(offset + 1);
debugLog(
`[CURSOR BUFFER SSE] Frame ${frameCount + 1}: flags=0x${flags.toString(16).padStart(2, "0")}, length=${length}`
);
if (offset + 5 + length > buffer.length) {
debugLog(
`[CURSOR BUFFER SSE] Incomplete frame, offset=${offset}, length=${length}, buffer.length=${buffer.length}`
);
break;
}
let payload = buffer.slice(offset + 5, offset + 5 + length);
offset += 5 + length;
const frame = readCursorFrame(buffer, offset, frameCount, " SSE");
if (frame.status === "done") break;
offset = frame.offset;
frameCount++;
payload = decompressPayload(payload, flags);
if (!payload) {
debugLog(`[CURSOR BUFFER SSE] Frame ${frameCount}: decompression failed, skipping`);
continue;
}
if (frame.status === "skip") continue;
const payload = frame.payload;
// Check for JSON error frames (byte-guard: only decode if starts with '{')
if (payload[0] === 0x7b) {
@@ -542,21 +522,7 @@ export class CursorExecutor extends BaseExecutor {
const tc = result.toolCall;
if (chunks.length === 0) {
chunks.push(
`data: ${JSON.stringify({
id: responseId,
object: "chat.completion.chunk",
created,
model,
choices: [
{
index: 0,
delta: { role: "assistant", content: "" },
finish_reason: null
}
]
})}\n\n`
);
chunks.push(chatChunkSse({ id: responseId, created, model, delta: { role: "assistant", content: "" } }));
}
if (toolCallsMap.has(tc.id)) {
@@ -569,33 +535,22 @@ export class CursorExecutor extends BaseExecutor {
// Stream the delta arguments
if (tc.function.arguments) {
emittedToolCallIds.add(tc.id);
chunks.push(
`data: ${JSON.stringify({
id: responseId,
object: "chat.completion.chunk",
created,
model,
choices: [
chunks.push(chatChunkSse({
id: responseId, created, model,
delta: {
tool_calls: [
{
index: 0,
delta: {
tool_calls: [
{
index: existing.index,
id: tc.id,
type: "function",
function: {
name: tc.function.name,
arguments: tc.function.arguments
}
}
]
},
finish_reason: null
index: existing.index,
id: tc.id,
type: "function",
function: {
name: tc.function.name,
arguments: tc.function.arguments
}
}
]
})}\n\n`
);
}
}));
}
} else {
// New tool call - assign index and add to map
@@ -606,56 +561,34 @@ export class CursorExecutor extends BaseExecutor {
// Stream initial tool call with name
emittedToolCallIds.add(tc.id);
chunks.push(
`data: ${JSON.stringify({
id: responseId,
object: "chat.completion.chunk",
created,
model,
choices: [
chunks.push(chatChunkSse({
id: responseId, created, model,
delta: {
tool_calls: [
{
index: 0,
delta: {
tool_calls: [
{
index: toolCallIndex,
id: tc.id,
type: "function",
function: {
name: tc.function.name,
arguments: tc.function.arguments
}
}
]
},
finish_reason: null
index: toolCallIndex,
id: tc.id,
type: "function",
function: {
name: tc.function.name,
arguments: tc.function.arguments
}
}
]
})}\n\n`
);
}
}));
}
}
if (result.text) {
totalContent += result.text;
chunks.push(
`data: ${JSON.stringify({
id: responseId,
object: "chat.completion.chunk",
created,
model,
choices: [
{
index: 0,
delta:
chunks.length === 0 && toolCalls.length === 0
? { role: "assistant", content: result.text }
: { content: result.text },
finish_reason: null
}
]
})}\n\n`
);
chunks.push(chatChunkSse({
id: responseId, created, model,
delta:
chunks.length === 0 && toolCalls.length === 0
? { role: "assistant", content: result.text }
: { content: result.text }
}));
}
if (isComposerModel(model) && result.thinking) {
@@ -665,24 +598,13 @@ export class CursorExecutor extends BaseExecutor {
const deltaContent = visibleContent.slice(emittedComposerThinkingContentLength);
emittedComposerThinkingContentLength = visibleContent.length;
totalContent += deltaContent;
chunks.push(
`data: ${JSON.stringify({
id: responseId,
object: "chat.completion.chunk",
created,
model,
choices: [
{
index: 0,
delta:
chunks.length === 0 && toolCalls.length === 0
? { role: "assistant", content: deltaContent }
: { content: deltaContent },
finish_reason: null
}
]
})}\n\n`
);
chunks.push(chatChunkSse({
id: responseId, created, model,
delta:
chunks.length === 0 && toolCalls.length === 0
? { role: "assistant", content: deltaContent }
: { content: deltaContent }
}));
}
}
}
@@ -708,53 +630,28 @@ export class CursorExecutor extends BaseExecutor {
// Emit SSE chunk for the finalized tool call if not already emitted
if (!emittedToolCallIds.has(tc.id)) {
chunks.push(
`data: ${JSON.stringify({
id: responseId,
object: "chat.completion.chunk",
created,
model,
choices: [
chunks.push(chatChunkSse({
id: responseId, created, model,
delta: {
tool_calls: [
{
index: 0,
delta: {
tool_calls: [
{
index: toolCallIndex,
id: tc.id,
type: "function",
function: {
name: tc.function.name,
arguments: tc.function.arguments
}
}
]
},
finish_reason: null
index: toolCallIndex,
id: tc.id,
type: "function",
function: {
name: tc.function.name,
arguments: tc.function.arguments
}
}
]
})}\n\n`
);
}
}));
}
}
}
if (chunks.length === 0 && toolCalls.length === 0) {
chunks.push(
`data: ${JSON.stringify({
id: responseId,
object: "chat.completion.chunk",
created,
model,
choices: [
{
index: 0,
delta: { role: "assistant", content: "" },
finish_reason: null
}
]
})}\n\n`
);
chunks.push(chatChunkSse({ id: responseId, created, model, delta: { role: "assistant", content: "" } }));
}
const usage = estimateUsage(body, totalContent.length, FORMATS.OPENAI);
@@ -775,15 +672,11 @@ export class CursorExecutor extends BaseExecutor {
usage
})}\n\n`
);
chunks.push("data: [DONE]\n\n");
chunks.push(SSE_DONE);
return new Response(chunks.join(""), {
status: 200,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive"
}
headers: { ...SSE_HEADERS }
});
}

View File

@@ -1,10 +1,80 @@
import { BaseExecutor } from "./base.js";
import { PROVIDERS } from "../config/providers.js";
import { PROVIDERS, PROVIDER_OAUTH } from "../config/providers.js";
import { ANTHROPIC_API_VERSION, OPENAI_COMPAT_BASE, ANTHROPIC_COMPAT_BASE } from "../providers/shared.js";
import { OAUTH_ENDPOINTS, buildKimiHeaders } from "../config/appConstants.js";
import { buildClineHeaders } from "../../src/shared/utils/clineAuth.js";
import { buildClineHeaders } from "../shared/clineAuth.js";
import { getCachedClaudeHeaders } from "../utils/claudeHeaderCache.js";
import { proxyAwareFetch } from "../utils/proxyFetch.js";
import { injectReasoningContent } from "../utils/reasoningContentInjector.js";
import { stripUnsupportedParams } from "../translator/concerns/paramSupport.js";
// Auth header descriptors — derived from registry transport.auth, fallback to hardcoded defaults.
const BEARER = { combined: true, header: "Authorization", scheme: "bearer" };
const XAPIKEY = { combined: true, header: "x-api-key", scheme: "raw" };
const AUTH_DESCRIPTORS = Object.fromEntries(
Object.entries(PROVIDERS)
.filter(([, t]) => t.auth)
.map(([id, t]) => [id, t.auth])
);
// Apply a token to a header per scheme (matches legacy: combined always sets, even when undefined).
function setAuth(headers, spec, token) {
headers[spec.header] = spec.scheme === "bearer" ? `Bearer ${token}` : token;
}
// Resolve auth onto headers from a descriptor.
function applyAuth(headers, desc, credentials) {
if (desc.combined) {
// combined providers always set the header (legacy behavior, incl. noAuth → "Bearer undefined")
setAuth(headers, desc, credentials.apiKey || credentials.accessToken);
if (desc.anthropicVersion && !headers["anthropic-version"]) headers["anthropic-version"] = ANTHROPIC_API_VERSION;
return;
}
// split apiKey/oauth: set only the matching branch (legacy: anthropic-compatible skips when both absent)
if (credentials.apiKey) setAuth(headers, desc.apiKey, credentials.apiKey);
else if (credentials.accessToken) setAuth(headers, desc.oauth, credentials.accessToken);
if (desc.anthropicVersion && !headers["anthropic-version"]) headers["anthropic-version"] = ANTHROPIC_API_VERSION;
}
// Provider-specific header quirks kept as small hooks (not pure auth).
const HEADER_HOOKS = {
kimiHeaders: (h) => Object.assign(h, buildKimiHeaders()),
clineHeaders: (h, c) => Object.assign(h, buildClineHeaders(c.apiKey || c.accessToken)),
kilocodeOrg: (h, c) => { if (c.providerSpecificData?.orgId) h["X-Kilocode-OrganizationID"] = c.providerSpecificData.orgId; },
claudeOverlay: (h) => {
const cached = getCachedClaudeHeaders();
if (!cached) return;
for (const lcKey of Object.keys(cached)) {
const titleKey = lcKey.replace(/(^|-)([a-z])/g, (_, sep, ch) => sep + ch.toUpperCase());
if (lcKey === "anthropic-beta") {
const staticBetaStr = h[titleKey] || h[lcKey] || "";
const flags = new Set(staticBetaStr.split(",").map(f => f.trim()).filter(Boolean));
for (const f of cached[lcKey].split(",").map(f => f.trim()).filter(Boolean)) flags.add(f);
cached[lcKey] = Array.from(flags).join(",");
}
if (titleKey !== lcKey && h[titleKey] !== undefined) delete h[titleKey];
}
Object.assign(h, cached);
},
};
// Config-driven OAuth refresh grants — derived from registry oauth.refresh.
const REFRESH_GRANTS = Object.fromEntries(
Object.entries(PROVIDER_OAUTH)
.filter(([, o]) => o.refresh)
.map(([id, o]) => {
const tokenUrl = o.tokenUrl;
const encoding = o.refresh.encoding;
const extraParams = o.refresh.scope ? { scope: o.refresh.scope } : {};
return [id, {
encoding,
url: () => tokenUrl,
params: (ex) => id === "gemini"
? { client_id: ex.config.clientId, client_secret: ex.config.clientSecret, ...extraParams }
: { client_id: o.clientId, ...extraParams },
}];
})
);
export class DefaultExecutor extends BaseExecutor {
constructor(provider) {
@@ -15,9 +85,11 @@ export class DefaultExecutor extends BaseExecutor {
const transformed = this.applyJsonSchemaFallback(body);
if (transformed && typeof transformed === "object") {
if (this.provider === "cerebras" || this.provider === "mistral") {
// quirk: some openai-compatible providers reject Anthropic's client_metadata field
if (this.config.quirks?.dropClientMetadata) {
delete transformed.client_metadata;
}
stripUnsupportedParams(this.provider, model, transformed);
}
return injectReasoningContent({ provider: this.provider, model, body: transformed });
@@ -44,120 +116,57 @@ export class DefaultExecutor extends BaseExecutor {
}
buildUrl(model, stream, urlIndex = 0, credentials = null) {
// Runtime transport (multi-endpoint providers): use the sourceFormat-matched endpoint
const rt = credentials?.runtimeTransport;
if (rt?.baseUrl) {
return rt.urlSuffix ? `${rt.baseUrl}${rt.urlSuffix}` : rt.baseUrl;
}
if (this.provider?.startsWith?.("openai-compatible-")) {
const baseUrl = credentials?.providerSpecificData?.baseUrl || "https://api.openai.com/v1";
const baseUrl = credentials?.providerSpecificData?.baseUrl || OPENAI_COMPAT_BASE;
const normalized = baseUrl.replace(/\/$/, "");
const path = this.provider.includes("responses") ? "/responses" : "/chat/completions";
return `${normalized}${path}`;
}
if (this.provider?.startsWith?.("anthropic-compatible-")) {
const baseUrl = credentials?.providerSpecificData?.baseUrl || "https://api.anthropic.com/v1";
const baseUrl = credentials?.providerSpecificData?.baseUrl || ANTHROPIC_COMPAT_BASE;
const normalized = baseUrl.replace(/\/$/, "");
return `${normalized}/messages`;
}
switch (this.provider) {
case "claude":
case "glm":
case "kimi":
case "minimax":
case "minimax-cn":
return `${this.config.baseUrl}?beta=true`;
case "kimi-coding":
return `${this.config.baseUrl}?beta=true`;
case "gemini":
return `${this.config.baseUrl}/${model}:${stream ? "streamGenerateContent?alt=sse" : "generateContent"}`;
default: {
const url = this.config.baseUrl;
if (url?.includes("{accountId}")) {
const accountId = credentials?.providerSpecificData?.accountId;
if (!accountId) throw new Error(`${this.provider} requires accountId in providerSpecificData`);
return url.replace("{accountId}", accountId);
}
return url;
}
// gemini-format: build :streamGenerateContent / :generateContent path
if (this.config.format === "gemini") {
return `${this.config.baseUrl}/${model}:${stream ? "streamGenerateContent?alt=sse" : "generateContent"}`;
}
// urlSuffix (e.g. ?beta=true) declared per-provider in registry
if (this.config.urlSuffix) {
return `${this.config.baseUrl}${this.config.urlSuffix}`;
}
const url = this.config.baseUrl;
if (url?.includes("{accountId}")) {
const accountId = credentials?.providerSpecificData?.accountId;
if (!accountId) throw new Error(`${this.provider} requires accountId in providerSpecificData`);
return url.replace("{accountId}", accountId);
}
return url;
}
// Fallback descriptor for providers without an explicit entry in AUTH_DESCRIPTORS.
resolveAuthDescriptor() {
if (this.provider?.startsWith?.("anthropic-compatible-")) {
return { apiKey: { header: "x-api-key", scheme: "raw" }, oauth: { header: "Authorization", scheme: "bearer" }, anthropicVersion: true };
}
if (this.config?.format === "claude") {
return { ...XAPIKEY, anthropicVersion: true };
}
return BEARER;
}
buildHeaders(credentials, stream = true) {
const headers = { "Content-Type": "application/json", ...this.config.headers };
switch (this.provider) {
case "gemini":
credentials.apiKey ? headers["x-goog-api-key"] = credentials.apiKey : headers["Authorization"] = `Bearer ${credentials.accessToken}`;
break;
case "claude": {
// Overlay live cached headers from real Claude Code client over static defaults.
// Static headers (Title-Case) remain as cold-start fallback.
const cached = getCachedClaudeHeaders();
if (cached) {
// Remove Title-Case static keys that conflict with incoming lowercase cached keys
for (const lcKey of Object.keys(cached)) {
// Build the Title-Case equivalent: "anthropic-version" → "Anthropic-Version"
const titleKey = lcKey.replace(/(^|-)([a-z])/g, (_, sep, c) => sep + c.toUpperCase());
// Special handling for Anthropic-Beta to preserve required flags like OAuth
if (lcKey === "anthropic-beta") {
const staticBetaStr = headers[titleKey] || headers[lcKey] || "";
const staticFlags = new Set(staticBetaStr.split(",").map(f => f.trim()).filter(Boolean));
const cachedFlags = new Set(cached[lcKey].split(",").map(f => f.trim()).filter(Boolean));
// Merge all static flags (which contain oauth, thinking, etc) into the cached ones
for (const flag of staticFlags) {
cachedFlags.add(flag);
}
cached[lcKey] = Array.from(cachedFlags).join(",");
}
if (titleKey !== lcKey && headers[titleKey] !== undefined) {
delete headers[titleKey];
}
}
Object.assign(headers, cached);
}
credentials.apiKey
? (headers["x-api-key"] = credentials.apiKey)
: (headers["Authorization"] = `Bearer ${credentials.accessToken}`);
break;
}
case "glm":
case "kimi":
case "minimax":
case "minimax-cn":
case "kimi-coding":
headers["x-api-key"] = credentials.apiKey || credentials.accessToken;
if (this.provider === "kimi-coding") Object.assign(headers, buildKimiHeaders());
break;
default:
if (this.provider?.startsWith?.("anthropic-compatible-")) {
if (credentials.apiKey) {
headers["x-api-key"] = credentials.apiKey;
} else if (credentials.accessToken) {
headers["Authorization"] = `Bearer ${credentials.accessToken}`;
}
if (!headers["anthropic-version"]) {
headers["anthropic-version"] = "2023-06-01";
}
} else if (this.provider === "gitlab") {
// GitLab Duo uses Bearer token (PAT with ai_features scope, or OAuth access token)
headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`;
} else if (this.provider === "codebuddy") {
headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`;
} else if (this.provider === "kilocode") {
headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`;
if (credentials.providerSpecificData?.orgId) {
headers["X-Kilocode-OrganizationID"] = credentials.providerSpecificData.orgId;
}
} else if (this.provider === "cline") {
Object.assign(headers, buildClineHeaders(credentials.apiKey || credentials.accessToken));
} else if (this.config?.format === "claude") {
// Generic claude-format provider (e.g. agentrouter): x-api-key + anthropic-version
headers["x-api-key"] = credentials.apiKey || credentials.accessToken;
if (!headers["anthropic-version"]) headers["anthropic-version"] = "2023-06-01";
} else {
headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`;
}
}
const rt = credentials?.runtimeTransport;
const headers = { "Content-Type": "application/json", ...(rt ? rt.headers : this.config.headers) };
const desc = rt?.auth || AUTH_DESCRIPTORS[this.provider] || this.resolveAuthDescriptor();
// Hooks run BEFORE auth so dynamic overlays (claude cached headers) can't clobber the token.
for (const hook of desc.hooks || []) HEADER_HOOKS[hook]?.(headers, credentials);
applyAuth(headers, desc, credentials);
// Strip first-party Claude Code identity headers for non-Anthropic anthropic-compatible upstreams
if (this.provider?.startsWith?.("anthropic-compatible-")) {
@@ -196,15 +205,25 @@ export class DefaultExecutor extends BaseExecutor {
return headers;
}
// Generic OAuth refresh for the common {grant_type, refresh_token, client_id[, ...]} shape.
// grant = REFRESH_GRANTS[provider]; client creds resolved from PROVIDERS or this.config.
refreshFromGrant(credentials, proxyOptions) {
const grant = REFRESH_GRANTS[this.provider];
const params = { grant_type: "refresh_token", refresh_token: credentials.refreshToken, ...grant.params(this) };
return grant.encoding === "json"
? this.refreshWithJSON(grant.url(), params, proxyOptions)
: this.refreshWithForm(grant.url(), params, proxyOptions);
}
async refreshCredentials(credentials, log, proxyOptions = null) {
if (!credentials.refreshToken) return null;
const refreshers = {
claude: () => this.refreshWithJSON(OAUTH_ENDPOINTS.anthropic.token, { grant_type: "refresh_token", refresh_token: credentials.refreshToken, client_id: PROVIDERS.claude.clientId }, proxyOptions),
codex: () => this.refreshWithForm(OAUTH_ENDPOINTS.openai.token, { grant_type: "refresh_token", refresh_token: credentials.refreshToken, client_id: PROVIDERS.codex.clientId, scope: "openid profile email offline_access" }, proxyOptions),
claude: () => this.refreshFromGrant(credentials, proxyOptions),
codex: () => this.refreshFromGrant(credentials, proxyOptions),
qwen: () => this.refreshWithForm(OAUTH_ENDPOINTS.qwen.token, { grant_type: "refresh_token", refresh_token: credentials.refreshToken, client_id: PROVIDERS.qwen.clientId }, proxyOptions),
iflow: () => this.refreshIflow(credentials.refreshToken, proxyOptions),
gemini: () => this.refreshGoogle(credentials.refreshToken, proxyOptions),
gemini: () => this.refreshFromGrant(credentials, proxyOptions),
kiro: () => this.refreshKiro(credentials.refreshToken, proxyOptions),
cline: () => this.refreshCline(credentials.refreshToken, proxyOptions),
"kimi-coding": () => this.refreshKimiCoding(credentials.refreshToken, proxyOptions),
@@ -258,17 +277,6 @@ export class DefaultExecutor extends BaseExecutor {
return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in };
}
async refreshGoogle(refreshToken, proxyOptions = null) {
const response = await proxyAwareFetch(OAUTH_ENDPOINTS.google.token, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json" },
body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: this.config.clientId, client_secret: this.config.clientSecret })
}, proxyOptions);
if (!response.ok) return null;
const tokens = await response.json();
return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in };
}
async refreshKiro(refreshToken, proxyOptions = null) {
const response = await proxyAwareFetch(PROVIDERS.kiro.tokenUrl, {
method: "POST",
@@ -281,37 +289,29 @@ export class DefaultExecutor extends BaseExecutor {
}
async refreshCline(refreshToken, proxyOptions = null) {
console.log('[DEBUG] Refreshing Cline token, refreshToken length:', refreshToken?.length);
const response = await proxyAwareFetch("https://api.cline.bot/api/v1/auth/refresh", {
const response = await proxyAwareFetch(PROVIDERS.cline.refreshUrl, {
method: "POST",
headers: { "Content-Type": "application/json", "Accept": "application/json" },
body: JSON.stringify({ refreshToken, grantType: "refresh_token", clientType: "extension" })
}, proxyOptions);
console.log('[DEBUG] Cline refresh response status:', response.status);
if (!response.ok) {
const errorText = await response.text();
console.log('[DEBUG] Cline refresh error:', errorText);
return null;
}
if (!response.ok) return null;
const payload = await response.json();
console.log('[DEBUG] Cline refresh payload:', JSON.stringify(payload).substring(0, 200));
const data = payload?.data || payload;
const expiresAtIso = data?.expiresAt;
const expiresIn = expiresAtIso ? Math.max(1, Math.floor((new Date(expiresAtIso).getTime() - Date.now()) / 1000)) : undefined;
console.log('[DEBUG] Cline refresh success, expiresIn:', expiresIn);
return { accessToken: data?.accessToken, refreshToken: data?.refreshToken || refreshToken, expiresIn };
}
async refreshKimiCoding(refreshToken, proxyOptions = null) {
const kimiHeaders = buildKimiHeaders();
const response = await proxyAwareFetch("https://auth.kimi.com/api/oauth/token", {
const response = await proxyAwareFetch(PROVIDERS["kimi-coding"].refreshUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
...kimiHeaders
},
body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: "17e5f671-d194-4dfb-9706-5516cb48c098" })
body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: PROVIDERS["kimi-coding"].clientId })
}, proxyOptions);
if (!response.ok) return null;
const tokens = await response.json();

View File

@@ -7,6 +7,8 @@ import { openaiResponsesToOpenAIResponse } from "../translator/response/openai-r
import { initState } from "../translator/index.js";
import { parseSSELine, formatSSE } from "../utils/streamHelpers.js";
import { proxyAwareFetch } from "../utils/proxyFetch.js";
import { stripUnsupportedParams } from "../translator/concerns/paramSupport.js";
import { SSE_DONE } from "../utils/sseConstants.js";
import crypto from "crypto";
export class GithubExecutor extends BaseExecutor {
@@ -108,54 +110,18 @@ export class GithubExecutor extends BaseExecutor {
return /gpt-5|o[134]-/i.test(model);
}
// Some models (like gpt-5.4) don't support the temperature parameter
supportsTemperature(model) {
// gpt-5.4 and similar newer models don't support temperature
return !/gpt-5\.4/i.test(model);
}
// GitHub Copilot /chat/completions rejects Claude-style thinking payloads
// (OpenClaw sends thinking: { type: "enabled" } → upstream 400).
// GPT-5 family on Copilot DOES honor reasoning_effort, so only strip for Claude. (#713)
supportsThinking(model) {
return !/claude/i.test(model);
}
// reasoning_effort works for GPT-5 family AND Claude Opus 4.6 / Sonnet 4.6
// on GitHub Copilot. Only strip for models that don't support it:
// Claude Haiku 4.5, Claude Opus 4.7 (rejected upstream).
supportsReasoningEffort(model) {
const m = model.toLowerCase();
// Claude models that DO support reasoning_effort
if (/claude.*opus.*4\.6/i.test(m) || /claude.*sonnet.*4\.6/i.test(m)) return true;
// All other Claude models: strip
if (/claude/i.test(model)) return false;
// GPT-5 family, Gemini, etc.: keep
return true;
}
transformRequest(model, body, stream, credentials) {
const transformed = { ...body };
if (this.requiresMaxCompletionTokens(model) && transformed.max_tokens !== undefined) {
transformed.max_completion_tokens = transformed.max_tokens;
delete transformed.max_tokens;
}
// Strip temperature for models that don't support it
if (!this.supportsTemperature(model) && transformed.temperature !== undefined) {
delete transformed.temperature;
}
// Always strip Claude-style thinking payload (Copilot doesn't understand it)
if (!this.supportsThinking(model)) {
delete transformed.thinking;
}
// "none" means no thinking — strip it so models that don't support "none" don't 400
if (transformed.reasoning_effort === "none") {
delete transformed.reasoning_effort;
}
// Strip reasoning_effort only for models that reject it
if (!this.supportsReasoningEffort(model) && transformed.reasoning_effort !== undefined) {
delete transformed.reasoning_effort;
}
// Config-driven strip of params unsupported by this provider/model
stripUnsupportedParams("github", model, transformed);
return transformed;
}
@@ -244,7 +210,7 @@ export class GithubExecutor extends BaseExecutor {
if (!parsed) continue;
if (parsed.done && stream === true) {
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"));
controller.enqueue(new TextEncoder().encode(SSE_DONE));
continue;
}

View File

@@ -1,5 +1,7 @@
import { BaseExecutor } from "./base.js";
import { PROVIDERS } from "../config/providers.js";
import { SSE_DONE, SSE_HEADERS_NO_BUFFER } from "../utils/sseConstants.js";
import { sseChunk } from "../utils/sse.js";
const GROK_CHAT_API = PROVIDERS["grok-web"].baseUrl;
const GROK_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36";
@@ -130,10 +132,6 @@ async function* extractContent(eventStream, isThinkingModel, signal) {
yield { done: true, fingerprint, responseId };
}
function sseChunk(data) {
return `data: ${JSON.stringify(data)}\n\n`;
}
function buildStreamingResponse(eventStream, model, cid, created, isThinkingModel, signal) {
const encoder = new TextEncoder();
return new ReadableStream({
@@ -175,13 +173,13 @@ function buildStreamingResponse(eventStream, model, cid, created, isThinkingMode
id: cid, object: "chat.completion.chunk", created, model, system_fingerprint: fp || null,
choices: [{ index: 0, delta: {}, finish_reason: "stop", logprobs: null }],
})));
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.enqueue(encoder.encode(SSE_DONE));
} catch (err) {
controller.enqueue(encoder.encode(sseChunk({
id: cid, object: "chat.completion.chunk", created, model, system_fingerprint: null,
choices: [{ index: 0, delta: { content: `[Stream error: ${err.message || String(err)}]` }, finish_reason: "stop", logprobs: null }],
})));
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.enqueue(encoder.encode(SSE_DONE));
} finally {
controller.close();
}
@@ -333,7 +331,7 @@ export class GrokWebExecutor extends BaseExecutor {
const sseStream = buildStreamingResponse(response.body, model, cid, created, isThinking, signal);
finalResponse = new Response(sseStream, {
status: 200,
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "X-Accel-Buffering": "no" },
headers: { ...SSE_HEADERS_NO_BUFFER },
});
} else {
finalResponse = await buildNonStreamingResponse(response.body, model, cid, created, isThinking, signal);

View File

@@ -17,6 +17,7 @@ import { OllamaLocalExecutor } from "./ollama-local.js";
import { CommandCodeExecutor } from "./commandcode.js";
import { XiaomiTokenplanExecutor } from "./xiaomi-tokenplan.js";
import { MimoFreeExecutor } from "./mimo-free.js";
import { CodeBuddyExecutor } from "./codebuddy-cn.js";
import { DefaultExecutor } from "./default.js";
const executors = {
@@ -42,6 +43,7 @@ const executors = {
"xiaomi-tokenplan": new XiaomiTokenplanExecutor(),
"mimo-free": new MimoFreeExecutor(),
mmf: new MimoFreeExecutor(), // Alias for mimo-free
"codebuddy-cn": new CodeBuddyExecutor(),
};
const defaultCache = new Map();
@@ -77,3 +79,4 @@ export { OllamaLocalExecutor } from "./ollama-local.js";
export { CommandCodeExecutor } from "./commandcode.js";
export { XiaomiTokenplanExecutor } from "./xiaomi-tokenplan.js";
export { MimoFreeExecutor } from "./mimo-free.js";
export { CodeBuddyExecutor } from "./codebuddy-cn.js";

View File

@@ -2,6 +2,7 @@ import { BaseExecutor } from "./base.js";
import { PROVIDERS } from "../config/providers.js";
import { v4 as uuidv4 } from "uuid";
import { refreshKiroToken } from "../services/tokenRefresh.js";
import { SSE_DONE, SSE_HEADERS } from "../utils/sseConstants.js";
/**
* KiroExecutor - Executor for Kiro AI (AWS CodeWhisperer)
@@ -19,13 +20,50 @@ export class KiroExecutor extends BaseExecutor {
"Amz-Sdk-Invocation-Id": uuidv4()
};
if (credentials.accessToken) {
// API-key auth: the key is stored as accessToken and sent as a bearer token
// exactly like an OAuth access token, but with an extra `tokentype: API_KEY`
// header so CodeWhisperer treats it as a long-lived API key rather than an
// OIDC/social access token. Mirrors the Kiro IDE headless-auth behavior.
const isApiKey = credentials?.providerSpecificData?.authMethod === "api_key";
const apiKey = credentials?.apiKey || (isApiKey ? credentials?.accessToken : null);
if (isApiKey && apiKey) {
headers["Authorization"] = `Bearer ${apiKey}`;
headers["tokentype"] = "API_KEY";
} else if (credentials.accessToken) {
headers["Authorization"] = `Bearer ${credentials.accessToken}`;
}
return headers;
}
/**
* Auth-aware endpoint ordering.
*
* API-key Kiro connections store a raw CodeWhisperer credential (validated
* against codewhisperer.us-east-1.amazonaws.com via ListAvailableProfiles).
* The Kiro IDE gateway (runtime.*.kiro.dev) expects Kiro OIDC/social tokens
* and rejects an `tokentype: API_KEY` token with 401/403 — which
* BaseExecutor.execute() returns immediately (only 429 / network errors fall
* through to the next host). So for api-key auth we must try the *.amazonaws.com
* CodeWhisperer hosts FIRST, mirroring the Kiro-Go reference fork which never
* routes api-key traffic through kiro.dev. OAuth keeps the default order
* (kiro.dev first) since its token is what that gateway accepts.
*/
getOrderedBaseUrls(credentials) {
const baseUrls = this.getBaseUrls();
const isApiKey = credentials?.providerSpecificData?.authMethod === "api_key";
if (!isApiKey) return baseUrls;
const amazon = baseUrls.filter((u) => u.includes("amazonaws.com"));
const others = baseUrls.filter((u) => !u.includes("amazonaws.com"));
return amazon.length > 0 ? [...amazon, ...others] : baseUrls;
}
buildUrl(model, stream, urlIndex = 0, credentials = null) {
const baseUrls = this.getOrderedBaseUrls(credentials);
return baseUrls[urlIndex] || baseUrls[0] || this.config.baseUrl;
}
transformRequest(model, body, stream, credentials) {
return body;
}
@@ -37,6 +75,8 @@ export class KiroExecutor extends BaseExecutor {
* BaseExecutor.execute() walks config.baseUrls (runtime.us-east-1.kiro.dev →
* codewhisperer → q) advancing to the next host on 429 (shouldRetry) and on
* network/5xx errors, while tryRetry handles in-place retries per `retry: {429: 2}`.
* Note: api-key connections reorder these so the *.amazonaws.com hosts come
* first — see getOrderedBaseUrls/buildUrl above.
* Note: the baseUrls are alternate surfaces of one regional service, so rotation
* is edge-level failover — it does not grant fresh 429 quota. Per-account 429
* spreading is handled upstream by account rotation in sse/handlers/chat.js.
@@ -73,6 +113,8 @@ export class KiroExecutor extends BaseExecutor {
const transformStream = new TransformStream({
async transform(chunk, controller) {
// Track output so we can emit a keepalive if this frame yields no chunk.
const enqueueCountBefore = chunkIndex;
// Append to buffer
const newBuffer = new Uint8Array(buffer.length + chunk.length);
newBuffer.set(buffer);
@@ -96,7 +138,7 @@ export class KiroExecutor extends BaseExecutor {
if (!event) continue;
const eventType = event.headers[":event-type"] || "";
// Track total content length for token estimation
if (!state.totalContentLength) state.totalContentLength = 0;
if (!state.contextUsagePercentage) state.contextUsagePercentage = 0;
@@ -105,7 +147,7 @@ export class KiroExecutor extends BaseExecutor {
if (eventType === "assistantResponseEvent" && event.payload?.content) {
const content = event.payload.content;
state.totalContentLength += content.length;
const chunk = {
id: responseId,
object: "chat.completion.chunk",
@@ -292,7 +334,7 @@ export class KiroExecutor extends BaseExecutor {
if (metrics && typeof metrics === 'object') {
const inputTokens = metrics.inputTokens || 0;
const outputTokens = metrics.outputTokens || 0;
if (inputTokens > 0 || outputTokens > 0) {
state.usage = {
prompt_tokens: inputTokens,
@@ -306,27 +348,27 @@ export class KiroExecutor extends BaseExecutor {
// Emit final chunk only after receiving BOTH meteringEvent AND contextUsageEvent
if (state.hasMeteringEvent && state.hasContextUsage && !state.finishEmitted) {
state.finishEmitted = true;
// Estimate tokens if not available from events
if (!state.usage) {
// Estimate output tokens from content length
const estimatedOutputTokens = state.totalContentLength > 0
const estimatedOutputTokens = state.totalContentLength > 0
? Math.max(1, Math.floor(state.totalContentLength / 4))
: 0;
// Estimate input tokens from contextUsagePercentage
// Kiro models typically have 200k context window
const estimatedInputTokens = state.contextUsagePercentage > 0
? Math.floor(state.contextUsagePercentage * 200000 / 100)
: 0;
state.usage = {
prompt_tokens: estimatedInputTokens,
completion_tokens: estimatedOutputTokens,
total_tokens: estimatedInputTokens + estimatedOutputTokens
};
}
const finishChunk = {
id: responseId,
object: "chat.completion.chunk",
@@ -338,12 +380,12 @@ export class KiroExecutor extends BaseExecutor {
finish_reason: state.hasToolCalls ? "tool_calls" : "stop"
}]
};
// Include usage in final chunk if available
if (state.usage) {
finishChunk.usage = state.usage;
}
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(finishChunk)}\n\n`));
}
}
@@ -351,6 +393,12 @@ export class KiroExecutor extends BaseExecutor {
if (iterations >= maxIterations) {
console.warn("[Kiro] Max iterations reached in event parsing");
}
// No client chunk produced this frame — emit an SSE comment keepalive
// so the stall watchdog sees upstream activity (ignored by parser/client).
if (chunkIndex === enqueueCountBefore && !state.finishEmitted) {
controller.enqueue(new TextEncoder().encode(": ka\n\n"));
}
},
flush(controller) {
@@ -372,24 +420,20 @@ export class KiroExecutor extends BaseExecutor {
}
// Send final done message
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"));
controller.enqueue(new TextEncoder().encode(SSE_DONE));
}
});
// Pipe response body through transform stream
if (!response.body) {
return new Response("data: [DONE]\n\n", { status: response.status, headers: { "Content-Type": "text/event-stream" } });
return new Response(SSE_DONE, { status: response.status, headers: { "Content-Type": "text/event-stream" } });
}
const transformedStream = response.body.pipeThrough(transformStream);
return new Response(transformedStream, {
status: response.status,
statusText: response.statusText,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive"
}
headers: { ...SSE_HEADERS }
});
}

View File

@@ -5,13 +5,20 @@ import { createHash } from "crypto";
import os from "os";
const BOOTSTRAP_URL = "https://api.xiaomimimo.com/api/free-ai/bootstrap";
const CHAT_URL = "https://api.xiaomimimo.com/api/free-ai/openai/chat";
const CHAT_URL = PROVIDERS["mimo-free"].baseUrl;
const SESSION_AFFINITY_PREFIX = "ses_";
const SESSION_ID_LENGTH = 24;
const JWT_FALLBACK_TTL_SEC = 3000;
const JWT_EXPIRY_BUFFER_MS = 300000;
const SESSION_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
// Anti-abuse gate: upstream rejects requests without a Chrome-like User-Agent with 403 "Illegal access"
const USER_AGENTS = [
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
];
// Anti-abuse gate marker: the free chat endpoint returns 403 "Illegal access"
// unless a system message contains this exact MiMoCode signature substring.
export const MIMO_SYSTEM_MARKER =
@@ -76,7 +83,10 @@ async function bootstrapJwt(proxyOptions = null) {
const response = await proxyAwareFetch(BOOTSTRAP_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: {
"Content-Type": "application/json",
"User-Agent": USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)],
},
body: JSON.stringify({ client: generateFingerprint() }),
}, proxyOptions);
@@ -108,6 +118,7 @@ export class MimoFreeExecutor extends BaseExecutor {
return {
"Content-Type": "application/json",
"X-Mimo-Source": "mimocode-cli-free",
"User-Agent": USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)],
"x-session-affinity": this.sessionId,
"Accept": stream ? "text/event-stream" : "application/json",
};

View File

@@ -1,9 +1,17 @@
import { BaseExecutor } from "./base.js";
import { PROVIDERS } from "../config/providers.js";
import { injectReasoningContent } from "../utils/reasoningContentInjector.js";
import { ANTHROPIC_API_VERSION } from "../providers/shared.js";
// Models that use /zen/go/v1/messages (Anthropic/Claude format + x-api-key auth)
const CLAUDE_FORMAT_MODELS = new Set(["minimax-m2.5", "minimax-m2.7"]);
const MESSAGES_FORMAT_MODELS = new Set([
"minimax-m3",
"minimax-m2.7",
"minimax-m2.5",
"qwen3.7-max",
"qwen3.7-plus",
"qwen3.6-plus",
]);
const BASE = "https://opencode.ai/zen/go/v1";
@@ -15,7 +23,7 @@ export class OpenCodeGoExecutor extends BaseExecutor {
// buildUrl runs before buildHeaders in BaseExecutor.execute, cache model here
buildUrl(model) {
this._lastModel = model;
return CLAUDE_FORMAT_MODELS.has(model)
return MESSAGES_FORMAT_MODELS.has(model)
? `${BASE}/messages`
: `${BASE}/chat/completions`;
}
@@ -24,9 +32,9 @@ export class OpenCodeGoExecutor extends BaseExecutor {
const key = credentials?.apiKey || credentials?.accessToken;
const headers = { "Content-Type": "application/json" };
if (CLAUDE_FORMAT_MODELS.has(this._lastModel)) {
if (MESSAGES_FORMAT_MODELS.has(this._lastModel)) {
headers["x-api-key"] = key;
headers["anthropic-version"] = "2023-06-01";
headers["anthropic-version"] = ANTHROPIC_API_VERSION;
} else {
headers["Authorization"] = `Bearer ${key}`;
}

View File

@@ -15,7 +15,7 @@ export class OpenCodeExecutor extends BaseExecutor {
}
buildUrl(model) {
const base = "https://opencode.ai";
const base = this.config.baseUrl;
return MESSAGES_MODELS.has(model)
? `${base}/zen/v1/messages`
: `${base}/zen/v1/chat/completions`;

View File

@@ -1,5 +1,7 @@
import { BaseExecutor } from "./base.js";
import { PROVIDERS } from "../config/providers.js";
import { SSE_DONE, SSE_HEADERS_NO_BUFFER } from "../utils/sseConstants.js";
import { sseChunk } from "../utils/sse.js";
const PPLX_SSE_ENDPOINT = PROVIDERS["perplexity-web"].baseUrl;
const PPLX_API_VERSION = "2.18";
@@ -289,10 +291,6 @@ async function* extractContent(eventStream, signal) {
yield { delta: "", answer: fullAnswer, backendUuid: backendUuid ?? undefined, done: true };
}
function sseChunk(data) {
return `data: ${JSON.stringify(data)}\n\n`;
}
function buildStreamingResponse(eventStream, model, cid, created, history, currentMsg, signal) {
const encoder = new TextEncoder();
return new ReadableStream({
@@ -340,7 +338,7 @@ function buildStreamingResponse(eventStream, model, cid, created, history, curre
id: cid, object: "chat.completion.chunk", created, model, system_fingerprint: null,
choices: [{ index: 0, delta: {}, finish_reason: "stop", logprobs: null }],
})));
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.enqueue(encoder.encode(SSE_DONE));
sessionStore(history, currentMsg, cleanResponse(fullAnswer), respBackendUuid);
} catch (err) {
@@ -348,7 +346,7 @@ function buildStreamingResponse(eventStream, model, cid, created, history, curre
id: cid, object: "chat.completion.chunk", created, model, system_fingerprint: null,
choices: [{ index: 0, delta: { content: `[Stream error: ${err.message || String(err)}]` }, finish_reason: "stop", logprobs: null }],
})));
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.enqueue(encoder.encode(SSE_DONE));
} finally {
controller.close();
}
@@ -493,7 +491,7 @@ export class PerplexityWebExecutor extends BaseExecutor {
const sseStream = buildStreamingResponse(response.body, model, cid, created, parsed.history, parsed.currentMsg, signal);
finalResponse = new Response(sseStream, {
status: 200,
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "X-Accel-Buffering": "no" },
headers: { ...SSE_HEADERS_NO_BUFFER },
});
} else {
finalResponse = await buildNonStreamingResponse(response.body, model, cid, created, parsed.history, parsed.currentMsg, signal);

View File

@@ -20,19 +20,20 @@
* different model upstream, so a missing entry is a hard error.
*/
import { qoderEncodeBody } from "@/lib/qoder/encoding.js";
import { buildCosyHeaders } from "@/lib/qoder/cosy.js";
import { qoderEncodeBody } from "../shared/qoder/encoding.js";
import { buildCosyHeaders } from "../shared/qoder/cosy.js";
import { v4 as uuidv4 } from "uuid";
import { createHash } from "crypto";
import { BaseExecutor } from "./base.js";
import { PROVIDERS } from "../config/providers.js";
import { proxyAwareFetch } from "../utils/proxyFetch.js";
import { SSE_DONE } from "../utils/sseConstants.js";
import { FETCH_CONNECT_TIMEOUT_MS } from "../config/runtimeConfig.js";
import {
QODER_CHAT_URL_ENCODED,
QODER_MODEL_MAP,
} from "@/lib/qoder/constants.js";
} from "../shared/qoder/constants.js";
import { getQoderModelConfig, resolveQoderModels } from "../services/qoderModels.js";
/**
@@ -356,7 +357,7 @@ async function wrapQoderSSE(response, model, midStreamError = {}) {
const data = trimmed.slice(5).trimStart();
if (data === "[DONE]") {
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.enqueue(encoder.encode(SSE_DONE));
doneEmitted = true;
return;
}
@@ -377,13 +378,13 @@ async function wrapQoderSSE(response, model, midStreamError = {}) {
choices: [{ index: 0, delta: { content: `\n\n${parsed.message}` }, finish_reason: "stop" }],
});
controller.enqueue(encoder.encode(`data: ${errChunk}\n\n`));
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.enqueue(encoder.encode(SSE_DONE));
doneEmitted = true;
return;
}
if (!inner) return;
if (inner === "[DONE]") {
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.enqueue(encoder.encode(SSE_DONE));
doneEmitted = true;
return;
}
@@ -408,7 +409,7 @@ async function wrapQoderSSE(response, model, midStreamError = {}) {
buffer = "";
}
if (!doneEmitted) {
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.enqueue(encoder.encode(SSE_DONE));
doneEmitted = true;
}
},

View File

@@ -1,18 +1,19 @@
import { DefaultExecutor } from "./default.js";
import { resolveXiaomiTokenplanBaseUrl } from "../config/providers.js";
import { getModelTargetFormat } from "../config/providerModels.js";
import { FORMATS } from "../translator/formats.js";
// import { getModelTargetFormat } from "../config/providerModels.js";
// import { FORMATS } from "../translator/formats.js";
export class XiaomiTokenplanExecutor extends DefaultExecutor {
constructor() {
super("xiaomi-tokenplan");
}
// Claude-native aliases route to the Anthropic-compatible messages endpoint
// Token Plan keys are region-specific. Route per sourceFormat-matched transport:
// claude → Anthropic /anthropic/v1/messages, openai → /chat/completions.
buildUrl(model, stream, urlIndex = 0, credentials = null) {
const baseUrl = resolveXiaomiTokenplanBaseUrl(credentials);
if (getModelTargetFormat(model, model) === FORMATS.CLAUDE) {
return `${baseUrl.replace(/\/v1\/?$/, "/anthropic/v1")}/messages`;
if (credentials?.runtimeTransport?.format === "claude") {
return `${baseUrl.replace(/\/v1\/?$/, "")}/anthropic/v1/messages`;
}
return `${baseUrl}/chat/completions`;
}

View File

@@ -1,12 +1,13 @@
import { detectFormat, getTargetFormat } from "../services/provider.js";
import { detectFormat, getTargetFormat, resolveTransport } from "../services/provider.js";
import { translateRequest } from "../translator/index.js";
import { FORMATS } from "../translator/formats.js";
import { normalizeClaudePassthrough } from "../translator/helpers/claudeHelper.js";
import { normalizeClaudePassthrough } from "../translator/formats/claude.js";
import { COLORS } from "../utils/stream.js";
import { createStreamController } from "../utils/streamHandler.js";
import { refreshWithRetry } from "../services/tokenRefresh.js";
import { createRequestLogger } from "../utils/requestLogger.js";
import { getModelTargetFormat, getModelStrip, getModelUpstreamId, getModelType, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.js";
import { PROVIDERS } from "../config/providers.js";
import { createErrorResult, parseUpstreamError, formatProviderError } from "../utils/error.js";
import { HTTP_STATUS } from "../config/runtimeConfig.js";
import { handleBypassRequest } from "../utils/bypassHandler.js";
@@ -19,7 +20,12 @@ import { handleStreamingResponse, buildOnStreamComplete } from "./chatCore/strea
import { detectClientTool, isNativePassthrough } from "../utils/clientDetector.js";
import { dedupeTools } from "../utils/toolDeduper.js";
import { injectCaveman } from "../rtk/caveman.js";
import { injectPonytail } from "../rtk/ponytail.js";
import { compressMessages, formatRtkLog } from "../rtk/index.js";
import { compressWithHeadroom, formatHeadroomLog } from "../rtk/headroom.js";
import { getCapabilitiesForModel } from "../providers/capabilities.js";
import { stripUnsupportedModalities } from "../translator/concerns/modality.js";
import { prefetchRemoteImages } from "../translator/concerns/prefetch.js";
/**
* Core chat handler - shared between SSE and Worker
@@ -28,7 +34,7 @@ import { compressMessages, formatRtkLog } from "../rtk/index.js";
* @param {object} options.credentials - Provider credentials
* @param {string} options.sourceFormatOverride - Override detected source format (e.g. "openai-responses")
*/
export async function handleChatCore({ body, modelInfo, credentials, log, onCredentialsRefreshed, onRequestSuccess, onDisconnect, onMidStreamError, clientRawRequest, connectionId, userAgent, apiKey, ccFilterNaming, rtkEnabled, cavemanEnabled, cavemanLevel, sourceFormatOverride, providerThinking }) {
export async function handleChatCore({ body, modelInfo, credentials, log, onCredentialsRefreshed, onRequestSuccess, onDisconnect, onMidStreamError, clientRawRequest, connectionId, userAgent, apiKey, ccFilterNaming, rtkEnabled, headroomEnabled, headroomUrl, headroomCompressUserMessages, cavemanEnabled, cavemanLevel, ponytailEnabled, ponytailLevel, sourceFormatOverride, providerThinking }) {
const { provider, model } = modelInfo;
const requestStartTime = Date.now();
@@ -40,7 +46,10 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
const alias = PROVIDER_ID_TO_ALIAS[provider] || provider;
const modelTargetFormat = getModelTargetFormat(alias, model);
const targetFormat = modelTargetFormat || getTargetFormat(provider);
// Multi-endpoint providers: pick transport matching sourceFormat → zero translation
const runtimeTransport = resolveTransport(provider, sourceFormat);
const targetFormat = modelTargetFormat || runtimeTransport?.format || getTargetFormat(provider);
if (runtimeTransport && credentials) credentials.runtimeTransport = runtimeTransport;
const stripList = getModelStrip(alias, model);
const upstreamModel = getModelUpstreamId(alias, model);
@@ -59,9 +68,16 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
}
const clientRequestedStreaming = body.stream === true || sourceFormat === FORMATS.ANTIGRAVITY || sourceFormat === FORMATS.GEMINI || sourceFormat === FORMATS.GEMINI_CLI;
const providerRequiresStreaming = provider === "openai" || provider === "codex" || provider === "commandcode";
const providerRequiresStreaming = PROVIDERS[provider]?.forceStream === true;
let stream = providerRequiresStreaming ? true : (body.stream !== false);
// Image generation models require non-streaming (Google v1internal:generateContent)
const modelType = getModelType(alias, model);
const isImageGenModel = modelType === "imageGen" || /image|imagen|image-generation/i.test(model);
if (isImageGenModel && (provider === "antigravity" || provider === "gemini-cli")) {
stream = false;
}
// DeepSeek-TUI: interactive TUI panel sends stream:true and needs SSE.
// Non-interactive mode (-p flag) sends without stream and can't parse SSE.
// Only force non-streaming when client didn't explicitly request it.
@@ -87,6 +103,22 @@ 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 || {};
// Auto-strip media blocks the model can't read (vision/audio/pdf) before translation.
if (!passthrough) {
const caps = getCapabilitiesForModel(provider, model);
if (stripUnsupportedModalities(body, sourceFormat, caps)) {
log?.debug?.("MODALITY", `stripped unsupported media for ${provider}/${model}`);
}
// Convert remote image URLs to base64 for targets that can't fetch URLs.
try {
const n = await prefetchRemoteImages(body, sourceFormat, targetFormat, { signal: undefined });
if (n > 0) log?.debug?.("MODALITY", `prefetched ${n} remote image(s) for ${targetFormat}`);
} catch (e) { log?.warn?.("MODALITY", `image prefetch failed: ${e.message}`); }
}
let translatedBody;
let toolNameMap;
if (passthrough) {
@@ -129,12 +161,23 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
const rtkLine = formatRtkLog(rtkStats);
if (rtkLine) console.log(rtkLine);
// Headroom: optional external proxy compression; fail open if proxy is absent.
const headroomStats = await compressWithHeadroom(translatedBody, { enabled: headroomEnabled, url: headroomUrl, model: upstreamModel, format: finalFormat, compressUserMessages: headroomCompressUserMessages });
const headroomLine = formatHeadroomLog(headroomStats);
if (headroomLine) log?.info?.("HEADROOM", headroomLine);
// Caveman: inject terse-style system prompt
if (cavemanEnabled && cavemanLevel) {
injectCaveman(translatedBody, finalFormat, cavemanLevel);
log?.debug?.("CAVEMAN", `${cavemanLevel} | ${finalFormat}`);
}
// Ponytail: inject lazy-senior-dev system prompt
if (ponytailEnabled && ponytailLevel) {
injectPonytail(translatedBody, finalFormat, ponytailLevel);
log?.debug?.("PONYTAIL", `${ponytailLevel} | ${finalFormat}`);
}
const executor = getExecutor(provider);
trackPendingRequest(model, provider, connectionId, true);
appendRequestLog({ model, provider, connectionId, status: "PENDING" }).catch(() => { });

View File

@@ -37,6 +37,12 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
function: { name: part.functionCall.name, arguments: JSON.stringify(part.functionCall.args || {}) }
});
}
// Handle inline image data (from image generation models)
const inlineData = part.inlineData || part.inline_data;
if (inlineData?.data) {
const mimeType = inlineData.mimeType || inlineData.mime_type || "image/png";
textContent += `\n![image](data:${mimeType};base64,${inlineData.data})\n`;
}
}
}
@@ -76,7 +82,12 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
// missing/null (e.g. M3 with max_tokens:1 spends the budget on thinking
// and returns `content: null`). Returning the raw body would leave the
// OpenAI client without a `choices` array and surface as a UI test error.
if (responseBody.content && !Array.isArray(responseBody.content)) return responseBody;
// Early return if the response is already in OpenAI format (has choices array)
// or if it has content as a non-array value (likely a different non-Claude format).
// Some providers (e.g. xiaomi-tokenplan) return OpenAI-format responses even when
// the request was translated to Claude format — the targetFormat is Claude but the
// actual response is OpenAI-native and needs no further translation.
if (responseBody.choices || (responseBody.content && !Array.isArray(responseBody.content))) return responseBody;
let textContent = "", thinkingContent = "";
const toolCalls = [];
@@ -208,11 +219,14 @@ export async function handleNonStreamingResponse({ providerResponse, provider, m
translatedResponse.usage = filterUsageForFormat(addBufferToUsage(translatedResponse.usage), sourceFormat);
}
// Strip reasoning_content — some clients (e.g. Firecrawl AI SDK) have JSON parsers that
// break on this non-standard field, even though OpenAI allows it in extensions.
// Strip reasoning_content only when content is non-empty.
// When content is empty (e.g. thinking models that used all tokens for reasoning),
// reasoning_content is the only useful output and must be preserved.
if (translatedResponse?.choices) {
for (const choice of translatedResponse.choices) {
if (choice?.message) delete choice.message.reasoning_content;
if (choice?.message?.reasoning_content && choice.message.content) {
delete choice.message.reasoning_content;
}
}
}

View File

@@ -2,7 +2,11 @@ import { convertResponsesStreamToJson } from "../../transformer/streamToJsonConv
import { createErrorResult } from "../../utils/error.js";
import { HTTP_STATUS } from "../../config/runtimeConfig.js";
import { FORMATS } from "../../translator/formats.js";
import { PROVIDERS } from "../../config/providers.js";
import { buildRequestDetail, extractRequestConfig, saveUsageStats } from "./requestDetail.js";
// Responses-API providers (e.g. codex) may emit SSE without content-type + use Responses output shape
const isResponsesProvider = (p) => PROVIDERS[p]?.format === FORMATS.OPENAI_RESPONSES;
import { saveRequestDetail, appendRequestLog } from "@/lib/usageDb.js";
function textFromResponsesMessageItem(item) {
@@ -100,7 +104,7 @@ export function parseSSEToOpenAIResponse(rawSSE, fallbackModel) {
*/
export async function handleForcedSSEToJson({ providerResponse, sourceFormat, provider, model, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess, trackDone, appendLog }) {
const contentType = providerResponse.headers.get("content-type") || "";
const isSSE = contentType.includes("text/event-stream") || (contentType === "" && provider === "codex");
const isSSE = contentType.includes("text/event-stream") || (contentType === "" && isResponsesProvider(provider));
if (!isSSE) return null; // not handled here
trackDone();
@@ -112,7 +116,7 @@ export async function handleForcedSSEToJson({ providerResponse, sourceFormat, pr
};
// Codex/Responses API SSE path
const isCodexResponsesApi = provider === "codex" || sourceFormat === FORMATS.OPENAI_RESPONSES;
const isCodexResponsesApi = isResponsesProvider(provider) || sourceFormat === FORMATS.OPENAI_RESPONSES;
if (isCodexResponsesApi) {
try {
const jsonResponse = await convertResponsesStreamToJson(providerResponse.body);

View File

@@ -7,12 +7,16 @@ import { STREAM_STALL_TIMEOUT_MS } from "../../config/runtimeConfig.js";
import { buildAbortedResponsesTerminalBytes } from "../../utils/responsesStreamHelpers.js";
import { buildRequestDetail, extractRequestConfig } from "./requestDetail.js";
import { saveRequestDetail } from "@/lib/usageDb.js";
import { SSE_HEADERS_CORS as SSE_HEADERS } from "../../utils/sseConstants.js";
const SSE_HEADERS = {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Access-Control-Allow-Origin": "*"
// Codex returns Responses API SSE → which client format to translate INTO, by request sourceFormat.
// Gemini-family all map to ANTIGRAVITY decoder; unknown sources fall back to OPENAI.
const CODEX_SOURCE_TO_TARGET = {
[FORMATS.OPENAI_RESPONSES]: FORMATS.OPENAI_RESPONSES,
[FORMATS.CLAUDE]: FORMATS.CLAUDE,
[FORMATS.ANTIGRAVITY]: FORMATS.ANTIGRAVITY,
[FORMATS.GEMINI]: FORMATS.ANTIGRAVITY,
[FORMATS.GEMINI_CLI]: FORMATS.ANTIGRAVITY,
};
/**
@@ -20,15 +24,12 @@ const SSE_HEADERS = {
*/
function buildTransformStream({ provider, sourceFormat, targetFormat, userAgent, reqLogger, toolNameMap, model, connectionId, body, onStreamComplete, apiKey }) {
const isDroidCLI = userAgent?.toLowerCase().includes("droid") || userAgent?.toLowerCase().includes("codex-cli");
const needsCodexTranslation = provider === "codex" && targetFormat === FORMATS.OPENAI_RESPONSES && !isDroidCLI;
// Responses-API providers (e.g. codex) emit Responses SSE → translate into client format
const isResponsesProvider = PROVIDERS[provider]?.format === FORMATS.OPENAI_RESPONSES;
const needsCodexTranslation = isResponsesProvider && targetFormat === FORMATS.OPENAI_RESPONSES && !isDroidCLI;
if (needsCodexTranslation) {
// Codex returns Responses API SSE → translate to client format
let codexTarget;
if (sourceFormat === FORMATS.OPENAI_RESPONSES) codexTarget = FORMATS.OPENAI_RESPONSES;
else if (sourceFormat === FORMATS.CLAUDE) codexTarget = FORMATS.CLAUDE;
else if (sourceFormat === FORMATS.ANTIGRAVITY || sourceFormat === FORMATS.GEMINI || sourceFormat === FORMATS.GEMINI_CLI) codexTarget = FORMATS.ANTIGRAVITY;
else codexTarget = FORMATS.OPENAI;
const codexTarget = CODEX_SOURCE_TO_TARGET[sourceFormat] || FORMATS.OPENAI;
return createSSETransformStreamWithLogger(FORMATS.OPENAI_RESPONSES, codexTarget, provider, reqLogger, toolNameMap, model, connectionId, body, onStreamComplete, apiKey);
}

View File

@@ -1,30 +1,21 @@
// OpenAI-compatible embeddings adapter (most providers)
import { bearerAuth } from "./_base.js";
import { PROVIDER_MEDIA } from "../../providers/index.js";
// media-only providers without a registry file keep URL here; rest derive from registry media.embeddingConfig.baseUrl
const ENDPOINTS = {
openai: "https://api.openai.com/v1/embeddings",
openrouter: "https://openrouter.ai/api/v1/embeddings",
mistral: "https://api.mistral.ai/v1/embeddings",
"voyage-ai": "https://api.voyageai.com/v1/embeddings",
fireworks: "https://api.fireworks.ai/inference/v1/embeddings",
together: "https://api.together.xyz/v1/embeddings",
nebius: "https://api.tokenfactory.nebius.com/v1/embeddings",
github: "https://models.github.ai/inference/embeddings",
nvidia: "https://integrate.api.nvidia.com/v1/embeddings",
"jina-ai": "https://api.jina.ai/v1/embeddings",
"vercel-ai-gateway": "https://ai-gateway.vercel.sh/v1/embeddings",
};
const embedCfg = (id) => PROVIDER_MEDIA[id]?.embeddingConfig || {};
const embedUrl = (id) => embedCfg(id).baseUrl || ENDPOINTS[id];
export default function createOpenAIEmbeddingAdapter(providerId) {
const cfg = embedCfg(providerId);
return {
buildUrl: () => ENDPOINTS[providerId],
buildUrl: () => embedUrl(providerId),
buildHeaders: (creds) => {
const headers = { "Content-Type": "application/json", ...bearerAuth(creds) };
if (providerId === "openrouter") {
headers["HTTP-Referer"] = "https://endpoint-proxy.local";
headers["X-Title"] = "Endpoint Proxy";
}
return headers;
return { "Content-Type": "application/json", ...bearerAuth(creds), ...(cfg.headers || {}) };
},
buildBody: (model, { input, encoding_format, dimensions }) => {
const body = { model, input };

View File

@@ -50,6 +50,47 @@ export async function handleImageGenerationCore({
);
}
// Executor-delegating adapters: skip manual URL/headers/body, use the proven executor flow
if (adapter.useExecutor && adapter.executeViaExecutor) {
try {
log?.debug?.("IMAGE", `${provider.toUpperCase()} | ${model} | prompt="${body.prompt.slice(0, 50)}..." (executor)`);
const responseBody = await adapter.executeViaExecutor(model, body, credentials, log);
if (onRequestSuccess) await onRequestSuccess();
const normalized = adapter.normalize(responseBody, body.prompt);
const finalBody = (normalized.created && Array.isArray(normalized.data)) ? normalized : responseBody;
if (binaryOutput) {
const first = finalBody.data?.[0];
let b64 = first?.b64_json;
if (!b64 && first?.url) {
try { b64 = await urlToBase64(first.url); } catch {}
}
if (b64) {
const buf = Buffer.from(b64, "base64");
const fmt = (body.output_format || "png").toLowerCase();
const mime = fmt === "jpeg" || fmt === "jpg" ? "image/jpeg" : fmt === "webp" ? "image/webp" : "image/png";
return {
success: true,
response: new Response(buf, {
headers: { "Content-Type": mime, "Content-Disposition": `inline; filename="image.${fmt === "jpeg" ? "jpg" : fmt}"`, "Access-Control-Allow-Origin": "*" },
}),
};
}
}
return {
success: true,
response: new Response(JSON.stringify(finalBody), {
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
}),
};
} catch (error) {
const errMsg = formatProviderError(error, provider, model, HTTP_STATUS.BAD_GATEWAY);
log?.debug?.("IMAGE", `Executor error: ${errMsg}`);
return createErrorResult(HTTP_STATUS.BAD_GATEWAY, errMsg);
}
}
let url;
let headers;
let requestBody;

View File

@@ -0,0 +1,73 @@
// Antigravity image adapter - delegates to the executor for correct request
// envelope (project, model, requestType, sessionId) and auth headers.
import { nowSec } from "./_base.js";
import { getExecutor } from "../../executors/index.js";
// Convert image input (data URI or raw base64) to Gemini inlineData part
function resolveImageInput(input) {
if (!input || typeof input !== "string") return null;
// data:image/png;base64,... format
const dataUriMatch = input.match(/^data:(image\/[^;]+);base64,(.+)$/);
if (dataUriMatch) {
return { inlineData: { mimeType: dataUriMatch[1], data: dataUriMatch[2] } };
}
// Raw base64 string (assume PNG)
if (/^[A-Za-z0-9+/]/.test(input) && input.length > 100 && !input.startsWith("http")) {
return { inlineData: { mimeType: "image/png", data: input } };
}
return null;
}
export default {
// Delegate to executor instead of building URL/headers/body manually
useExecutor: true,
// Stubs - required by imageGenerationCore interface but unused with useExecutor
buildUrl: () => "",
buildHeaders: () => ({}),
buildBody: () => ({}),
async executeViaExecutor(model, body, credentials, log) {
const executor = getExecutor("antigravity");
if (!executor) throw new Error("Antigravity executor not found");
// Build parts: text prompt + optional input image for editing
const parts = [{ text: body.prompt }];
const imageInput = body.image || (Array.isArray(body.images) && body.images[0]);
if (imageInput) {
const inlineData = resolveImageInput(imageInput);
if (inlineData) parts.unshift(inlineData);
}
const chatBody = {
contents: [{ role: "user", parts }],
};
const result = await executor.execute({
model,
body: chatBody,
stream: false,
credentials,
log,
});
if (!result.response.ok) {
const text = await result.response.text();
throw new Error(text || `HTTP ${result.response.status}`);
}
return result.response.json();
},
normalize: (responseBody, prompt) => {
const candidates = responseBody.candidates || responseBody.response?.candidates || [];
const parts = candidates[0]?.content?.parts || [];
const images = parts.filter((p) => p.inlineData?.data).map((p) => ({
b64_json: p.inlineData.data,
}));
return {
created: nowSec(),
data: images.length > 0 ? images : [{ b64_json: "", revised_prompt: prompt }],
};
},
};

View File

@@ -1,7 +1,8 @@
// Black Forest Labs (FLUX) — async submit + polling_url
import { sleep, nowSec, POLL_INTERVAL_MS, POLL_TIMEOUT_MS } from "./_base.js";
import { PROVIDER_MEDIA } from "../../providers/index.js";
const BASE_URL = "https://api.bfl.ai/v1";
const BASE_URL = PROVIDER_MEDIA["black-forest-labs"]?.imageConfig?.baseUrl;
export default {
async: true,

View File

@@ -1,6 +1,7 @@
import { nowSec, urlToBase64 } from "./_base.js";
import { PROVIDER_MEDIA } from "../../providers/index.js";
const BASE_URL = "https://api.cloudflare.com/client/v4/accounts";
const BASE_URL = PROVIDER_MEDIA["cloudflare-ai"]?.imageConfig?.baseUrl;
const MULTIPART_MODELS = new Set([
"@cf/black-forest-labs/flux-2-dev",

View File

@@ -1,8 +1,9 @@
// Codex (ChatGPT Plus/Pro) image generation via Responses API + SSE
import { randomUUID } from "node:crypto";
import { nowSec } from "./_base.js";
import { PROVIDERS } from "../../config/providers.js";
const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
const CODEX_RESPONSES_URL = PROVIDERS["codex"].baseUrl;
const CODEX_USER_AGENT = "codex_cli_rs/0.136.0";
const CODEX_VERSION = "0.136.0";
const CODEX_ORIGINATOR = "codex_cli_rs";

View File

@@ -1,7 +1,11 @@
// ComfyUI — local, noAuth (placeholder; full graph workflow not implemented)
import { PROVIDER_MEDIA } from "../../providers/index.js";
const BASE_URL = PROVIDER_MEDIA["comfyui"]?.imageConfig?.baseUrl;
export default {
noAuth: true,
buildUrl: () => "http://localhost:8188",
buildUrl: () => BASE_URL,
buildHeaders: () => ({ "Content-Type": "application/json" }),
buildBody: (_model, body) => ({ prompt: body.prompt }),
normalize: (responseBody) => responseBody,

View File

@@ -1,7 +1,8 @@
// Fal.ai — async submit + queue polling
import { sleep, nowSec, sizeToAspectRatio, POLL_INTERVAL_MS, POLL_TIMEOUT_MS } from "./_base.js";
import { PROVIDER_MEDIA } from "../../providers/index.js";
const BASE_URL = "https://queue.fal.run";
const BASE_URL = PROVIDER_MEDIA["fal-ai"]?.imageConfig?.baseUrl;
export default {
async: true,

View File

@@ -1,7 +1,8 @@
// Google Gemini adapter (Nano Banana models)
import { nowSec } from "./_base.js";
import { PROVIDER_MEDIA } from "../../providers/index.js";
const BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models";
const BASE_URL = PROVIDER_MEDIA["gemini"]?.imageConfig?.baseUrl;
export default {
buildUrl: (model, creds) => {

View File

@@ -1,7 +1,8 @@
// HuggingFace Inference API — returns binary image
import { nowSec } from "./_base.js";
import { PROVIDER_MEDIA } from "../../providers/index.js";
const BASE_URL = "https://api-inference.huggingface.co/models";
const BASE_URL = PROVIDER_MEDIA["huggingface"]?.imageConfig?.baseUrl;
export default {
buildUrl: (model) => `${BASE_URL}/${model}`,

View File

@@ -11,6 +11,7 @@ import stabilityAi from "./stabilityAi.js";
import blackForestLabs from "./blackForestLabs.js";
import runwayml from "./runwayml.js";
import cloudflareAi from "./cloudflareAi.js";
import antigravity from "./antigravity.js";
const ADAPTERS = {
openai: createOpenAIAdapter("openai"),
@@ -25,6 +26,7 @@ const ADAPTERS = {
comfyui,
huggingface,
nanobanana,
antigravity,
"fal-ai": falAi,
"stability-ai": stabilityAi,
"black-forest-labs": blackForestLabs,

View File

@@ -1,8 +1,10 @@
// NanoBanana API — async submit + poll record-info
import { sleep, nowSec, sizeToAspectRatio, POLL_INTERVAL_MS, POLL_TIMEOUT_MS } from "./_base.js";
import { PROVIDER_MEDIA } from "../../providers/index.js";
const SUBMIT_URL = "https://api.nanobananaapi.ai/api/v1/nanobanana/generate";
const POLL_BASE = "https://api.nanobananaapi.ai/api/v1/nanobanana/record-info";
const IMG_CFG = PROVIDER_MEDIA["nanobanana"]?.imageConfig || {};
const SUBMIT_URL = IMG_CFG.baseUrl;
const POLL_BASE = IMG_CFG.pollUrl;
export default {
async: true,

View File

@@ -1,40 +1,32 @@
// OpenAI-compatible adapter (used by openai, minimax, openrouter, recraft)
import { PROVIDER_MEDIA } from "../../providers/index.js";
const ENDPOINTS = {
openai: "https://api.openai.com/v1/images/generations",
minimax: "https://api.minimaxi.com/v1/images/generations",
openrouter: "https://openrouter.ai/api/v1/images/generations",
recraft: "https://external.api.recraft.ai/v1/images/generations",
"vercel-ai-gateway": "https://ai-gateway.vercel.sh/v1/images/generations",
xai: "https://api.x.ai/v1/images/generations",
};
const imageCfg = (id) => PROVIDER_MEDIA[id]?.imageConfig || {};
const imageUrl = (id) => imageCfg(id).baseUrl;
export default function createOpenAIAdapter(providerId) {
const cfg = imageCfg(providerId);
return {
buildUrl: () => ENDPOINTS[providerId],
buildUrl: () => imageUrl(providerId),
buildHeaders: (creds) => {
const headers = { "Content-Type": "application/json" };
const headers = { "Content-Type": "application/json", ...(cfg.headers || {}) };
const key = creds?.apiKey || creds?.accessToken;
if (key) headers["Authorization"] = `Bearer ${key}`;
if (providerId === "openrouter") {
headers["HTTP-Referer"] = "https://endpoint-proxy.local";
headers["X-Title"] = "Endpoint Proxy";
}
return headers;
},
buildBody: (model, body) => {
const { prompt, n = 1, size = "1024x1024", quality, style, response_format } = body;
// xAI only accepts prompt, model, n, response_format
if (providerId === "xai") {
const req = { model, prompt, n };
if (response_format) req.response_format = response_format;
const full = { model, prompt, n, size };
if (quality) full.quality = quality;
if (style) full.style = style;
if (response_format) full.response_format = response_format;
// bodyFields whitelist (e.g. xAI accepts only model/prompt/n/response_format)
if (Array.isArray(cfg.bodyFields)) {
const req = {};
for (const f of cfg.bodyFields) if (full[f] !== undefined) req[f] = full[f];
return req;
}
const req = { model, prompt, n, size };
if (quality) req.quality = quality;
if (style) req.style = style;
if (response_format) req.response_format = response_format;
return req;
return full;
},
normalize: (responseBody) => responseBody,
};

View File

@@ -1,7 +1,8 @@
// Runway ML — async submit + /tasks/{id} polling
import { sleep, nowSec, sizeToAspectRatio, POLL_INTERVAL_MS, POLL_TIMEOUT_MS } from "./_base.js";
import { PROVIDER_MEDIA } from "../../providers/index.js";
const BASE_URL = "https://api.dev.runwayml.com/v1";
const BASE_URL = PROVIDER_MEDIA["runwayml"]?.imageConfig?.baseUrl;
export default {
async: true,

View File

@@ -1,9 +1,12 @@
// SD WebUI (AUTOMATIC1111) — local, noAuth
import { nowSec } from "./_base.js";
import { PROVIDER_MEDIA } from "../../providers/index.js";
const BASE_URL = PROVIDER_MEDIA["sdwebui"]?.imageConfig?.baseUrl;
export default {
noAuth: true,
buildUrl: () => "http://localhost:7860/sdapi/v1/txt2img",
buildUrl: () => BASE_URL,
buildHeaders: () => ({ "Content-Type": "application/json" }),
buildBody: (_model, body) => {
const { prompt, n = 1, size = "1024x1024" } = body;

View File

@@ -1,7 +1,8 @@
// Stability AI v2 — sync, returns { image: "<b64>" }
import { nowSec, sizeToAspectRatio } from "./_base.js";
import { PROVIDER_MEDIA } from "../../providers/index.js";
const BASE_URL = "https://api.stability.ai/v2beta/stable-image/generate";
const BASE_URL = PROVIDER_MEDIA["stability-ai"]?.imageConfig?.baseUrl;
// Map model id → endpoint segment
function modelToEndpoint(model) {

View File

@@ -4,9 +4,10 @@
*/
import { handleChatCore } from "./chatCore.js";
import { convertResponsesApiFormat } from "../translator/helpers/responsesApiHelper.js";
import { convertResponsesApiFormat } from "../translator/formats/responsesApi.js";
import { createResponsesApiTransformStream } from "../transformer/responsesTransformer.js";
import { convertResponsesStreamToJson } from "../transformer/streamToJsonConverter.js";
import { SSE_HEADERS_CORS } from "../utils/sseConstants.js";
/**
* Handle /v1/responses request
@@ -87,12 +88,7 @@ export async function handleResponsesCore({ body, modelInfo, credentials, log, o
success: true,
response: new Response(transformedBody, {
status: 200,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Access-Control-Allow-Origin": "*"
}
headers: { ...SSE_HEADERS_CORS }
})
};
}

View File

@@ -2,6 +2,12 @@
* Wrap chat-completions endpoints (with built-in web search) into the unified
* /v1/search response format. Supports gemini, openai, xai, kimi, minimax, perplexity.
*/
import { PROVIDER_MEDIA } from "../../providers/index.js";
// Default search model + endpoint derive from registry searchViaChat (single source)
const searchModel = (id) => PROVIDER_MEDIA[id]?.searchViaChat?.defaultModel;
const searchEndpoint = (id, model) =>
(PROVIDER_MEDIA[id]?.searchViaChat?.endpoint || "").replace("{model}", model || "");
const REQUEST_TIMEOUT_MS = 15000;
const DEFAULT_MAX_RESULTS = 10;
@@ -43,9 +49,7 @@ function normalizeCitation(c) {
*/
const CHAT_SEARCH_CONFIG = {
gemini: {
endpoint: (model) =>
`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`,
defaultModel: "gemini-2.5-flash",
endpoint: (model) => searchEndpoint("gemini", model),
buildBody: (query) => ({
contents: [{ role: "user", parts: [{ text: query }] }],
tools: [{ google_search: {} }]
@@ -70,8 +74,7 @@ const CHAT_SEARCH_CONFIG = {
},
openai: {
endpoint: () => "https://api.openai.com/v1/chat/completions",
defaultModel: "gpt-4o-mini",
endpoint: () => searchEndpoint("openai"),
buildBody: (query, model) => {
const body = {
model,
@@ -105,8 +108,7 @@ const CHAT_SEARCH_CONFIG = {
},
xai: {
endpoint: () => "https://api.x.ai/v1/responses",
defaultModel: "grok-4.20-reasoning",
endpoint: () => searchEndpoint("xai"),
buildBody: (query, model) => ({
model,
input: [{ role: "user", content: query }],
@@ -145,8 +147,7 @@ const CHAT_SEARCH_CONFIG = {
},
kimi: {
endpoint: () => "https://api.moonshot.cn/v1/chat/completions",
defaultModel: "kimi-k2.5",
endpoint: () => searchEndpoint("kimi"),
buildBody: (query, model) => ({
model,
messages: [{ role: "user", content: query }],
@@ -195,8 +196,7 @@ const CHAT_SEARCH_CONFIG = {
},
minimax: {
endpoint: () => "https://api.minimaxi.com/v1/text/chatcompletion_v2",
defaultModel: "MiniMax-M2.7",
endpoint: () => searchEndpoint("minimax"),
buildBody: (query, model) => ({
model,
messages: [{ role: "user", content: query }],
@@ -254,8 +254,7 @@ const CHAT_SEARCH_CONFIG = {
},
perplexity: {
endpoint: () => "https://api.perplexity.ai/chat/completions",
defaultModel: "sonar",
endpoint: () => searchEndpoint("perplexity"),
buildBody: (query, model) => ({
model,
messages: [{ role: "user", content: query }]
@@ -324,7 +323,7 @@ export async function handleChatSearch({
Number.isFinite(maxResults) && maxResults > 0
? Math.floor(maxResults)
: DEFAULT_MAX_RESULTS;
const useModel = model || cfg.defaultModel;
const useModel = model || searchModel(provider);
const url = cfg.endpoint(useModel);
const body = cfg.buildBody(query, useModel);
const headers = cfg.buildHeaders(token);

View File

@@ -1,7 +1,6 @@
import { Buffer } from "node:buffer";
import { createErrorResult } from "../utils/error.js";
import { HTTP_STATUS } from "../config/runtimeConfig.js";
import { AI_PROVIDERS } from "../../src/shared/constants/providers.js";
// Build auth headers from sttConfig + token
function buildAuthHeaders(cfg, token) {
@@ -167,11 +166,11 @@ function jsonResponse(obj) {
* STT core handler — dispatch by sttConfig.format.
* @returns {Promise<{success, response, status?, error?}>}
*/
export async function handleSttCore({ provider, model, formData, credentials }) {
export async function handleSttCore({ provider, model, formData, credentials, sttConfig }) {
const file = formData.get("file");
if (!file) return createErrorResult(HTTP_STATUS.BAD_REQUEST, "Missing required field: file");
const cfg = AI_PROVIDERS[provider]?.sttConfig;
const cfg = sttConfig;
if (!cfg) return createErrorResult(HTTP_STATUS.BAD_REQUEST, `Provider '${provider}' does not support STT`);
const token = cfg.authType === "none" ? null : (credentials?.apiKey || credentials?.accessToken);

View File

@@ -1,9 +1,12 @@
// Gemini TTS — generateContent with AUDIO modality returns PCM L16, wrap as WAV
import { Buffer } from "node:buffer";
import { PROVIDER_MEDIA } from "../../providers/index.js";
const DEFAULT_MODEL = "gemini-2.5-flash-preview-tts";
const TTS_CFG = PROVIDER_MEDIA["gemini"]?.ttsConfig || {};
const TTS_BASE = TTS_CFG.baseUrl;
const KNOWN_MODELS = (TTS_CFG.models || []).map((m) => m.id);
const DEFAULT_MODEL = KNOWN_MODELS[0];
const DEFAULT_VOICE = "Kore";
const KNOWN_MODELS = ["gemini-2.5-flash-preview-tts", "gemini-2.5-pro-preview-tts"];
// Parse "model/voice" — if input doesn't match a known TTS model, treat it as voice with default model
function parseGeminiModelVoice(input) {
@@ -51,7 +54,7 @@ export default {
async synthesize(text, model, credentials, _responseFormat, opts = {}) {
if (!credentials?.apiKey) throw new Error("No Gemini API key configured");
const { modelId, voiceId } = parseGeminiModelVoice(model);
const url = `https://generativelanguage.googleapis.com/v1beta/models/${modelId}:generateContent?key=${credentials.apiKey}`;
const url = `${TTS_BASE}/${modelId}:generateContent?key=${credentials.apiKey}`;
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },

View File

@@ -33,8 +33,10 @@ export async function synthesizeViaConfig(provider, text, model, credentials) {
if (!handler) return null;
const apiKey = credentials?.apiKey;
if (cfg.authType !== "none" && !apiKey) throw new Error(`${provider} API key required`);
const defaultModel = cfg.models?.[0]?.id || "";
const { modelId, voiceId } = parseModelVoice(model, defaultModel, "", cfg.models || []);
const { PROVIDER_MODELS } = await import("open-sse/config/providerModels.js");
const ttsModels = (PROVIDER_MODELS[provider] || []).filter(m => (m.kind || m.type) === "tts");
const defaultModel = ttsModels[0]?.id || "";
const { modelId, voiceId } = parseModelVoice(model, defaultModel, "", ttsModels);
return handler({ baseUrl: cfg.baseUrl, apiKey, text, modelId, voiceId });
}

View File

@@ -1,11 +1,14 @@
// OpenAI TTS — model format: "tts-model/voice"
import { Buffer } from "node:buffer";
import { PROVIDER_MEDIA } from "../../providers/index.js";
const DEFAULT_TTS_MODEL = PROVIDER_MEDIA["openai"]?.ttsConfig?.defaultModel;
export default {
async synthesize(text, model, credentials) {
if (!credentials?.apiKey) throw new Error("No OpenAI API key configured");
let ttsModel = "gpt-4o-mini-tts";
let ttsModel = DEFAULT_TTS_MODEL;
let voice = "alloy";
if (model && model.includes("/")) {
const parts = model.split("/");

View File

@@ -1,10 +1,14 @@
// OpenRouter TTS — via chat completions + audio modality (SSE stream)
import { PROVIDER_MEDIA } from "../../providers/index.js";
const TTS_CFG = PROVIDER_MEDIA["openrouter"]?.ttsConfig || {};
export default {
async synthesize(text, model, credentials) {
if (!credentials?.apiKey) throw new Error("No OpenRouter API key configured");
// model format: "tts-model/voice" e.g. "openai/gpt-4o-mini-tts/alloy"
let ttsModel = "openai/gpt-4o-mini-tts";
let ttsModel = TTS_CFG.defaultModel;
let voice = "alloy";
if (model && model.includes("/")) {
const lastSlash = model.lastIndexOf("/");
@@ -20,13 +24,12 @@ export default {
voice = model;
}
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
const res = await fetch(TTS_CFG.baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${credentials.apiKey}`,
"HTTP-Referer": "https://endpoint-proxy.local",
"X-Title": "Endpoint Proxy",
...(TTS_CFG.headers || {}),
},
body: JSON.stringify({
model: ttsModel,

View File

@@ -30,9 +30,6 @@ export {
// Services
export {
detectFormat,
getProviderConfig,
buildProviderUrl,
buildProviderHeaders,
getTargetFormat
} from "./services/provider.js";

View File

@@ -0,0 +1,98 @@
/**
* REGISTRY ENTRY TEMPLATE — copy into registry/{id}.js when adding a new provider.
*
* NOT imported by registry/index.js (lives outside registry/, static-import list ignores it).
* Delete every block your provider does not need. Only `id` + `category` are required.
* Field contract: see schema.js `@typedef RegistryEntry`. Runtime builders: providers/index.js.
*
* Quick recipes:
* - Plain API-key LLM → id, alias, category:"apikey", display, transport{baseUrl}, models.
* - OAuth LLM (device/PKCE)→ add oauth{...}; clientId/tokenUrl auto-inject into transport.
* - Media-only (tts/stt/…) → drop `models`+chat baseUrl, fill media{serviceKinds, *Config}.
*/
// import { CLAUDE_API_HEADERS, GOOGLE_OAUTH_CLIENT, OPENAI_COMPAT_BASE } from "./shared.js";
export default {
// ── identity ────────────────────────────────────────────────────────────
id: "example", // REQUIRED. kebab-case, unique.
alias: "ex", // short key for PROVIDER_MODELS (defaults to id if omitted).
aliases: ["example-ai"], // optional extra lookup tokens.
uiAlias: "ex", // optional UI badge token.
category: "apikey", // REQUIRED. "apikey" | "oauth" | "freeTier" | ...
// ── auth hints (only when relevant) ──────────────────────────────────────
authType: "apikey", // "apikey" | "oauth".
hasOAuth: false, // true if an OAuth flow exists.
authModes: ["apikey"], // e.g. ["oauth","apikey"] when both supported.
// noAuth: true, // local/free providers needing no credential.
// ── UI display ───────────────────────────────────────────────────────────
display: {
name: "Example",
icon: "bolt", // material icon name OR textIcon fallback.
color: "#3B82F6",
textIcon: "EX",
website: "https://example.com",
notice: { apiKeyUrl: "https://example.com/keys" }, // or signupUrl.
// deprecated: true, deprecationNotice: "RISK_NOTICE",
// kindNotice: { image: "Requires paid plan." },
// mediaPriority: 1,
},
// ── transport (HTTP runtime) → PROVIDERS[id] ─────────────────────────────
// Defaults applied: format:"openai". Declare ONLY what differs.
transport: {
baseUrl: "https://api.example.com/v1/chat/completions",
format: "openai", // "openai" | "claude" | "gemini" | "openai-responses" | ...
// validateUrl: "https://api.example.com/v1/models",
// headers: { "User-Agent": "..." }, // static fingerprint (anti-ban) lives here.
// auth: { header: "x-api-key", scheme: "raw" },
// forceStream: true, urlSuffix: "?beta=true",
// quirks: { dropOutputConfig: true },
// retry: { 429: { attempts: 6 }, 503: { attempts: 3 } },
// usage: { url: "https://api.example.com/usage" }, // or { urls: [...] } for multi-call.
// modelsFetcher: { url: "https://api.example.com/models", type: "openai" }, // dynamic model list.
// regions: { sgp: "https://sgp...", cn: "https://cn..." }, defaultRegion: "sgp",
// NOTE: clientId/clientSecret/tokenUrl are injected from `oauth` — do NOT duplicate here.
},
// ── oauth flow → PROVIDER_OAUTH[id] (omit for pure API-key) ───────────────
// oauth: {
// clientId: "app_xxx",
// authorizeUrl: "https://auth.example.com/oauth/authorize", // PKCE/code flow.
// tokenUrl: "https://auth.example.com/oauth/token",
// deviceCodeUrl: "https://auth.example.com/device", // device-code flow.
// refreshUrl: "https://auth.example.com/oauth/token",
// scope: "openid profile offline_access", // or scopes: [...].
// codeChallengeMethod: "S256",
// redirectUri: "http://127.0.0.1:1455/auth/callback", fixedPort: 1455, callbackPath: "/auth/callback",
// extraParams: { foo: "bar" },
// refresh: { encoding: "form", scope: "openid offline_access" }, // "form" | "json".
// refreshLeadMs: 300000,
// userInfoUrl: "https://example.com/userinfo",
// },
// ── media (non-LLM services) → PROVIDER_MEDIA[id] ────────────────────────
// media: {
// serviceKinds: ["llm", "tts", "stt", "embedding", "image", "imageToText", "webSearch"],
// ttsConfig: { baseUrl: "...", authType: "apikey", authHeader: "bearer", format: "openai", defaultModel: "tts-1", models: [{ id: "tts-1", name: "TTS-1" }] },
// sttConfig: { baseUrl: "...", authType: "apikey", authHeader: "bearer", format: "openai", models: [{ id: "whisper-1", name: "Whisper" }] },
// embeddingConfig: { baseUrl: "...", authType: "apikey", authHeader: "bearer", models: [{ id: "emb-1", name: "Emb", dimensions: 1536 }] },
// imageConfig: { baseUrl: "https://api.example.com/v1/images/generations" },
// searchViaChat: { defaultModel: "ex-search", pricingUrl: "https://example.com/pricing" },
// // hiddenKinds: ["image"],
// },
// ── models (omit = no key; [] = explicit empty) ──────────────────────────
models: [
{ id: "example-large", name: "Example Large" },
// { id: "example-img", name: "Example Image", type: "image", capabilities: ["text2img"], params: ["size"] },
// { id: "example-emb", name: "Example Embed", type: "embedding" },
],
// ── optional flags ───────────────────────────────────────────────────────
// features: { usage: true },
// thinkingConfig: { options: ["auto", "none", "low", "high"], defaultMode: "auto" },
// passthroughModels: true,
};

View File

@@ -0,0 +1,269 @@
// 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
// thinking wire format (only meaningful when reasoning:true). null → derive from transport.format.
// enum: openai|claude-adaptive|claude-budget|gemini-level|gemini-budget|zai|qwen|deepseek|kimi|minimax|hunyuan|step
thinkingFormat: null,
thinkingCanDisable: true, // false → model cannot turn thinking off (clamp to min instead of disable)
thinkingRange: null, // { min, max } for budget formats; null = no clamp
// limits (tokens)
contextWindow: 200000,
maxOutput: 64000,
};
// User-added model metadata can carry dashboard service kinds instead of the
// runtime capability names used here. Map those typed model kinds into input /
// output capabilities so custom vision models are not treated as text-only.
const SERVICE_KIND_CAPABILITIES = {
imageToText: { vision: true },
image: { imageOutput: true },
stt: { audioInput: true },
tts: { audioOutput: true },
embedding: { tools: false },
};
export function capabilitiesFromServiceKind(kind) {
return SERVICE_KIND_CAPABILITIES[kind] || null;
}
/**
* 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 + adaptive thinking (override generic claude pattern)
"claude-opus-4.6": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 },
"claude-opus-4.7": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 },
"claude-opus-4-6": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 },
"claude-sonnet-4.6": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 64000 },
"claude-sonnet-4-6": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", 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, thinkingFormat: "zai", contextWindow: 128000 },
// Qwen plain coder/text (no vision) — registry "vision-model" / "coder-model" aliases
"vision-model": { vision: true, reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000 },
"coder-model": { reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000 },
};
/**
* Provider-specific capability overrides. Keyed by provider alias/id.
*/
export const PROVIDER_CAPABILITIES = {
// CodeBuddy.cn — authoritative per-model metadata from the gateway's model
// config (contextWindow=maxInputTokens, maxOutput=maxOutputTokens, vision=
// supportsImages). Every model reasons via OpenAI-style reasoning_effort
// (see registry thinkingFormat). `onlyReasoning` models can't turn thinking
// off → thinkingCanDisable:false (clamped to minimal instead of disabled).
"codebuddy-cn": {
"glm-5.2": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 48000 },
"glm-5.1": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 48000 },
"glm-5.0": { reasoning: true, thinkingFormat: "openai", contextWindow: 200000, maxOutput: 48000 },
"glm-5.0-turbo": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 48000 },
"glm-5v-turbo": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 38000 },
"glm-4.7": { reasoning: true, thinkingFormat: "openai", contextWindow: 200000, maxOutput: 48000 },
"minimax-m3": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 512000, maxOutput: 48000 },
"minimax-m2.7": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 48000 },
"kimi-k2.7": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 256000, maxOutput: 32000 },
"kimi-k2.6": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 256000, maxOutput: 32000 },
"kimi-k2.5": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 164000, maxOutput: 32000 },
"hy3-preview": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 192000, maxOutput: 64000 },
"deepseek-v4-pro": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 50000 },
"deepseek-v4-flash": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 50000 },
"deepseek-v3-2-volc": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 96000, maxOutput: 32000 },
},
};
/**
* 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.6+ = adaptive thinking; older/haiku = budget) ──────
{ pattern: "*claude*opus-4.6*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive" } },
{ pattern: "*claude*opus-4.7*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive" } },
{ pattern: "*claude*opus-4.8*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive" } },
{ pattern: "*claude*sonnet-4.6*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive" } },
{ pattern: "*claude*sonnet-4.7*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive" } },
{ pattern: "*claude*haiku*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget" } },
{ pattern: "*claude*opus*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget" } },
{ pattern: "*claude*sonnet*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget" } },
{ pattern: "*claude*fable*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget", contextWindow: 1000000, maxOutput: 128000 } },
{ pattern: "*claude*mythos*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget", contextWindow: 1000000, maxOutput: 128000 } },
{ pattern: "*claude-3*", caps: { vision: true } },
{ pattern: "*claude*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget" } },
// ── 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, thinkingFormat: "gemini-level", thinkingCanDisable: false, contextWindow: 1048576, maxOutput: 65535 } },
{ pattern: "*gemini-3*", caps: { vision: true, audioInput: true, videoInput: true, reasoning: true, search: true, thinkingFormat: "gemini-level", thinkingCanDisable: false, contextWindow: 1048576, maxOutput: 65536 } },
{ pattern: "*gemini-2.5*", caps: { vision: true, audioInput: true, videoInput: true, reasoning: true, search: true, thinkingFormat: "gemini-budget", thinkingRange: { min: 0, max: 24576 }, 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, thinkingFormat: "openai", contextWindow: 400000, maxOutput: 128000 } },
{ pattern: "*gpt-5*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "openai", 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, thinkingFormat: "openai", contextWindow: 128000 } },
// ── OpenAI o-series (reasoning, vision) ──────────────────────────
{ pattern: "*o1-mini*", caps: { reasoning: true, thinkingFormat: "openai", contextWindow: 128000 } },
{ pattern: "*o1*", caps: { vision: true, reasoning: true, thinkingFormat: "openai", contextWindow: 200000, maxOutput: 100000 } },
{ pattern: "*o3*", caps: { vision: true, reasoning: true, thinkingFormat: "openai", contextWindow: 200000, maxOutput: 100000 } },
{ pattern: "*o4*", caps: { vision: true, reasoning: true, thinkingFormat: "openai", contextWindow: 200000, maxOutput: 100000 } },
// ── Grok (vision + Live Search) ──────────────────────────────────
{ pattern: "*grok*image*", caps: { imageOutput: true } },
{ pattern: "*grok-code*", caps: { reasoning: true, thinkingFormat: "openai", contextWindow: 256000 } },
{ pattern: "*grok-4*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 256000 } },
{ pattern: "*grok-3*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 131072 } },
{ pattern: "*grok*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 256000 } },
// ── Qwen (enable_thinking + thinking_budget; QwQ = thinking-only) ─
{ pattern: "*qwen*vl*", caps: { vision: true, reasoning: true, thinkingFormat: "qwen", contextWindow: 262144 } },
{ pattern: "*qwen*max*", caps: { vision: true, reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000, maxOutput: 65536 } },
{ pattern: "*qwen*plus*", caps: { vision: true, reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000, maxOutput: 65536 } },
{ pattern: "*qwen*235b*", caps: { reasoning: true, thinkingFormat: "qwen", contextWindow: 262144 } },
{ pattern: "*qwen*coder*", caps: { reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000 } },
{ pattern: "*qwq*", caps: { reasoning: true, thinkingFormat: "qwen", thinkingCanDisable: false, contextWindow: 131072 } },
{ pattern: "*qwen*", caps: { reasoning: true, thinkingFormat: "qwen", contextWindow: 262144 } },
// ── Kimi (enabled→reasoning_effort; K2.7-code cannot disable) ─────
{ pattern: "*kimi*k2.7*code*", caps: { vision: true, reasoning: true, thinkingFormat: "kimi", thinkingCanDisable: false, contextWindow: 262144, maxOutput: 262144 } },
{ pattern: "*kimi*k2*", caps: { vision: true, reasoning: true, thinkingFormat: "kimi", contextWindow: 262144, maxOutput: 262144 } },
{ pattern: "*kimi*", caps: { reasoning: true, thinkingFormat: "kimi", contextWindow: 262144 } },
// ── GLM / Z.ai (thinking.enabled; disable via enable_thinking:false) ─
{ pattern: "*glm-5*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000, maxOutput: 128000 } },
{ pattern: "*glm-4.7*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000, maxOutput: 128000 } },
{ pattern: "*glm-4*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000 } },
{ pattern: "*glm*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000 } },
// ── DeepSeek (thinking.enabled + reasoning_effort; r1 = thinking-only) ─
{ pattern: "*deepseek-v4*", caps: { reasoning: true, thinkingFormat: "deepseek", contextWindow: 1000000, maxOutput: 384000 } },
{ pattern: "*reasoner*", caps: { reasoning: true, thinkingFormat: "deepseek", thinkingCanDisable: false, contextWindow: 128000 } },
{ pattern: "*deepseek-r*", caps: { reasoning: true, thinkingFormat: "deepseek", thinkingCanDisable: false, contextWindow: 128000 } },
{ pattern: "*deepseek-chat*", caps: { contextWindow: 128000 } },
{ pattern: "*deepseek*", caps: { reasoning: true, thinkingFormat: "deepseek", contextWindow: 128000 } },
// ── MiniMax (M3 = adaptive; M2.x cannot disable) ─────────────────
{ pattern: "*minimax*image*", caps: { imageOutput: true } },
{ pattern: "*minimax-m3*", caps: { vision: true, reasoning: true, thinkingFormat: "minimax", contextWindow: 1048576, maxOutput: 512000 } },
{ pattern: "*minimax-m2.7*", caps: { reasoning: true, thinkingFormat: "minimax", thinkingCanDisable: false, contextWindow: 204800, maxOutput: 131072 } },
{ pattern: "*minimax*", caps: { reasoning: true, thinkingFormat: "minimax", thinkingCanDisable: false, 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, thinkingFormat: "hunyuan", contextWindow: 262144, maxOutput: 262144 } },
{ pattern: "hy3*", caps: { reasoning: true, thinkingFormat: "hunyuan", contextWindow: 262144, maxOutput: 262144 } },
{ pattern: "*step-*", caps: { reasoning: true, thinkingFormat: "step", contextWindow: 128000 } },
{ pattern: "*nemotron*", caps: { reasoning: true, contextWindow: 128000 } },
{ pattern: "*ling-*", caps: { reasoning: true, contextWindow: 128000 } },
];
/**
* Resolve capabilities for a model using the 4-step fallback chain,
* merged over DEFAULT_CAPABILITIES so the result is always complete.
*
* @param {string} provider
* @param {string} model
* @returns {object} full capabilities object
*/
export function getCapabilitiesForModel(provider, model) {
if (!model) return { ...DEFAULT_CAPABILITIES };
// 1. Provider-specific override
if (provider && PROVIDER_CAPABILITIES[provider]?.[model]) {
return { ...DEFAULT_CAPABILITIES, ...PROVIDER_CAPABILITIES[provider][model] };
}
// 2. Canonical exact (strip vendor prefix: "anthropic/claude-opus-4.7" -> "claude-opus-4.7")
const baseModel = model.includes("/") ? model.split("/").pop() : model;
if (MODEL_CAPABILITIES[baseModel]) return { ...DEFAULT_CAPABILITIES, ...MODEL_CAPABILITIES[baseModel] };
if (MODEL_CAPABILITIES[model]) return { ...DEFAULT_CAPABILITIES, ...MODEL_CAPABILITIES[model] };
// 3. Pattern match (first match wins)
for (const { pattern, caps } of PATTERN_CAPABILITIES) {
if (matchPattern(pattern, baseModel) || matchPattern(pattern, model)) {
return { ...DEFAULT_CAPABILITIES, ...caps };
}
}
// 4. Floor
return { ...DEFAULT_CAPABILITIES };
}

View File

@@ -0,0 +1,51 @@
// Single source: build PROVIDERS + PROVIDER_MODELS from registry/{id}.js (transport + models co-located).
import REGISTRY from "./registry/index.js";
import { PROVIDER_DEFAULTS } from "./schema.js";
import { normalizeModel } from "./models/schema.js";
import { buildTtsProviderModels } from "../config/ttsModels.js";
// oauth block is canonical for these fields; inject into transport so executors reading
// this.config.{clientId,clientSecret,tokenUrl} keep working without duplicating in transport
const OAUTH_INJECT_FIELDS = ["clientId", "clientSecret", "tokenUrl"];
// transport: re-apply shared default (format:"openai") + inject oauth-canonical fields
function buildTransport(transport, oauth) {
const t = { ...transport };
if (!t.format) t.format = PROVIDER_DEFAULTS.format;
if (oauth) {
for (const f of OAUTH_INJECT_FIELDS) {
if (t[f] === undefined && oauth[f] !== undefined) t[f] = oauth[f];
}
}
return t;
}
const MEDIA_KEYS = new Set([
"serviceKinds", "ttsConfig", "sttConfig", "embeddingConfig",
"imageConfig", "imageToTextConfig", "videoConfig", "musicConfig",
"searchViaChat", "searchConfig", "fetchConfig",
"modelsFetcher", "mediaPriority", "hiddenKinds",
]);
export const PROVIDERS = {};
export const PROVIDER_MODELS = {};
export const PROVIDER_OAUTH = {};
export const PROVIDER_MEDIA = {};
for (const entry of REGISTRY) {
if (entry.transport) {
PROVIDERS[entry.id] = buildTransport(entry.transport, entry.oauth);
if (entry.transports) PROVIDERS[entry.id].transports = entry.transports;
}
if (entry.models !== undefined) PROVIDER_MODELS[entry.alias || entry.id] = entry.models.map(normalizeModel);
if (entry.oauth) PROVIDER_OAUTH[entry.id] = entry.oauth;
// Build PROVIDER_MEDIA from top-level fields (post-migration) + legacy entry.media
const mediaFields = {};
for (const k of MEDIA_KEYS) {
if (entry[k] !== undefined) mediaFields[k] = entry[k];
}
if (entry.media) Object.assign(mediaFields, entry.media);
if (Object.keys(mediaFields).length) PROVIDER_MEDIA[entry.id] = mediaFields;
}
// TTS model/voice tables keyed by special names (openai-tts-models, ...), not provider ids
Object.assign(PROVIDER_MODELS, buildTtsProviderModels());

View File

@@ -0,0 +1,20 @@
// Codex auto-generates a "-review" variant for each llm model (review quota family)
export const CODEX_REVIEW_SUFFIX = "-review";
export function withCodexReviewModels(models) {
return models.flatMap((model) => {
if ((model.kind || model.type || "llm") !== "llm" || model.id.endsWith(CODEX_REVIEW_SUFFIX)) {
return [model];
}
return [
model,
{
...model,
id: `${model.id}${CODEX_REVIEW_SUFFIX}`,
name: `${model.name} Review`,
upstreamModelId: model.upstreamModelId || model.id,
quotaFamily: "review"
}
];
});
}

View File

@@ -0,0 +1,33 @@
// Derive a display name from a model id when the entry omits `name` (mirrors PATTERN_PRICING).
// Provider entries that ship their own `name` always win; this is only a fallback for terse entries.
// Capitalize a hyphen/space separated token group: "coder-plus" → "Coder Plus".
function titleCase(s) {
return s
.split(/[-_\s]+/)
.filter(Boolean)
.map((w) => (/^\d/.test(w) ? w : w.charAt(0).toUpperCase() + w.slice(1)))
.join(" ");
}
// Ordered: first match wins. Keep specific patterns above generic ones.
export const NAME_PATTERNS = [
[/^kimi-k(\d+(?:\.\d+)?)(-thinking)?$/i, (m) => `Kimi K${m[1]}${m[2] ? " Thinking" : ""}`],
[/^glm-(\d+(?:\.\d+)?)(v)?$/i, (m) => `GLM ${m[1]}${m[2] ? "V (Vision)" : ""}`],
[/^minimax-m(\d+(?:\.\d+)?)$/i, (m) => `MiniMax M${m[1]}`],
[/^gpt-(.+)$/i, (m) => `GPT ${titleCase(m[1])}`],
[/^gemini-(.+)$/i, (m) => `Gemini ${titleCase(m[1])}`],
[/^grok-(.+)$/i, (m) => `Grok ${titleCase(m[1])}`],
[/^deepseek-(.+)$/i, (m) => `DeepSeek ${titleCase(m[1])}`],
[/^qwen([\d.]+.*)$/i, (m) => `Qwen ${titleCase(m[1])}`],
];
// id → display name (regex fallback → id verbatim)
export function deriveModelName(id) {
if (typeof id !== "string") return id;
for (const [re, fn] of NAME_PATTERNS) {
const m = id.match(re);
if (m) return fn(m);
}
return id;
}

View File

@@ -0,0 +1,31 @@
import { deriveModelName } from "./namePatterns.js";
// Model defaults centralized (was scattered as `m.kind || "llm"`, `quotaFamily || "normal"`, etc.)
export const MODEL_DEFAULTS = {
kind: "llm",
quotaFamily: "normal",
strip: [],
targetFormat: null
};
// Normalize a registry model entry: accept terse "id" string, fill name via regex when omitted.
// Override always wins (raw spread last); name falls back to regex → id.
export function normalizeModel(raw) {
const model = typeof raw === "string" ? { id: raw } : raw;
if (model.name !== undefined) return model;
return { ...model, name: deriveModelName(model.id) };
}
// Resolve model kind with default (accepts legacy `type` field)
export function modelKind(model) {
return model?.kind || model?.type || MODEL_DEFAULTS.kind;
}
export function modelQuotaFamily(model) {
return model?.quotaFamily || MODEL_DEFAULTS.quotaFamily;
}
export function modelStrip(model) {
return model?.strip || [];
}
export function modelTargetFormat(model) {
return model?.targetFormat || MODEL_DEFAULTS.targetFormat;
}

View File

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

View File

@@ -0,0 +1,29 @@
export default {
id: "alicode-intl",
priority: 10,
alias: "alicode-intl",
display: {
name: "Alibaba Intl",
icon: "cloud",
color: "#FF6A00",
textIcon: "ALi",
website: "https://modelstudio.console.alibabacloud.com",
notice: {
apiKeyUrl: "https://modelstudio.console.alibabacloud.com/?apiKey=1",
},
},
category: "apikey",
transport: {
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1/chat/completions",
headers: {},
},
models: [
{ id: "qwen3.5-plus", name: "Qwen3.5 Plus" },
{ id: "kimi-k2.5", name: "Kimi K2.5" },
{ id: "glm-5", name: "GLM 5" },
{ id: "MiniMax-M2.5", name: "MiniMax M2.5" },
{ id: "qwen3-coder-next", name: "Qwen3 Coder Next" },
{ id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" },
{ id: "glm-4.7", name: "GLM 4.7" },
],
};

View File

@@ -0,0 +1,30 @@
export default {
id: "alicode",
priority: 20,
alias: "alicode",
display: {
name: "Alibaba",
icon: "cloud",
color: "#FF6A00",
textIcon: "ALi",
website: "https://bailian.console.aliyun.com",
notice: {
apiKeyUrl: "https://bailian.console.aliyun.com/?apiKey=1",
},
},
category: "apikey",
transport: {
baseUrl: "https://coding.dashscope.aliyuncs.com/v1/chat/completions",
headers: {},
},
models: [
{ id: "qwen3.5-plus", name: "Qwen3.5 Plus" },
{ id: "kimi-k2.5", name: "Kimi K2.5" },
{ id: "glm-5", name: "GLM 5" },
{ id: "MiniMax-M2.5", name: "MiniMax M2.5" },
{ id: "qwen3-max-2026-01-23", name: "Qwen3 Max" },
{ id: "qwen3-coder-next", name: "Qwen3 Coder Next" },
{ id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" },
{ id: "glm-4.7", name: "GLM 4.7" },
],
};

View File

@@ -0,0 +1,32 @@
import { CLAUDE_API_HEADERS } from "../shared.js";
export default {
id: "anthropic",
priority: 30,
alias: "anthropic",
display: {
name: "Anthropic",
icon: "smart_toy",
color: "#D97757",
textIcon: "AN",
website: "https://console.anthropic.com",
notice: {
apiKeyUrl: "https://console.anthropic.com/settings/keys",
},
},
category: "apikey",
transport: {
baseUrl: "https://api.anthropic.com/v1/messages",
format: "claude",
headers: {
"Anthropic-Version": "2023-06-01",
"Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14",
},
},
models: [
{ id: "claude-sonnet-4-20250514", name: "Claude Sonnet 4" },
{ id: "claude-opus-4-20250514", name: "Claude Opus 4" },
{ id: "claude-3-5-sonnet-20241022", name: "Claude 3.5 Sonnet" },
],
serviceKinds: ["llm","imageToText"],
};

View File

@@ -0,0 +1,82 @@
import { platform, arch } from "os";
import { ANTIGRAVITY_OAUTH_CLIENT } from "../shared.js";
export default {
id: "antigravity",
priority: 20,
alias: "ag",
uiAlias: "ag",
display: {
name: "Antigravity",
icon: "rocket_launch",
color: "#F59E0B",
website: "https://antigravity.google",
notice: {
signupUrl: "https://antigravity.google",
},
deprecated: true,
deprecationNotice: "RISK_NOTICE",
},
category: "oauth",
serviceKinds: ["llm", "image"],
transport: {
baseUrls: [
"https://daily-cloudcode-pa.googleapis.com",
"https://daily-cloudcode-pa.sandbox.googleapis.com",
],
format: "antigravity",
headers: {
"User-Agent": "antigravity/1.107.0 darwin/arm64",
},
retry: {
"429": {
attempts: 3,
},
"503": {
attempts: 3,
},
},
usage: {
quotaApiUrl: "https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels",
loadProjectApiUrl: "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
tokenUrl: "https://oauth2.googleapis.com/token",
},
clientId: "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com",
clientSecret: "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",
},
models: [
{ id: "gemini-3-flash-agent", name: "Gemini 3.5 Flash (High)" },
{ id: "gemini-3.5-flash-low", name: "Gemini 3.5 Flash (Medium)" },
{ id: "gemini-3.5-flash-extra-low", name: "Gemini 3.5 Flash (Low)" },
{ id: "gemini-pro-agent", name: "Gemini 3.1 Pro (High)" },
{ id: "gemini-3.1-pro-low", name: "Gemini 3.1 Pro (Low)" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 (Thinking)" },
{ id: "claude-opus-4-6-thinking", name: "Claude Opus 4.6 (Thinking)" },
{ id: "gpt-oss-120b-medium", name: "GPT-OSS 120B (Medium)" },
{ id: "gemini-3-flash", name: "Gemini 3 Flash", thinking: false },
// Image generation models
{ id: "gemini-3.1-flash-image", name: "Gemini 3.1 Flash (Image)", kind: "image", imageGen: true, capabilities: ["textToImage"] },
],
oauth: {
authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
tokenUrl: "https://oauth2.googleapis.com/token",
userInfoUrl: "https://www.googleapis.com/oauth2/v1/userinfo",
scopes: [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/cclog",
"https://www.googleapis.com/auth/experimentsandconfigs",
],
apiEndpoint: "https://cloudcode-pa.googleapis.com",
apiVersion: "v1internal",
loadCodeAssistEndpoint: "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
onboardUserEndpoint: "https://cloudcode-pa.googleapis.com/v1internal:onboardUser",
loadCodeAssistUserAgent: "google-api-nodejs-client/9.15.1",
loadCodeAssistApiClient: "google-cloud-sdk vscode_cloudshelleditor/0.1",
refreshLeadMs: 300000,
},
features: {
usage: true,
},
};

View File

@@ -0,0 +1,38 @@
export default {
id: "assemblyai",
priority: 30,
alias: "assemblyai",
aliases: [
"aai",
],
uiAlias: "aai",
display: {
name: "AssemblyAI",
icon: "record_voice_over",
color: "#0062FF",
textIcon: "AA",
website: "https://assemblyai.com",
notice: {
apiKeyUrl: "https://www.assemblyai.com/app/api-keys",
},
},
category: "apikey",
authType: "apikey",
transport: {
baseUrl: "https://api.assemblyai.com/v1/audio/transcriptions",
validateUrl: "https://api.assemblyai.com/v1/account",
},
models: [
{ id: "universal-3-pro", name: "Universal 3 Pro", params: ["language"], kind: "stt" },
{ id: "universal-2", name: "Universal 2", params: ["language"], kind: "stt" },
{ id: "best", name: "Best (Nano + Universal)", kind: "stt" },
{ id: "nano", name: "Nano (Fast)", kind: "stt" },
],
serviceKinds: ["stt"],
sttConfig: {
baseUrl: "https://api.assemblyai.com/v2/transcript",
authType: "apikey",
authHeader: "authorization",
format: "assemblyai",
},
};

View File

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

View File

@@ -0,0 +1,21 @@
export default {
id: "azure",
priority: 40,
alias: "azure",
display: {
name: "Azure OpenAI",
icon: "cloud",
color: "#0078D4",
textIcon: "AZ",
website: "https://azure.microsoft.com/en-us/products/ai-services/openai-service",
notice: {
apiKeyUrl: "https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/OpenAI",
},
},
category: "apikey",
hasProviderSpecificData: true,
transport: {
baseUrl: "",
headers: {},
},
};

View File

@@ -0,0 +1,32 @@
export default {
id: "black-forest-labs",
priority: 50,
alias: "black-forest-labs",
aliases: [
"bfl",
],
uiAlias: "bfl",
display: {
name: "Black Forest Labs",
icon: "image",
color: "#111827",
textIcon: "BF",
website: "https://blackforestlabs.ai",
notice: {
apiKeyUrl: "https://api.bfl.ai",
},
},
category: "apikey",
authType: "apikey",
transport: null,
models: [
{ id: "flux-pro-1.1", name: "FLUX Pro 1.1", params: ["n","size"], kind: "image" },
{ id: "flux-pro-1.1-ultra", name: "FLUX Pro 1.1 Ultra", params: ["size"], kind: "image" },
{ id: "flux-pro", name: "FLUX Pro", params: ["n","size"], kind: "image" },
{ id: "flux-dev", name: "FLUX Dev", params: ["n","size"], kind: "image" },
{ id: "flux-kontext-pro", name: "FLUX Kontext Pro (Edit)", params: ["size"], capabilities: ["edit"], kind: "image" },
{ id: "flux-kontext-max", name: "FLUX Kontext Max (Edit)", params: ["size"], capabilities: ["edit"], kind: "image" },
],
serviceKinds: ["image"],
imageConfig: { baseUrl: "https://api.bfl.ai/v1" },
};

View File

@@ -0,0 +1,43 @@
export default {
id: "blackbox",
priority: 50,
alias: "blackbox",
aliases: [
"bb",
],
uiAlias: "bb",
display: {
name: "Blackbox AI",
icon: "smart_toy",
color: "#5B5FEF",
textIcon: "BB",
website: "https://blackbox.ai",
notice: {
apiKeyUrl: "https://www.blackbox.ai/api-management",
},
},
category: "apikey",
transport: {
baseUrl: "https://api.blackbox.ai/chat/completions",
thinkingFormat: "openai",
},
models: [
{ id: "gpt-4o", name: "GPT-4o" },
{ id: "gpt-4o-mini", name: "GPT-4o mini" },
{ id: "claude-sonnet-4.6", name: "Claude Sonnet 4.6" },
{ id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5" },
{ id: "claude-opus-4.6", name: "Claude Opus 4.6" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 (Legacy)" },
{ id: "claude-opus-4-6", name: "Claude Opus 4.6 (Legacy)" },
{ id: "deepseek-chat", name: "DeepSeek Chat" },
{ id: "deepseek-v3-671b", name: "DeepSeek V3 671B" },
{ id: "deepseek-r1", name: "DeepSeek R1" },
{ id: "o1", name: "OpenAI o1" },
{ id: "o3-mini", name: "OpenAI o3-mini" },
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash" },
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" },
{ id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" },
{ id: "qwen3-max", name: "Qwen3 Max" },
{ id: "qwen3-vl-plus", name: "Qwen3 VL Plus" },
],
};

View File

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

View File

@@ -0,0 +1,35 @@
export default {
id: "byteplus",
priority: 70,
alias: "byteplus",
aliases: [
"bpm",
],
uiAlias: "bpm",
display: {
name: "BytePlus ModelArk",
icon: "cloud",
color: "#2563EB",
textIcon: "BP",
website: "https://console.byteplus.com/ark",
notice: {
text: "Free credits for new accounts. Access to Seed 2.0, Kimi K2 Thinking, GLM 4.7, GPT-OSS-120B models.",
apiKeyUrl: "https://console.byteplus.com/ark/region:ark+ap-southeast-1/apiKey",
},
},
category: "freeTier",
transport: {
baseUrl: "https://ark.ap-southeast.bytepluses.com/api/coding/v3/chat/completions",
headers: {},
},
models: [
{ id: "seed-2-0-pro-260328", name: "Seed 2.0 Pro" },
{ id: "seed-2-0-code-preview-260328", name: "Seed 2.0 Code Preview" },
{ id: "seed-2-0-mini-260215", name: "Seed 2.0 Mini" },
{ id: "seed-2-0-lite-260228", name: "Seed 2.0 Lite" },
{ id: "kimi-k2-thinking-251104", name: "Kimi K2 Thinking" },
{ id: "glm-4-7-251222", name: "GLM 4.7" },
{ id: "gpt-oss-120b-250805", name: "GPT-OSS-120B" },
],
serviceKinds: ["llm"],
};

View File

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

View File

@@ -0,0 +1,31 @@
export default {
id: "cerebras",
priority: 60,
alias: "cerebras",
display: {
name: "Cerebras",
icon: "memory",
color: "#FF4F00",
textIcon: "CB",
website: "https://www.cerebras.ai",
notice: {
apiKeyUrl: "https://cloud.cerebras.ai/platform",
},
},
category: "apikey",
transport: {
baseUrl: "https://api.cerebras.ai/v1/chat/completions",
validateUrl: "https://api.cerebras.ai/v1/models",
quirks: {
dropClientMetadata: true,
},
},
models: [
{ id: "gpt-oss-120b", name: "GPT OSS 120B" },
{ id: "zai-glm-4.7", name: "ZAI GLM 4.7" },
{ id: "llama-3.3-70b", name: "Llama 3.3 70B" },
{ id: "llama-4-scout-17b-16e-instruct", name: "Llama 4 Scout" },
{ id: "qwen-3-235b-a22b-instruct-2507", name: "Qwen3 235B A22B" },
{ id: "qwen-3-32b", name: "Qwen3 32B" },
],
};

View File

@@ -0,0 +1,24 @@
export default {
id: "chutes",
priority: 70,
alias: "chutes",
aliases: [
"ch",
],
uiAlias: "ch",
display: {
name: "Chutes AI",
icon: "water_drop",
color: "#ffffffff",
textIcon: "CH",
website: "https://chutes.ai",
notice: {
apiKeyUrl: "https://chutes.ai/app/api",
},
},
category: "apikey",
transport: {
baseUrl: "https://llm.chutes.ai/v1/chat/completions",
validateUrl: "https://llm.chutes.ai/v1/models",
},
};

View File

@@ -0,0 +1,89 @@
import { CLAUDE_CLI_SPOOF_HEADERS } from "../shared.js";
export default {
id: "claude",
priority: 10,
alias: "cc",
uiAlias: "cc",
display: {
name: "Claude Code",
icon: "smart_toy",
color: "#D97757",
website: "https://claude.ai",
notice: {
signupUrl: "https://claude.ai",
},
deprecated: true,
deprecationNotice: "RISK_NOTICE",
},
category: "oauth",
transport: {
baseUrl: "https://api.anthropic.com/v1/messages",
format: "claude",
urlSuffix: "?beta=true",
headers: {
"Anthropic-Version": "2023-06-01",
"Anthropic-Beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28",
"Anthropic-Dangerous-Direct-Browser-Access": "true",
"User-Agent": "claude-cli/2.1.92 (external, sdk-cli)",
"X-App": "cli",
"X-Stainless-Helper-Method": "stream",
"X-Stainless-Retry-Count": "0",
"X-Stainless-Runtime-Version": "v24.14.0",
"X-Stainless-Package-Version": "0.80.0",
"X-Stainless-Runtime": "node",
"X-Stainless-Lang": "js",
"X-Stainless-Arch": "arm64",
"X-Stainless-Os": "MacOS",
"X-Stainless-Timeout": "600",
},
quirks: {
cloakToolsOnOAuth: true,
},
auth: {
apiKey: {
header: "x-api-key",
scheme: "raw",
},
oauth: {
header: "Authorization",
scheme: "bearer",
},
hooks: [
"claudeOverlay",
],
},
usage: {
oauthUrl: "https://api.anthropic.com/api/oauth/usage",
orgUrl: "https://api.anthropic.com/v1/organizations/{org_id}/usage",
settingsUrl: "https://api.anthropic.com/v1/settings",
},
},
models: [
{ id: "claude-opus-4-8", name: "Claude Opus 4.8" },
{ id: "claude-opus-4-7", name: "Claude Opus 4.7" },
{ id: "claude-opus-4-6", name: "Claude Opus 4.6" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ id: "claude-opus-4-5-20251101", name: "Claude 4.5 Opus" },
{ id: "claude-sonnet-4-5-20250929", name: "Claude 4.5 Sonnet" },
{ id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" },
],
oauth: {
clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
authorizeUrl: "https://claude.ai/oauth/authorize",
tokenUrl: "https://api.anthropic.com/v1/oauth/token",
scopes: [
"org:create_api_key",
"user:profile",
"user:inference",
],
codeChallengeMethod: "S256",
refreshLeadMs: 14400000,
refresh: {
encoding: "json",
},
},
features: {
usage: true,
},
};

View File

@@ -0,0 +1,51 @@
export default {
id: "cline",
priority: 80,
alias: "cl",
uiAlias: "cl",
display: {
name: "Cline",
icon: "smart_toy",
color: "#5B9BD5",
textIcon: "CL",
website: "https://cline.bot",
notice: {
signupUrl: "https://cline.bot",
},
},
category: "oauth",
transport: {
baseUrl: "https://api.cline.bot/api/v1/chat/completions",
headers: {
"HTTP-Referer": "https://cline.bot",
"X-Title": "Cline",
},
tokenUrl: "https://api.cline.bot/api/v1/auth/token",
refreshUrl: "https://api.cline.bot/api/v1/auth/refresh",
auth: {
combined: true,
header: "Authorization",
scheme: "bearer",
hooks: [
"clineHeaders",
],
},
},
models: [
{ id: "anthropic/claude-opus-4.7", name: "Claude Opus 4.7" },
{ id: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6" },
{ id: "anthropic/claude-opus-4.6", name: "Claude Opus 4.6" },
{ id: "openai/gpt-5.3-codex", name: "GPT-5.3 Codex" },
{ id: "openai/gpt-5.4", name: "GPT-5.4" },
{ id: "google/gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview" },
{ id: "google/gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview" },
{ id: "kwaipilot/kat-coder-pro", name: "KAT Coder Pro" },
],
oauth: {
appBaseUrl: "https://app.cline.bot",
apiBaseUrl: "https://api.cline.bot",
authorizeUrl: "https://api.cline.bot/api/v1/auth/authorize",
tokenExchangeUrl: "https://api.cline.bot/api/v1/auth/token",
refreshUrl: "https://api.cline.bot/api/v1/auth/refresh",
},
};

View File

@@ -0,0 +1,55 @@
export default {
id: "cloudflare-ai",
priority: 60,
hasFree: true,
alias: "cloudflare-ai",
aliases: [
"cf",
],
uiAlias: "cf",
display: {
name: "Cloudflare",
icon: "cloud",
color: "#F38020",
textIcon: "CF",
website: "https://developers.cloudflare.com/workers-ai/",
notice: {
text: "Workers AI free tier. Requires a Cloudflare API token and Account ID.",
apiKeyUrl: "https://dash.cloudflare.com/profile/api-tokens",
},
},
category: "freeTier",
hasProviderSpecificData: true,
transport: {
baseUrl: "https://api.cloudflare.com/client/v4/accounts/{accountId}/ai/v1/chat/completions",
thinkingFormat: "openai",
},
models: [
{ id: "@cf/meta/llama-3.2-1b-instruct", name: "Llama 3.2 1B Instruct" },
{ id: "@cf/meta/llama-3.2-3b-instruct", name: "Llama 3.2 3B Instruct" },
{ id: "@cf/meta/llama-3.1-8b-instruct-fp8-fast", name: "Llama 3.1 8B Instruct FP8 Fast" },
{ id: "@cf/meta/llama-3.1-8b-instruct-awq", name: "Llama 3.1 8B Instruct AWQ" },
{ id: "@cf/mistralai/mistral-small-3.1-24b-instruct", name: "Mistral Small 3.1 24B Instruct" },
{ id: "@cf/meta/llama-3.1-70b-instruct-fp8-fast", name: "Llama 3.1 70B Instruct FP8 Fast" },
{ id: "@cf/meta/llama-3.3-70b-instruct-fp8-fast", name: "Llama 3.3 70B Instruct FP8 Fast" },
{ id: "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b", name: "DeepSeek R1 Distill Qwen 32B" },
{ id: "@cf/moonshotai/kimi-k2.5", name: "Kimi K2.5" },
{ id: "@cf/moonshotai/kimi-k2.6", name: "Kimi K2.6" },
{ id: "@cf/zai-org/glm-4.7-flash", name: "GLM 4.7 Flash" },
{ id: "@cf/qwen/qwq-32b", name: "QwQ 32B" },
{ id: "@cf/qwen/qwen2.5-coder-32b-instruct", name: "Qwen 2.5 Coder 32B Instruct" },
{ id: "@cf/black-forest-labs/flux-2-klein-9b", name: "FLUX.2 Klein 9B", params: ["size"], kind: "image" },
{ id: "@cf/black-forest-labs/flux-2-klein-4b", name: "FLUX.2 Klein 4B", params: ["size"], kind: "image" },
{ id: "@cf/black-forest-labs/flux-2-dev", name: "FLUX.2 Dev", params: ["size"], kind: "image" },
{ id: "@cf/leonardo/lucid-origin", name: "Lucid Origin", params: ["size"], kind: "image" },
{ id: "@cf/leonardo/phoenix-1.0", name: "Phoenix 1.0", params: ["size"], kind: "image" },
{ id: "@cf/black-forest-labs/flux-1-schnell", name: "FLUX.1 Schnell", params: ["size"], kind: "image" },
{ id: "@cf/bytedance/stable-diffusion-xl-lightning", name: "SDXL Lightning", params: ["size"], kind: "image" },
{ id: "@cf/lykon/dreamshaper-8-lcm", name: "DreamShaper 8 LCM", params: ["size"], kind: "image" },
{ id: "@cf/runwayml/stable-diffusion-v1-5-img2img", name: "Stable Diffusion v1.5 Img2Img", params: ["size"], capabilities: ["edit"], kind: "image" },
{ id: "@cf/runwayml/stable-diffusion-v1-5-inpainting", name: "Stable Diffusion v1.5 Inpainting", params: ["size"], capabilities: ["edit","mask"], kind: "image" },
{ id: "@cf/stabilityai/stable-diffusion-xl-base-1.0", name: "SDXL Base 1.0", params: ["size"], kind: "image" },
],
serviceKinds: ["llm","image"],
imageConfig: { baseUrl: "https://api.cloudflare.com/client/v4/accounts" },
};

View File

@@ -0,0 +1,77 @@
export default {
id: "codebuddy-cn",
// Short model prefix (cbcn/glm-5.2). "cbcn" = CodeBuddy CN; reserve "cbai"
// for a future codebuddy-ai (intl) provider. The full id still resolves.
alias: "cbcn",
uiAlias: "cbcn",
hidden: false,
priority: 90,
display: {
name: "CodeBuddy CN",
icon: "smart_toy",
color: "#006EFF",
website: "https://copilot.tencent.com",
notice: {
signupUrl: "https://copilot.tencent.com",
},
},
category: "oauth",
authModes: ["oauth", "apikey"],
hasOAuth: true,
transport: {
baseUrl: "https://copilot.tencent.com/v2/chat/completions",
forceStream: true,
// CodeBuddy is a unified OpenAI-compatible gateway: every model (GLM, Kimi,
// MiniMax, DeepSeek, Hunyuan) takes reasoning via OpenAI-style reasoning_effort,
// not its vendor-native thinking shape. Force the openai thinking format.
thinkingFormat: "openai",
headers: {
"User-Agent": "CLI/2.108.1 CodeBuddy/2.108.1",
"X-Product": "SaaS",
"X-IDE-Type": "CLI",
"X-IDE-Name": "CLI",
"x-requested-with": "XMLHttpRequest",
"x-codebuddy-request": "1",
},
auth: {
combined: true,
header: "Authorization",
scheme: "bearer",
},
// Quota endpoint differs from the chat gateway: POST returns nested Tencent
// billing payload (data.Response.Data.Accounts[]). See services/usage/codebuddy-cn.js.
usage: {
url: "https://copilot.tencent.com/v2/billing/meter/get-user-resource",
},
},
models: [
{ id: "glm-5.2", name: "GLM-5.2" },
{ id: "glm-5.1", name: "GLM-5.1" },
{ id: "glm-5.0", name: "GLM-5.0" },
{ id: "glm-5.0-turbo", name: "GLM-5.0-Turbo" },
{ id: "glm-5v-turbo", name: "GLM-5v-Turbo" },
{ id: "glm-4.7", name: "GLM-4.7" },
{ id: "minimax-m3", name: "MiniMax-M3" },
{ id: "minimax-m2.7", name: "MiniMax-M2.7" },
{ id: "kimi-k2.7", name: "Kimi-K2.7-Code" },
{ id: "kimi-k2.6", name: "Kimi-K2.6" },
{ id: "kimi-k2.5", name: "Kimi-K2.5" },
{ id: "hy3-preview", name: "Hy3 Preview" },
{ id: "deepseek-v4-pro", name: "DeepSeek-V4-Pro" },
{ id: "deepseek-v4-flash", name: "DeepSeek-V4-Flash" },
{ id: "deepseek-v3-2-volc", name: "DeepSeek-V3.2" },
],
oauth: {
baseUrl: "https://copilot.tencent.com",
stateUrl: "https://copilot.tencent.com/v2/plugin/auth/state",
tokenUrl: "https://copilot.tencent.com/v2/plugin/auth/token",
refreshUrl: "https://copilot.tencent.com/v2/plugin/auth/token/refresh",
userAgent: "CLI/2.63.2 CodeBuddy/2.63.2",
platform: "CLI",
pollInterval: 5000,
},
features: {
usage: true,
usageApikey: true,
},
};

View File

@@ -0,0 +1,94 @@
import { withCodexReviewModels } from "../models/helpers.js";
export default {
id: "codex",
priority: 30,
alias: "cx",
uiAlias: "cx",
display: {
name: "OpenAI Codex",
icon: "code",
color: "#3B82F6",
website: "https://chatgpt.com/codex",
notice: {
signupUrl: "https://chatgpt.com/codex",
},
deprecated: true,
deprecationNotice: "RISK_NOTICE",
kindNotice: {
image: "Requires a ChatGPT Plus (or higher) account. Free accounts are not supported for image generation.",
},
},
category: "oauth",
thinkingConfig: {
options: [
"auto",
"none",
"low",
"medium",
"high",
],
defaultMode: "auto",
},
transport: {
baseUrl: "https://chatgpt.com/backend-api/codex/responses",
format: "openai-responses",
forceStream: true,
headers: {
originator: "codex_cli_rs",
"User-Agent": "codex_cli_rs/0.136.0",
},
usage: {
url: "https://chatgpt.com/backend-api/wham/usage",
resetCreditsConsumeUrl: "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume",
},
},
models: [
{ id: "gpt-5.5", name: "GPT 5.5" },
{ id: "gpt-5.5-review", name: "GPT 5.5 Review", upstreamModelId: "gpt-5.5", quotaFamily: "review" },
{ id: "gpt-5.4", name: "GPT 5.4" },
{ id: "gpt-5.4-review", name: "GPT 5.4 Review", upstreamModelId: "gpt-5.4", quotaFamily: "review" },
{ id: "gpt-5.4-mini", name: "GPT 5.4 Mini" },
{ id: "gpt-5.4-mini-review", name: "GPT 5.4 Mini Review", upstreamModelId: "gpt-5.4-mini", quotaFamily: "review" },
{ id: "gpt-5.3-codex", name: "GPT 5.3 Codex" },
{ id: "gpt-5.3-codex-review", name: "GPT 5.3 Codex Review", upstreamModelId: "gpt-5.3-codex", quotaFamily: "review" },
{ id: "gpt-5.3-codex-xhigh", name: "GPT 5.3 Codex (xHigh)" },
{ id: "gpt-5.3-codex-xhigh-review", name: "GPT 5.3 Codex (xHigh) Review", upstreamModelId: "gpt-5.3-codex-xhigh", quotaFamily: "review" },
{ id: "gpt-5.3-codex-high", name: "GPT 5.3 Codex (High)" },
{ id: "gpt-5.3-codex-high-review", name: "GPT 5.3 Codex (High) Review", upstreamModelId: "gpt-5.3-codex-high", quotaFamily: "review" },
{ id: "gpt-5.3-codex-low", name: "GPT 5.3 Codex (Low)" },
{ id: "gpt-5.3-codex-low-review", name: "GPT 5.3 Codex (Low) Review", upstreamModelId: "gpt-5.3-codex-low", quotaFamily: "review" },
{ id: "gpt-5.3-codex-none", name: "GPT 5.3 Codex (None)" },
{ id: "gpt-5.3-codex-none-review", name: "GPT 5.3 Codex (None) Review", upstreamModelId: "gpt-5.3-codex-none", quotaFamily: "review" },
{ id: "gpt-5.3-codex-spark", name: "GPT 5.3 Codex Spark" },
{ id: "gpt-5.3-codex-spark-review", name: "GPT 5.3 Codex Spark Review", upstreamModelId: "gpt-5.3-codex-spark", quotaFamily: "review" },
{ id: "gpt-5.5-image", name: "GPT 5.5 Image", capabilities: ["text2img","edit"], params: ["size","quality","background","image_detail","output_format"], kind: "image" },
{ id: "gpt-5.4-image", name: "GPT 5.4 Image", capabilities: ["text2img","edit"], params: ["size","quality","background","image_detail","output_format"], kind: "image" },
{ id: "gpt-5.3-image", name: "GPT 5.3 Image", capabilities: ["text2img","edit"], params: ["size","quality","background","image_detail","output_format"], kind: "image" },
],
serviceKinds: ["llm","image"],
oauth: {
clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
authorizeUrl: "https://auth.openai.com/oauth/authorize",
tokenUrl: "https://auth.openai.com/oauth/token",
scope: "openid profile email offline_access",
codeChallengeMethod: "S256",
fixedPort: 1455,
callbackPath: "/auth/callback",
extraParams: {
id_token_add_organizations: "true",
codex_cli_simplified_flow: "true",
originator: "codex_cli_rs",
},
refreshLeadMs: 432000000,
refresh: {
encoding: "form",
scope: "openid profile email offline_access",
},
maxRefreshAgeMs: 691200000,
trackRefreshAt: true,
},
features: {
usage: true,
},
};

View File

@@ -0,0 +1,25 @@
export default {
id: "cohere",
priority: 90,
alias: "cohere",
display: {
name: "Cohere",
icon: "hub",
color: "#39594D",
textIcon: "CO",
website: "https://cohere.com",
notice: {
apiKeyUrl: "https://dashboard.cohere.com/api-keys",
},
},
category: "apikey",
transport: {
baseUrl: "https://api.cohere.ai/v1/chat/completions",
validateUrl: "https://api.cohere.ai/v1/models",
},
models: [
{ id: "command-r-plus-08-2024", name: "Command R+ (Aug 2024)" },
{ id: "command-r-08-2024", name: "Command R (Aug 2024)" },
{ id: "command-a-03-2025", name: "Command A (Mar 2025)" },
],
};

View File

@@ -0,0 +1,20 @@
export default {
id: "comfyui",
priority: 120,
alias: "comfyui",
display: {
name: "ComfyUI",
icon: "account_tree",
color: "#4CAF50",
textIcon: "CF",
website: "https://github.com/comfyanonymous/ComfyUI",
},
category: "apikey",
transport: null,
models: [
{ id: "flux-dev", name: "FLUX Dev", params: ["n","size"], kind: "image" },
{ id: "sdxl", name: "SDXL", params: ["n","size"], kind: "image" },
],
serviceKinds: ["image"],
imageConfig: { baseUrl: "http://localhost:8188" },
};

View File

@@ -0,0 +1,43 @@
export default {
id: "commandcode",
priority: 100,
alias: "commandcode",
aliases: [
"cmc",
],
uiAlias: "cmc",
display: {
name: "Command Code",
icon: "smart_toy",
color: "#000000",
textIcon: "CC",
website: "https://commandcode.ai",
notice: {
text: "Use your CommandCode CLI API key (starts with user_...) from ~/.commandcode/auth.json or commandcode.ai/studio.",
apiKeyUrl: "https://commandcode.ai/studio",
},
},
category: "apikey",
transport: {
baseUrl: "https://api.commandcode.ai/alpha/generate",
format: "commandcode",
forceStream: true,
headers: {
"x-command-code-version": "0.25.7",
"x-cli-environment": "cli",
},
},
models: [
{ id: "deepseek/deepseek-v4-pro", name: "DeepSeek V4 Pro" },
{ id: "deepseek/deepseek-v4-flash", name: "DeepSeek V4 Flash" },
{ id: "moonshotai/Kimi-K2.6", name: "Kimi K2.6" },
{ id: "moonshotai/Kimi-K2.5", name: "Kimi K2.5" },
{ id: "zai-org/GLM-5.1", name: "GLM 5.1" },
{ id: "zai-org/GLM-5", name: "GLM 5" },
{ id: "MiniMaxAI/MiniMax-M2.7", name: "MiniMax M2.7" },
{ id: "MiniMaxAI/MiniMax-M2.5", name: "MiniMax M2.5" },
{ id: "Qwen/Qwen3.6-Max-Preview", name: "Qwen 3.6 Max Preview" },
{ id: "Qwen/Qwen3.6-Plus", name: "Qwen 3.6 Plus" },
{ id: "stepfun/Step-3.5-Flash", name: "Step 3.5 Flash" },
],
};

View File

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

View File

@@ -0,0 +1,58 @@
export default {
id: "cursor",
priority: 50,
alias: "cu",
uiAlias: "cu",
display: {
name: "Cursor IDE",
icon: "edit_note",
color: "#00D4AA",
website: "https://cursor.com",
notice: {
signupUrl: "https://cursor.com",
},
},
category: "oauth",
transport: {
baseUrl: "https://api2.cursor.sh",
chatPath: "/aiserver.v1.ChatService/StreamUnifiedChatWithTools",
format: "cursor",
headers: {
"connect-accept-encoding": "gzip",
"connect-protocol-version": "1",
"Content-Type": "application/connect+proto",
"User-Agent": "connect-es/1.6.1",
},
clientVersion: "3.1.0",
},
models: [
{ id: "default", name: "Auto (Server Picks)" },
{ id: "claude-4.5-opus-high-thinking", name: "Claude 4.5 Opus High Thinking" },
{ id: "claude-4.5-opus-high", name: "Claude 4.5 Opus High" },
{ id: "claude-4.5-sonnet-thinking", name: "Claude 4.5 Sonnet Thinking" },
{ id: "claude-4.5-sonnet", name: "Claude 4.5 Sonnet" },
{ id: "claude-4.5-haiku", name: "Claude 4.5 Haiku" },
{ id: "claude-4.5-opus", name: "Claude 4.5 Opus" },
{ id: "gpt-5.2-codex", name: "GPT 5.2 Codex" },
{ id: "claude-4.6-opus-max", name: "Claude 4.6 Opus Max" },
{ id: "claude-4.6-sonnet-medium-thinking", name: "Claude 4.6 Sonnet Medium Thinking" },
{ id: "kimi-k2.5", name: "Kimi K2.5" },
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" },
{ id: "gpt-5.2", name: "GPT 5.2" },
{ id: "gpt-5.3-codex", name: "GPT 5.3 Codex" },
],
oauth: {
apiEndpoint: "https://api2.cursor.sh",
chatEndpoint: "/aiserver.v1.ChatService/StreamUnifiedChatWithTools",
modelsEndpoint: "/aiserver.v1.AiService/GetDefaultModelNudgeData",
api3Endpoint: "https://api3.cursor.sh",
agentEndpoint: "https://agent.api5.cursor.sh",
agentNonPrivacyEndpoint: "https://agentn.api5.cursor.sh",
clientVersion: "3.1.0",
clientType: "ide",
dbKeys: {
accessToken: "cursorAuth/accessToken",
machineId: "storage.serviceMachineId",
},
},
};

View File

@@ -0,0 +1,33 @@
export default {
id: "deepgram",
priority: 20,
alias: "deepgram",
aliases: [
"dg",
],
uiAlias: "dg",
display: {
name: "Deepgram",
icon: "mic",
color: "#13EF93",
textIcon: "DG",
website: "https://deepgram.com",
notice: {
text: "$200 free credit on signup (no card required). Aura-1: $0.015/1k chars, Aura-2: $0.030/1k chars (Pay-As-You-Go).",
apiKeyUrl: "https://console.deepgram.com/api-keys",
},
},
category: "apikey",
authType: "apikey",
transport: {
baseUrl: "https://api.deepgram.com/v1/listen",
},
models: [
{ id: "nova-3", name: "Nova 3", params: ["language"], kind: "stt" },
{ id: "nova-2", name: "Nova 2", params: ["language"], kind: "stt" },
{ id: "whisper-large", name: "Whisper Large", params: ["language"], kind: "stt" },
{ id: "nova", name: "Nova", kind: "stt" },
],
serviceKinds: ["stt"],
sttConfig: { baseUrl: "https://api.deepgram.com/v1/listen", authType: "apikey", authHeader: "token", format: "deepgram" },
};

View File

@@ -0,0 +1,51 @@
import { CLAUDE_API_HEADERS } from "../shared.js";
export default {
id: "deepseek",
priority: 110,
alias: "deepseek",
aliases: [
"ds",
],
uiAlias: "ds",
display: {
name: "DeepSeek",
icon: "bolt",
color: "#4D6BFE",
textIcon: "DS",
website: "https://deepseek.com",
notice: {
apiKeyUrl: "https://platform.deepseek.com/api_keys",
},
},
category: "apikey",
transport: {
baseUrl: "https://api.deepseek.com/chat/completions",
validateUrl: "https://api.deepseek.com/models",
reasoningInject: {
scope: "all",
},
},
// Multi-endpoint: pick the transport matching client sourceFormat to skip translation.
transports: [
{
format: "openai",
baseUrl: "https://api.deepseek.com/chat/completions",
auth: { combined: true, header: "Authorization", scheme: "bearer" },
},
{
format: "claude",
baseUrl: "https://api.deepseek.com/anthropic/v1/messages",
headers: { ...CLAUDE_API_HEADERS },
auth: { combined: true, header: "x-api-key", scheme: "raw" },
},
],
models: [
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro" },
{ id: "deepseek-v4-pro-max", name: "DeepSeek V4 Pro Max", upstreamModelId: "deepseek-v4-pro" },
{ id: "deepseek-v4-pro-none", name: "DeepSeek V4 Pro No Thinking", upstreamModelId: "deepseek-v4-pro" },
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash" },
{ id: "deepseek-chat", name: "DeepSeek V3.2 Chat" },
{ id: "deepseek-reasoner", name: "DeepSeek V3.2 Reasoner" },
],
};

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,34 @@
export default {
id: "fal-ai",
priority: 90,
hasFree: true,
alias: "fal-ai",
aliases: [
"fal",
],
uiAlias: "fal",
display: {
name: "Fal.ai",
icon: "image",
color: "#2563EB",
textIcon: "FL",
website: "https://fal.ai",
notice: {
apiKeyUrl: "https://fal.ai/dashboard/keys",
},
},
category: "apikey",
authType: "apikey",
transport: null,
models: [
{ id: "fal-ai/flux/schnell", name: "FLUX Schnell", params: ["n","size"], kind: "image" },
{ id: "fal-ai/flux/dev", name: "FLUX Dev", params: ["n","size"], kind: "image" },
{ id: "fal-ai/flux-pro/v1.1", name: "FLUX Pro v1.1", params: ["n","size"], kind: "image" },
{ id: "fal-ai/flux-pro/v1.1-ultra", name: "FLUX Pro v1.1 Ultra", params: ["n","size"], kind: "image" },
{ id: "fal-ai/recraft-v3", name: "Recraft V3", params: ["n","size","style"], kind: "image" },
{ id: "fal-ai/ideogram/v2", name: "Ideogram V2", params: ["n","size","style"], kind: "image" },
{ id: "fal-ai/stable-diffusion-v35-large", name: "SD 3.5 Large", params: ["n","size"], kind: "image" },
],
serviceKinds: ["image"],
imageConfig: { baseUrl: "https://queue.fal.run" },
};

View File

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

View File

@@ -0,0 +1,29 @@
export default {
id: "fireworks",
priority: 50,
alias: "fireworks",
display: {
name: "Fireworks AI",
icon: "local_fire_department",
color: "#7B2EF2",
textIcon: "FW",
website: "https://fireworks.ai",
notice: {
apiKeyUrl: "https://fireworks.ai/account/api-keys",
},
},
category: "apikey",
authType: "apikey",
transport: {
baseUrl: "https://api.fireworks.ai/inference/v1/chat/completions",
validateUrl: "https://api.fireworks.ai/inference/v1/models",
},
models: [
{ id: "accounts/fireworks/models/deepseek-v3p1", name: "DeepSeek V3.1" },
{ id: "accounts/fireworks/models/llama-v3p3-70b-instruct", name: "Llama 3.3 70B" },
{ id: "accounts/fireworks/models/qwen3-235b-a22b", name: "Qwen3 235B" },
{ id: "nomic-ai/nomic-embed-text-v1.5", name: "Nomic Embed Text v1.5", kind: "embedding" },
],
serviceKinds: ["llm", "embedding"],
embeddingConfig: { baseUrl: "https://api.fireworks.ai/inference/v1/embeddings" },
};

View File

@@ -0,0 +1,58 @@
import { GOOGLE_OAUTH_CLIENT } from "../shared.js";
export default {
id: "gemini-cli",
priority: 20,
hasFree: true,
alias: "gc",
uiAlias: "gc",
display: {
name: "Gemini CLI",
icon: "terminal",
color: "#4285F4",
website: "https://github.com/google-gemini/gemini-cli",
notice: {
signupUrl: "https://github.com/google-gemini/gemini-cli",
},
deprecated: true,
deprecationNotice: "RISK_NOTICE",
},
category: "free",
transport: {
baseUrl: "https://cloudcode-pa.googleapis.com/v1internal",
format: "gemini-cli",
cliVersion: "0.34.0",
apiClient: "google-genai-sdk/1.41.0 gl-node/v22.19.0",
usage: {
quotaUrl: "https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota",
loadCodeAssistUrl: "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
},
clientId: "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com",
clientSecret: "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",
},
models: [
{ id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview" },
{ id: "gemini-3-pro-preview", name: "Gemini 3 Pro Preview" },
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" },
{ id: "gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview" },
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro" },
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash" },
{ id: "gemini-2.5-flash-lite", name: "Gemini 2.5 Flash Lite" },
],
oauth: {
authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
tokenUrl: "https://oauth2.googleapis.com/token",
userInfoUrl: "https://www.googleapis.com/oauth2/v1/userinfo",
scopes: [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
],
refresh: {
encoding: "form",
},
},
features: {
usage: true,
},
};

View File

@@ -0,0 +1,80 @@
import { GOOGLE_OAUTH_CLIENT } from "../shared.js";
export default {
id: "gemini",
priority: 50,
hasFree: true,
alias: "gemini",
display: {
name: "Gemini",
icon: "diamond",
color: "#4285F4",
textIcon: "GE",
website: "https://ai.google.dev",
notice: {
apiKeyUrl: "https://aistudio.google.com/app/apikey",
},
},
category: "freeTier",
mediaPriority: 1,
transport: {
baseUrl: "https://generativelanguage.googleapis.com/v1beta/models",
format: "gemini",
clientId: "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com",
clientSecret: "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",
auth: {
apiKey: {
header: "x-goog-api-key",
scheme: "raw",
},
oauth: {
header: "Authorization",
scheme: "bearer",
},
},
},
models: [
{ id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview" },
{ id: "gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview" },
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" },
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro" },
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash" },
{ id: "gemini-2.5-flash-lite", name: "Gemini 2.5 Flash Lite" },
{ id: "gemma-4-31b-it", name: "Gemma 4 31B IT" },
{ id: "gemini-embedding-2-preview", name: "Gemini Embedding 2 Preview", kind: "embedding" },
{ id: "gemini-embedding-001", name: "Gemini Embedding 001", kind: "embedding" },
{ id: "text-embedding-005", name: "Text Embedding 005", kind: "embedding" },
{ id: "text-embedding-004", name: "Text Embedding 004 (Legacy)", kind: "embedding" },
{ id: "gemini-3.1-flash-image-preview", name: "Gemini 3.1 Flash Image (Nano Banana 2)", params: [], kind: "image" },
{ id: "gemini-3-pro-image-preview", name: "Gemini 3 Pro Image (Nano Banana Pro)", params: [], kind: "image" },
{ id: "gemini-2.5-flash-image", name: "Gemini 2.5 Flash Image (Nano Banana)", params: [], kind: "image" },
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro (Best)", params: ["language","prompt"], kind: "stt" },
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", params: ["language","prompt"], kind: "stt" },
{ id: "gemini-2.5-flash-lite", name: "Gemini 2.5 Flash Lite (Cheapest)", params: ["language","prompt"], kind: "stt" },
{ id: "gemini-2.0-flash", name: "Gemini 2.0 Flash", params: ["language","prompt"], kind: "stt" },
{ id: "gemini-2.5-flash-preview-tts", name: "Gemini 2.5 Flash TTS", kind: "tts" },
{ id: "gemini-2.5-pro-preview-tts", name: "Gemini 2.5 Pro TTS", kind: "tts" },
{ id: "embedding-001", name: "Embedding 001", dimensions: 768, kind: "embedding" },
],
serviceKinds: ["llm","embedding","image","imageToText","webSearch","tts","stt"],
ttsConfig: {
baseUrl: "https://generativelanguage.googleapis.com/v1beta/models",
authType: "apikey",
authHeader: "key",
format: "gemini-tts",
},
sttConfig: {
baseUrl: "https://generativelanguage.googleapis.com/v1beta/models",
authType: "apikey",
authHeader: "key",
format: "gemini-stt",
},
embeddingConfig: { baseUrl: "https://generativelanguage.googleapis.com/v1beta/models", authType: "apikey", authHeader: "key" },
imageConfig: { baseUrl: "https://generativelanguage.googleapis.com/v1beta/models" },
searchViaChat: {
defaultModel: "gemini-2.5-flash",
endpoint: "https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent",
pricingUrl: "https://ai.google.dev/pricing",
freeTier: "Free tier: 15 RPM, 1M tokens/day on gemini-2.5-flash via AI Studio.",
},
};

View File

@@ -0,0 +1,82 @@
export default {
id: "github",
priority: 40,
alias: "gh",
uiAlias: "gh",
display: {
name: "GitHub Copilot",
icon: "code",
color: "#333333",
website: "https://github.com/features/copilot",
notice: {
signupUrl: "https://github.com/features/copilot",
},
deprecated: true,
deprecationNotice: "RISK_NOTICE",
},
category: "oauth",
transport: {
baseUrl: "https://api.githubcopilot.com/chat/completions",
responsesUrl: "https://api.githubcopilot.com/responses",
headers: {
"copilot-integration-id": "vscode-chat",
"editor-version": "vscode/1.110.0",
"editor-plugin-version": "copilot-chat/0.38.0",
"user-agent": "GitHubCopilotChat/0.38.0",
"openai-intent": "conversation-panel",
"x-github-api-version": "2025-04-01",
"x-vscode-user-agent-library-version": "electron-fetch",
"X-Initiator": "user",
Accept: "application/json",
"Content-Type": "application/json",
},
copilot: {
vscodeVersion: "1.110.0",
chatVersion: "0.38.0",
userAgent: "GitHubCopilotChat/0.38.0",
apiVersion: "2025-04-01",
},
usage: {
url: "https://api.github.com/copilot_internal/user",
},
},
models: [
{ id: "gpt-5.2", name: "GPT-5.2" },
{ id: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
{ id: "gpt-5.4", name: "GPT-5.4" },
{ id: "gpt-5.4-mini", name: "GPT-5.4 Mini" },
{ id: "claude-haiku-4.5", name: "Claude Haiku 4.5" },
{ id: "claude-opus-4.5", name: "Claude Opus 4.5" },
{ id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5" },
{ id: "claude-sonnet-4.6", name: "Claude Sonnet 4.6" },
{ id: "claude-opus-4.6", name: "Claude Opus 4.6" },
{ id: "claude-opus-4.7", name: "Claude Opus 4.7" },
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro" },
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash" },
{ id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro" },
{ id: "grok-code-fast-1", name: "Grok Code Fast 1" },
{ id: "oswe-vscode-prime", name: "Raptor Mini" },
{ id: "goldeneye-free-auto", name: "GoldenEye" },
{ id: "text-embedding-3-small", name: "Text Embedding 3 Small (GitHub)", kind: "embedding" },
{ id: "text-embedding-3-large", name: "Text Embedding 3 Large (GitHub)", kind: "embedding" },
],
serviceKinds: ["llm","embedding"],
embeddingConfig: { baseUrl: "https://models.github.ai/inference/embeddings", authType: "apikey", authHeader: "bearer" },
oauth: {
clientId: "Iv1.b507a08c87ecfe98",
authorizeUrl: "https://github.com/login/oauth/authorize",
deviceCodeUrl: "https://github.com/login/device/code",
tokenUrl: "https://github.com/login/oauth/access_token",
userInfoUrl: "https://api.github.com/user",
scopes: "read:user",
apiVersion: "2022-11-28",
copilotTokenUrl: "https://api.github.com/copilot_internal/v2/token",
userAgent: "GitHubCopilotChat/0.26.7",
editorVersion: "vscode/1.85.0",
editorPluginVersion: "copilot-chat/0.26.7",
},
features: {
usage: true,
},
};

View File

@@ -0,0 +1,32 @@
export default {
id: "gitlab",
hidden: true,
priority: 100,
display: {
name: "GitLab Duo",
icon: "code",
color: "#FC6D26",
textIcon: "GL",
website: "https://gitlab.com",
notice: {
signupUrl: "https://gitlab.com",
},
},
category: "oauth",
transport: {
baseUrl: "https://gitlab.com/api/v4/chat/completions",
auth: {
combined: true,
header: "Authorization",
scheme: "bearer",
},
},
oauth: {
defaultBaseUrl: "https://gitlab.com",
authorizeUrlPath: "/oauth/authorize",
tokenUrlPath: "/oauth/token",
userInfoUrlPath: "/api/v4/user",
scope: "api read_user",
codeChallengeMethod: "S256",
},
};

View File

@@ -0,0 +1,35 @@
export default {
id: "glm-cn",
priority: 130,
alias: "glm-cn",
display: {
name: "GLM (China)",
icon: "code",
color: "#DC2626",
textIcon: "GC",
website: "https://open.bigmodel.cn",
notice: {
apiKeyUrl: "https://open.bigmodel.cn/usercenter/apikeys",
},
},
category: "apikey",
transport: {
baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4/chat/completions",
headers: {},
usage: {
url: "https://open.bigmodel.cn/api/monitor/usage/quota/limit",
},
},
models: [
{ id: "glm-5.2", name: "GLM 5.2" },
{ id: "glm-5.1", name: "GLM 5.1" },
{ id: "glm-5", name: "GLM 5" },
{ id: "glm-4.7", name: "GLM-4.7" },
{ id: "glm-4.6", name: "GLM-4.6" },
{ id: "glm-4.5-air", name: "GLM-4.5-Air" },
],
features: {
usage: true,
usageApikey: true,
},
};

View File

@@ -0,0 +1,58 @@
import { CLAUDE_API_HEADERS } from "../shared.js";
export default {
id: "glm",
priority: 140,
alias: "glm",
display: {
name: "GLM Coding",
icon: "code",
color: "#2563EB",
textIcon: "GL",
website: "https://open.bigmodel.cn",
notice: {
apiKeyUrl: "https://open.bigmodel.cn/usercenter/apikeys",
},
},
category: "apikey",
transport: {
baseUrl: "https://api.z.ai/api/anthropic/v1/messages",
format: "claude",
urlSuffix: "?beta=true",
headers: { ...CLAUDE_API_HEADERS },
auth: {
combined: true,
header: "x-api-key",
scheme: "raw",
},
usage: {
url: "https://api.z.ai/api/monitor/usage/quota/limit",
},
},
// Multi-endpoint: pick the transport matching client sourceFormat to skip translation.
transports: [
{
format: "openai",
baseUrl: "https://api.z.ai/api/coding/paas/v4/chat/completions",
auth: { combined: true, header: "Authorization", scheme: "bearer" },
},
{
format: "claude",
baseUrl: "https://api.z.ai/api/anthropic/v1/messages",
urlSuffix: "?beta=true",
headers: { ...CLAUDE_API_HEADERS },
auth: { combined: true, header: "x-api-key", scheme: "raw" },
},
],
models: [
{ id: "glm-5.2", name: "GLM 5.2" },
{ id: "glm-5.1", name: "GLM 5.1" },
{ id: "glm-5", name: "GLM 5" },
{ id: "glm-4.7", name: "GLM 4.7" },
{ id: "glm-4.6v", name: "GLM 4.6V (Vision)" },
],
features: {
usage: true,
usageApikey: true,
},
};

View File

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

View File

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

View File

@@ -0,0 +1,39 @@
export default {
id: "grok-web",
priority: 150,
alias: "grok-web",
aliases: [
"gw",
],
uiAlias: "gw",
display: {
name: "Grok Web (Subscription)",
icon: "auto_awesome",
color: "#1DA1F2",
textIcon: "GW",
website: "https://grok.com",
},
category: "webCookie",
authType: "cookie",
authHint: "Paste your sso= cookie value from grok.com",
transport: {
baseUrl: "https://grok.com/rest/app-chat/conversations/new",
format: "grok-web",
authType: "cookie",
},
models: [
{ id: "grok-3", name: "Grok 3" },
{ id: "grok-3-mini", name: "Grok 3 Mini (Thinking)" },
{ id: "grok-3-thinking", name: "Grok 3 Thinking" },
{ id: "grok-4", name: "Grok 4" },
{ id: "grok-4-mini", name: "Grok 4 Mini (Thinking)" },
{ id: "grok-4-thinking", name: "Grok 4 Thinking" },
{ id: "grok-4-heavy", name: "Grok 4 Heavy (SuperGrok)" },
{ id: "grok-4.1-mini", name: "Grok 4.1 Mini (Thinking)" },
{ id: "grok-4.1-fast", name: "Grok 4.1 Fast" },
{ id: "grok-4.1-expert", name: "Grok 4.1 Expert" },
{ id: "grok-4.1-thinking", name: "Grok 4.1 Thinking" },
{ id: "grok-4.2", name: "Grok 4.2 (4.20 Beta)" },
],
passthroughModels: true,
};

Some files were not shown because too many files have changed in this diff Show More