merge: resolve conflicts with origin/master - keep both local and remote features
This commit is contained in:
39
open-sse/AGENTS.md
Normal file
39
open-sse/AGENTS.md
Normal 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
27
open-sse/config/mediaConfig.js
Normal file
27
open-sse/config/mediaConfig.js
Normal 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",
|
||||
]);
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
36
open-sse/executors/codebuddy-cn.js
Normal file
36
open-sse/executors/codebuddy-cn.js
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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 }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
@@ -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(() => { });
|
||||
|
||||
@@ -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\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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
73
open-sse/handlers/imageProviders/antigravity.js
Normal file
73
open-sse/handlers/imageProviders/antigravity.js
Normal 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 }],
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 }
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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("/");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -30,9 +30,6 @@ export {
|
||||
// Services
|
||||
export {
|
||||
detectFormat,
|
||||
getProviderConfig,
|
||||
buildProviderUrl,
|
||||
buildProviderHeaders,
|
||||
getTargetFormat
|
||||
} from "./services/provider.js";
|
||||
|
||||
|
||||
98
open-sse/providers/REGISTRY_TEMPLATE.js
Normal file
98
open-sse/providers/REGISTRY_TEMPLATE.js
Normal 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,
|
||||
};
|
||||
269
open-sse/providers/capabilities.js
Normal file
269
open-sse/providers/capabilities.js
Normal 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 };
|
||||
}
|
||||
51
open-sse/providers/index.js
Normal file
51
open-sse/providers/index.js
Normal 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());
|
||||
20
open-sse/providers/models/helpers.js
Normal file
20
open-sse/providers/models/helpers.js
Normal 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"
|
||||
}
|
||||
];
|
||||
});
|
||||
}
|
||||
33
open-sse/providers/models/namePatterns.js
Normal file
33
open-sse/providers/models/namePatterns.js
Normal 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;
|
||||
}
|
||||
31
open-sse/providers/models/schema.js
Normal file
31
open-sse/providers/models/schema.js
Normal 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;
|
||||
}
|
||||
304
open-sse/providers/pricing.js
Normal file
304
open-sse/providers/pricing.js
Normal file
@@ -0,0 +1,304 @@
|
||||
// Pricing rates for AI models — all rates in $/1M tokens
|
||||
//
|
||||
// Fallback order (first match wins):
|
||||
// 1. PROVIDER_PRICING[provider][model] — provider-specific override
|
||||
// 2. MODEL_PRICING[model] — canonical model price (provider-agnostic)
|
||||
// 3. PATTERN_PRICING — glob pattern match (e.g. "codex-*")
|
||||
|
||||
/**
|
||||
* Canonical model pricing — provider-agnostic.
|
||||
* Cover all known models; deduplicated across providers.
|
||||
*/
|
||||
export const MODEL_PRICING = {
|
||||
// === Anthropic / Claude ===
|
||||
"claude-opus-4-6": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 25.00, cache_creation: 6.25 },
|
||||
"claude-opus-4-5-20251101": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 25.00, cache_creation: 6.25 },
|
||||
"claude-sonnet-4-6": { input: 3.00, output: 15.00, cached: 0.30, reasoning: 15.00, cache_creation: 3.75 },
|
||||
"claude-sonnet-4-5-20250929": { input: 3.00, output: 15.00, cached: 0.30, reasoning: 15.00, cache_creation: 3.75 },
|
||||
"claude-haiku-4-5-20251001": { input: 1.00, output: 5.00, cached: 0.10, reasoning: 5.00, cache_creation: 1.25 },
|
||||
"claude-sonnet-4-20250514": { input: 3.00, output: 15.00, cached: 1.50, reasoning: 15.00, cache_creation: 3.00 },
|
||||
"claude-opus-4-20250514": { input: 15.00, output: 25.00, cached: 7.50, reasoning: 112.50, cache_creation: 15.00 },
|
||||
"claude-3-5-sonnet-20241022": { input: 3.00, output: 15.00, cached: 1.50, reasoning: 15.00, cache_creation: 3.00 },
|
||||
"claude-haiku-4.5": { input: 0.50, output: 2.50, cached: 0.05, reasoning: 3.75, cache_creation: 0.50 },
|
||||
"claude-opus-4.1": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 },
|
||||
"claude-opus-4.5": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 },
|
||||
"claude-opus-4.6": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 },
|
||||
"claude-sonnet-4": { input: 3.00, output: 15.00, cached: 0.30, reasoning: 22.50, cache_creation: 3.00 },
|
||||
"claude-sonnet-4.5": { input: 3.00, output: 15.00, cached: 0.30, reasoning: 22.50, cache_creation: 3.00 },
|
||||
"claude-sonnet-4.6": { input: 3.00, output: 15.00, cached: 0.30, reasoning: 22.50, cache_creation: 3.00 },
|
||||
"claude-opus-4-5-thinking": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 },
|
||||
"claude-opus-4-6-thinking": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 },
|
||||
|
||||
// === OpenAI / GPT ===
|
||||
"gpt-3.5-turbo": { input: 0.50, output: 1.50, cached: 0.25, reasoning: 2.25, cache_creation: 0.50 },
|
||||
"gpt-4": { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 },
|
||||
"gpt-4-turbo": { input: 10.00, output: 30.00, cached: 5.00, reasoning: 45.00, cache_creation: 10.00 },
|
||||
"gpt-4o": { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 },
|
||||
"gpt-4o-mini": { input: 0.15, output: 0.60, cached: 0.075, reasoning: 0.90, cache_creation: 0.15 },
|
||||
"gpt-4.1": { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 },
|
||||
"gpt-5": { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 },
|
||||
"gpt-5-mini": { input: 0.75, output: 3.00, cached: 0.375, reasoning: 4.50, cache_creation: 0.75 },
|
||||
"gpt-5-codex": { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 },
|
||||
"gpt-5.1": { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 },
|
||||
"gpt-5.1-codex": { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 },
|
||||
"gpt-5.1-codex-mini": { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 },
|
||||
"gpt-5.1-codex-mini-high": { input: 2.00, output: 8.00, cached: 1.00, reasoning: 12.00, cache_creation: 2.00 },
|
||||
"gpt-5.1-codex-max": { input: 8.00, output: 32.00, cached: 4.00, reasoning: 48.00, cache_creation: 8.00 },
|
||||
"gpt-5.2": { input: 5.00, output: 20.00, cached: 2.50, reasoning: 30.00, cache_creation: 5.00 },
|
||||
"gpt-5.2-codex": { input: 5.00, output: 20.00, cached: 2.50, reasoning: 30.00, cache_creation: 5.00 },
|
||||
"gpt-5.3-codex": { input: 6.00, output: 24.00, cached: 3.00, reasoning: 36.00, cache_creation: 6.00 },
|
||||
"gpt-5.3-codex-xhigh": { input: 10.00, output: 40.00, cached: 5.00, reasoning: 60.00, cache_creation: 10.00 },
|
||||
"gpt-5.3-codex-high": { input: 8.00, output: 32.00, cached: 4.00, reasoning: 48.00, cache_creation: 8.00 },
|
||||
"gpt-5.3-codex-low": { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 },
|
||||
"gpt-5.3-codex-none": { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 },
|
||||
"gpt-5.3-codex-spark": { input: 3.00, output: 12.00, cached: 0.30, reasoning: 12.00, cache_creation: 3.00 },
|
||||
"o1": { input: 15.00, output: 60.00, cached: 7.50, reasoning: 90.00, cache_creation: 15.00 },
|
||||
"o1-mini": { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 },
|
||||
|
||||
// === Gemini ===
|
||||
"gemini-3-flash-preview": { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 },
|
||||
"gemini-3-pro-preview": { input: 2.00, output: 12.00, cached: 0.25, reasoning: 18.00, cache_creation: 2.00 },
|
||||
"gemini-3.1-pro-low": { input: 2.00, output: 12.00, cached: 0.25, reasoning: 18.00, cache_creation: 2.00 },
|
||||
"gemini-3.1-pro-high": { input: 4.00, output: 18.00, cached: 0.50, reasoning: 27.00, cache_creation: 4.00 },
|
||||
"gemini-pro-agent": { input: 4.00, output: 18.00, cached: 0.50, reasoning: 27.00, cache_creation: 4.00 },
|
||||
"gemini-3-flash-agent": { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 },
|
||||
"gemini-3.5-flash-low": { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 },
|
||||
"gemini-3.5-flash-extra-low": { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 },
|
||||
"gemini-3-flash": { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 },
|
||||
"gemini-2.5-pro": { input: 2.00, output: 12.00, cached: 0.25, reasoning: 18.00, cache_creation: 2.00 },
|
||||
"gemini-2.5-flash": { input: 0.30, output: 2.50, cached: 0.03, reasoning: 3.75, cache_creation: 0.30 },
|
||||
"gemini-2.5-flash-lite": { input: 0.15, output: 1.25, cached: 0.015, reasoning: 1.875, cache_creation: 0.15 },
|
||||
|
||||
// === Qwen ===
|
||||
"qwen3-coder-plus": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 },
|
||||
"qwen3-coder-flash": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
|
||||
|
||||
// === Kimi ===
|
||||
"kimi-k2": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 },
|
||||
"kimi-k2-thinking": { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 },
|
||||
"kimi-k2.5": { input: 1.20, output: 4.80, cached: 0.60, reasoning: 7.20, cache_creation: 1.20 },
|
||||
"kimi-k2.5-thinking": { input: 1.80, output: 7.20, cached: 0.90, reasoning: 10.80, cache_creation: 1.80 },
|
||||
"kimi-latest": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 },
|
||||
|
||||
// === DeepSeek ===
|
||||
"deepseek-chat": { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 },
|
||||
"deepseek-reasoner": { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 },
|
||||
"deepseek-r1": { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 },
|
||||
"deepseek-v3.2-chat": { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 },
|
||||
"deepseek-v3.2-reasoner": { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 },
|
||||
"deepseek-v4-flash": { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 },
|
||||
"deepseek-v4-pro": { input: 0.435, output: 0.87, cached: 0.003625, reasoning: 0.87, cache_creation: 0.435 },
|
||||
|
||||
// === GLM ===
|
||||
"glm-4.6": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
|
||||
"glm-4.6v": { input: 0.75, output: 3.00, cached: 0.375, reasoning: 4.50, cache_creation: 0.75 },
|
||||
"glm-4.7": { input: 0.75, output: 3.00, cached: 0.375, reasoning: 4.50, cache_creation: 0.75 },
|
||||
"glm-5": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 },
|
||||
|
||||
// === MiniMax ===
|
||||
"MiniMax-M3": { input: 0.30, output: 1.20, cached: 0.06, reasoning: 1.80, cache_creation: 0.30 },
|
||||
"MiniMax-M2.1": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
|
||||
"MiniMax-M2.5": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
|
||||
"MiniMax-M2.7": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
|
||||
"minimax-m2.1": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
|
||||
"minimax-m2.5": { input: 0.60, output: 2.40, cached: 0.30, reasoning: 3.60, cache_creation: 0.60 },
|
||||
|
||||
// === Grok ===
|
||||
"grok-code-fast-1": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
|
||||
|
||||
// === OpenRouter fallback ===
|
||||
"auto": { input: 2.00, output: 8.00, cached: 1.00, reasoning: 12.00, cache_creation: 2.00 },
|
||||
|
||||
// === Misc ===
|
||||
"oswe-vscode-prime": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 },
|
||||
"gpt-oss-120b-medium": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
|
||||
"vision-model": { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 },
|
||||
"coder-model": { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 },
|
||||
};
|
||||
|
||||
/**
|
||||
* Provider-specific pricing overrides.
|
||||
* Only include entries where price DIFFERS from MODEL_PRICING.
|
||||
* Keyed by provider alias (cc, cx, gc, gh, ...) or provider id (openai, anthropic, ...).
|
||||
*/
|
||||
export const PROVIDER_PRICING = {
|
||||
// GitHub Copilot (gh) — gpt-5.3-codex has different rate than canonical
|
||||
gh: {
|
||||
"gpt-5.3-codex": { input: 1.75, output: 14.00, cached: 0.175, reasoning: 14.00, cache_creation: 1.75 },
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Pattern-based pricing fallback — matched when no exact model entry found.
|
||||
* Patterns use simple glob: "*" matches any substring.
|
||||
* First match wins — order matters.
|
||||
*/
|
||||
export const PATTERN_PRICING = [
|
||||
// --- Codex variants ---
|
||||
{ pattern: "*-codex-xhigh", pricing: { input: 10.00, output: 40.00, cached: 5.00, reasoning: 60.00, cache_creation: 10.00 } },
|
||||
{ pattern: "*-codex-high", pricing: { input: 8.00, output: 32.00, cached: 4.00, reasoning: 48.00, cache_creation: 8.00 } },
|
||||
{ pattern: "*-codex-max", pricing: { input: 8.00, output: 32.00, cached: 4.00, reasoning: 48.00, cache_creation: 8.00 } },
|
||||
{ pattern: "*-codex-mini-*", pricing: { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 } },
|
||||
{ pattern: "*-codex-mini", pricing: { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 } },
|
||||
{ pattern: "*-codex-low", pricing: { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 } },
|
||||
{ pattern: "*-codex-none", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } },
|
||||
{ pattern: "*-codex-spark", pricing: { input: 3.00, output: 12.00, cached: 0.30, reasoning: 12.00, cache_creation: 3.00 } },
|
||||
{ pattern: "codex-*", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } },
|
||||
{ pattern: "*-codex", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } },
|
||||
|
||||
// --- Claude ---
|
||||
{ pattern: "claude-opus-*", pricing: { input: 5.00, output: 25.00, cached: 0.50, reasoning: 25.00, cache_creation: 6.25 } },
|
||||
{ pattern: "claude-sonnet-*", pricing: { input: 3.00, output: 15.00, cached: 0.30, reasoning: 15.00, cache_creation: 3.75 } },
|
||||
{ pattern: "claude-haiku-*", pricing: { input: 1.00, output: 5.00, cached: 0.10, reasoning: 5.00, cache_creation: 1.25 } },
|
||||
{ pattern: "claude-*", pricing: { input: 3.00, output: 15.00, cached: 0.30, reasoning: 15.00, cache_creation: 3.75 } },
|
||||
|
||||
// --- Gemini (specific first, generic last) ---
|
||||
{ pattern: "gemini-*-flash-lite", pricing: { input: 0.15, output: 1.25, cached: 0.015, reasoning: 1.875, cache_creation: 0.15 } },
|
||||
{ pattern: "gemini-*-flash", pricing: { input: 0.30, output: 2.50, cached: 0.03, reasoning: 3.75, cache_creation: 0.30 } },
|
||||
{ pattern: "gemini-*-pro", pricing: { input: 2.00, output: 12.00, cached: 0.25, reasoning: 18.00, cache_creation: 2.00 } },
|
||||
{ pattern: "gemini-3-*", pricing: { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 } },
|
||||
{ pattern: "gemini-2.5-*", pricing: { input: 0.30, output: 2.50, cached: 0.03, reasoning: 3.75, cache_creation: 0.30 } },
|
||||
{ pattern: "gemini-*", pricing: { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 } },
|
||||
|
||||
// --- GPT (specific first, generic last) ---
|
||||
{ pattern: "gpt-5.3-*", pricing: { input: 6.00, output: 24.00, cached: 3.00, reasoning: 36.00, cache_creation: 6.00 } },
|
||||
{ pattern: "gpt-5.2-*", pricing: { input: 5.00, output: 20.00, cached: 2.50, reasoning: 30.00, cache_creation: 5.00 } },
|
||||
{ pattern: "gpt-5.1-*", pricing: { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 } },
|
||||
{ pattern: "gpt-5-*", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } },
|
||||
{ pattern: "gpt-5*", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } },
|
||||
{ pattern: "gpt-4o-*", pricing: { input: 0.15, output: 0.60, cached: 0.075, reasoning: 0.90, cache_creation: 0.15 } },
|
||||
{ pattern: "gpt-4o", pricing: { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 } },
|
||||
{ pattern: "gpt-4*", pricing: { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 } },
|
||||
|
||||
// --- o1 / o-series ---
|
||||
{ pattern: "o1-*", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } },
|
||||
{ pattern: "o1", pricing: { input: 15.00, output: 60.00, cached: 7.50, reasoning: 90.00, cache_creation: 15.00 } },
|
||||
{ pattern: "o3-*", pricing: { input: 10.00, output: 40.00, cached: 5.00, reasoning: 60.00, cache_creation: 10.00 } },
|
||||
{ pattern: "o4-*", pricing: { input: 2.00, output: 8.00, cached: 1.00, reasoning: 12.00, cache_creation: 2.00 } },
|
||||
|
||||
// --- Qwen ---
|
||||
{ pattern: "qwen3-coder-*", pricing: { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 } },
|
||||
{ pattern: "qwen*-coder-*", pricing: { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 } },
|
||||
{ pattern: "qwen*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } },
|
||||
|
||||
// --- Kimi ---
|
||||
{ pattern: "kimi-*-thinking", pricing: { input: 1.80, output: 7.20, cached: 0.90, reasoning: 10.80, cache_creation: 1.80 } },
|
||||
{ pattern: "kimi-k2*", pricing: { input: 1.20, output: 4.80, cached: 0.60, reasoning: 7.20, cache_creation: 1.20 } },
|
||||
{ pattern: "kimi-*", pricing: { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 } },
|
||||
|
||||
// --- DeepSeek ---
|
||||
{ pattern: "deepseek-*reasoner*", pricing: { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 } },
|
||||
{ pattern: "deepseek-r*", pricing: { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 } },
|
||||
{ pattern: "deepseek-v*", pricing: { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 } },
|
||||
{ pattern: "deepseek-*", pricing: { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 } },
|
||||
|
||||
// --- GLM ---
|
||||
{ pattern: "glm-5*", pricing: { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 } },
|
||||
{ pattern: "glm-4*", pricing: { input: 0.75, output: 3.00, cached: 0.375, reasoning: 4.50, cache_creation: 0.75 } },
|
||||
{ pattern: "glm-*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } },
|
||||
|
||||
// --- MiniMax ---
|
||||
{ pattern: "MiniMax-*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } },
|
||||
{ pattern: "minimax-*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } },
|
||||
|
||||
// --- Grok ---
|
||||
{ pattern: "grok-code-*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } },
|
||||
{ pattern: "grok-*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } },
|
||||
];
|
||||
|
||||
/**
|
||||
* Match a model ID against a glob pattern (* = wildcard). Case-insensitive:
|
||||
* registry ids mix casing (e.g. "MiniMax-M2.5" vs "minimax-m2.5").
|
||||
*/
|
||||
export function matchPattern(pattern, model) {
|
||||
const regex = new RegExp("^" + pattern.split("*").map(s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$", "i");
|
||||
return regex.test(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve pricing for a model using the 3-step fallback chain:
|
||||
* 1. PROVIDER_PRICING[provider][model]
|
||||
* 2. MODEL_PRICING[model]
|
||||
* 3. PATTERN_PRICING (glob match)
|
||||
*
|
||||
* @param {string} provider
|
||||
* @param {string} model
|
||||
* @returns {object|null}
|
||||
*/
|
||||
export function getPricingForModel(provider, model) {
|
||||
if (!model) return null;
|
||||
|
||||
// 1. Provider-specific override
|
||||
if (provider && PROVIDER_PRICING[provider]?.[model]) {
|
||||
return PROVIDER_PRICING[provider][model];
|
||||
}
|
||||
|
||||
// 2. Canonical model pricing (strip vendor prefix if needed: "deepseek/deepseek-chat" → "deepseek-chat")
|
||||
const baseModel = model.includes("/") ? model.split("/").pop() : model;
|
||||
if (MODEL_PRICING[baseModel]) return MODEL_PRICING[baseModel];
|
||||
if (MODEL_PRICING[model]) return MODEL_PRICING[model];
|
||||
|
||||
// 3. Pattern match
|
||||
for (const { pattern, pricing } of PATTERN_PRICING) {
|
||||
if (matchPattern(pattern, baseModel) || matchPattern(pattern, model)) {
|
||||
return pricing;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all provider pricing (for UI / API).
|
||||
* Returns PROVIDER_PRICING — consumers should fall back to MODEL_PRICING for unlisted models.
|
||||
*/
|
||||
export function getDefaultPricing() {
|
||||
return PROVIDER_PRICING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format cost for display
|
||||
* @param {number} cost
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatCost(cost) {
|
||||
if (cost === null || cost === undefined || isNaN(cost)) return "$0.00";
|
||||
return `$${cost.toFixed(2)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate cost from tokens and pricing
|
||||
* @param {object} tokens
|
||||
* @param {object} pricing
|
||||
* @returns {number} cost in dollars
|
||||
*/
|
||||
export function calculateCostFromTokens(tokens, pricing) {
|
||||
if (!tokens || !pricing) return 0;
|
||||
|
||||
let cost = 0;
|
||||
|
||||
const inputTokens = tokens.prompt_tokens || tokens.input_tokens || 0;
|
||||
const cachedTokens = tokens.cached_tokens || tokens.cache_read_input_tokens || 0;
|
||||
const nonCachedInput = Math.max(0, inputTokens - cachedTokens);
|
||||
|
||||
cost += nonCachedInput * (pricing.input / 1000000);
|
||||
|
||||
if (cachedTokens > 0) {
|
||||
cost += cachedTokens * ((pricing.cached || pricing.input) / 1000000);
|
||||
}
|
||||
|
||||
const outputTokens = tokens.completion_tokens || tokens.output_tokens || 0;
|
||||
cost += outputTokens * (pricing.output / 1000000);
|
||||
|
||||
const reasoningTokens = tokens.reasoning_tokens || 0;
|
||||
if (reasoningTokens > 0) {
|
||||
cost += reasoningTokens * ((pricing.reasoning || pricing.output) / 1000000);
|
||||
}
|
||||
|
||||
const cacheCreationTokens = tokens.cache_creation_input_tokens || 0;
|
||||
if (cacheCreationTokens > 0) {
|
||||
cost += cacheCreationTokens * ((pricing.cache_creation || pricing.input) / 1000000);
|
||||
}
|
||||
|
||||
return cost;
|
||||
}
|
||||
29
open-sse/providers/registry/alicode-intl.js
Normal file
29
open-sse/providers/registry/alicode-intl.js
Normal 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" },
|
||||
],
|
||||
};
|
||||
30
open-sse/providers/registry/alicode.js
Normal file
30
open-sse/providers/registry/alicode.js
Normal 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" },
|
||||
],
|
||||
};
|
||||
32
open-sse/providers/registry/anthropic.js
Normal file
32
open-sse/providers/registry/anthropic.js
Normal 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"],
|
||||
};
|
||||
82
open-sse/providers/registry/antigravity.js
Normal file
82
open-sse/providers/registry/antigravity.js
Normal 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,
|
||||
},
|
||||
};
|
||||
38
open-sse/providers/registry/assemblyai.js
Normal file
38
open-sse/providers/registry/assemblyai.js
Normal 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",
|
||||
},
|
||||
};
|
||||
45
open-sse/providers/registry/aws-polly.js
Normal file
45
open-sse/providers/registry/aws-polly.js
Normal file
@@ -0,0 +1,45 @@
|
||||
export default {
|
||||
id: "aws-polly",
|
||||
alias: "polly",
|
||||
display: {
|
||||
name: "AWS Polly",
|
||||
icon: "record_voice_over",
|
||||
color: "#FF9900",
|
||||
textIcon: "PL",
|
||||
website: "https://aws.amazon.com/polly/",
|
||||
notice: {
|
||||
text: "Use AWS Secret Access Key as API key; set providerSpecificData.accessKeyId and optional region.",
|
||||
apiKeyUrl: "https://console.aws.amazon.com/iam/home#/security_credentials"
|
||||
}
|
||||
},
|
||||
category: "apikey",
|
||||
authType: "apikey",
|
||||
serviceKinds: [
|
||||
"tts"
|
||||
],
|
||||
ttsConfig: {
|
||||
baseUrl: "https://polly.{region}.amazonaws.com/v1/speech",
|
||||
authType: "apikey",
|
||||
authHeader: "aws-sigv4",
|
||||
format: "aws-polly",
|
||||
models: [
|
||||
{
|
||||
id: "standard",
|
||||
name: "Standard"
|
||||
},
|
||||
{
|
||||
id: "neural",
|
||||
name: "Neural"
|
||||
},
|
||||
{
|
||||
id: "long-form",
|
||||
name: "Long-form"
|
||||
},
|
||||
{
|
||||
id: "generative",
|
||||
name: "Generative"
|
||||
}
|
||||
]
|
||||
},
|
||||
hasProviderSpecificData: true
|
||||
};
|
||||
21
open-sse/providers/registry/azure.js
Normal file
21
open-sse/providers/registry/azure.js
Normal 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: {},
|
||||
},
|
||||
};
|
||||
32
open-sse/providers/registry/black-forest-labs.js
Normal file
32
open-sse/providers/registry/black-forest-labs.js
Normal 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" },
|
||||
};
|
||||
43
open-sse/providers/registry/blackbox.js
Normal file
43
open-sse/providers/registry/blackbox.js
Normal 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" },
|
||||
],
|
||||
};
|
||||
35
open-sse/providers/registry/brave-search.js
Normal file
35
open-sse/providers/registry/brave-search.js
Normal file
@@ -0,0 +1,35 @@
|
||||
export default {
|
||||
id: "brave-search",
|
||||
alias: "brave",
|
||||
display: {
|
||||
name: "Brave Search",
|
||||
icon: "travel_explore",
|
||||
color: "#FB542B",
|
||||
textIcon: "BR",
|
||||
website: "https://brave.com/search/api",
|
||||
notice: {
|
||||
apiKeyUrl: "https://api-dashboard.search.brave.com/app/keys"
|
||||
}
|
||||
},
|
||||
category: "apikey",
|
||||
authType: "apikey",
|
||||
serviceKinds: [
|
||||
"webSearch"
|
||||
],
|
||||
searchConfig: {
|
||||
baseUrl: "https://api.search.brave.com/res/v1",
|
||||
method: "GET",
|
||||
authType: "apikey",
|
||||
authHeader: "x-subscription-token",
|
||||
costPerQuery: 0.005,
|
||||
freeMonthlyQuota: 1000,
|
||||
searchTypes: [
|
||||
"web",
|
||||
"news"
|
||||
],
|
||||
defaultMaxResults: 5,
|
||||
maxMaxResults: 20,
|
||||
timeoutMs: 10000,
|
||||
cacheTTLMs: 300000
|
||||
}
|
||||
};
|
||||
35
open-sse/providers/registry/byteplus.js
Normal file
35
open-sse/providers/registry/byteplus.js
Normal 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"],
|
||||
};
|
||||
36
open-sse/providers/registry/cartesia.js
Normal file
36
open-sse/providers/registry/cartesia.js
Normal file
@@ -0,0 +1,36 @@
|
||||
export default {
|
||||
id: "cartesia",
|
||||
alias: "cartesia",
|
||||
display: {
|
||||
name: "Cartesia",
|
||||
icon: "spatial_audio",
|
||||
color: "#FF4F8B",
|
||||
textIcon: "CA",
|
||||
website: "https://cartesia.ai",
|
||||
notice: {
|
||||
apiKeyUrl: "https://play.cartesia.ai/keys"
|
||||
}
|
||||
},
|
||||
category: "apikey",
|
||||
authType: "apikey",
|
||||
serviceKinds: [
|
||||
"tts"
|
||||
],
|
||||
ttsConfig: {
|
||||
baseUrl: "https://api.cartesia.ai/tts/bytes",
|
||||
authType: "apikey",
|
||||
authHeader: "x-api-key",
|
||||
format: "cartesia",
|
||||
models: [
|
||||
{
|
||||
id: "sonic-2",
|
||||
name: "Sonic 2"
|
||||
},
|
||||
{
|
||||
id: "sonic-3",
|
||||
name: "Sonic 3"
|
||||
}
|
||||
]
|
||||
},
|
||||
hidden: true
|
||||
};
|
||||
31
open-sse/providers/registry/cerebras.js
Normal file
31
open-sse/providers/registry/cerebras.js
Normal 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" },
|
||||
],
|
||||
};
|
||||
24
open-sse/providers/registry/chutes.js
Normal file
24
open-sse/providers/registry/chutes.js
Normal 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",
|
||||
},
|
||||
};
|
||||
89
open-sse/providers/registry/claude.js
Normal file
89
open-sse/providers/registry/claude.js
Normal 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,
|
||||
},
|
||||
};
|
||||
51
open-sse/providers/registry/cline.js
Normal file
51
open-sse/providers/registry/cline.js
Normal 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",
|
||||
},
|
||||
};
|
||||
55
open-sse/providers/registry/cloudflare-ai.js
Normal file
55
open-sse/providers/registry/cloudflare-ai.js
Normal 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" },
|
||||
};
|
||||
77
open-sse/providers/registry/codebuddy-cn.js
Normal file
77
open-sse/providers/registry/codebuddy-cn.js
Normal 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,
|
||||
},
|
||||
};
|
||||
94
open-sse/providers/registry/codex.js
Normal file
94
open-sse/providers/registry/codex.js
Normal 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,
|
||||
},
|
||||
};
|
||||
25
open-sse/providers/registry/cohere.js
Normal file
25
open-sse/providers/registry/cohere.js
Normal 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)" },
|
||||
],
|
||||
};
|
||||
20
open-sse/providers/registry/comfyui.js
Normal file
20
open-sse/providers/registry/comfyui.js
Normal 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" },
|
||||
};
|
||||
43
open-sse/providers/registry/commandcode.js
Normal file
43
open-sse/providers/registry/commandcode.js
Normal 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" },
|
||||
],
|
||||
};
|
||||
30
open-sse/providers/registry/coqui.js
Normal file
30
open-sse/providers/registry/coqui.js
Normal file
@@ -0,0 +1,30 @@
|
||||
export default {
|
||||
id: "coqui",
|
||||
alias: "coqui",
|
||||
display: {
|
||||
name: "Coqui TTS",
|
||||
icon: "record_voice_over",
|
||||
color: "#10B981",
|
||||
textIcon: "CQ",
|
||||
website: "https://github.com/coqui-ai/TTS"
|
||||
},
|
||||
category: "freeTier",
|
||||
authType: "none",
|
||||
serviceKinds: [
|
||||
"tts"
|
||||
],
|
||||
noAuth: true,
|
||||
ttsConfig: {
|
||||
baseUrl: "http://localhost:5002/api/tts",
|
||||
authType: "none",
|
||||
authHeader: "none",
|
||||
format: "coqui",
|
||||
models: [
|
||||
{
|
||||
id: "tts_models/en/ljspeech/tacotron2-DDC",
|
||||
name: "Tacotron2 DDC (LJSpeech)"
|
||||
}
|
||||
]
|
||||
},
|
||||
hidden: true
|
||||
};
|
||||
58
open-sse/providers/registry/cursor.js
Normal file
58
open-sse/providers/registry/cursor.js
Normal 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",
|
||||
},
|
||||
},
|
||||
};
|
||||
33
open-sse/providers/registry/deepgram.js
Normal file
33
open-sse/providers/registry/deepgram.js
Normal 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" },
|
||||
};
|
||||
51
open-sse/providers/registry/deepseek.js
Normal file
51
open-sse/providers/registry/deepseek.js
Normal 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" },
|
||||
],
|
||||
};
|
||||
24
open-sse/providers/registry/edge-tts.js
Normal file
24
open-sse/providers/registry/edge-tts.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export default {
|
||||
id: "edge-tts",
|
||||
alias: "edge-tts",
|
||||
display: {
|
||||
name: "Edge TTS",
|
||||
icon: "record_voice_over",
|
||||
color: "#0078D4",
|
||||
textIcon: "ET"
|
||||
},
|
||||
category: "freeTier",
|
||||
authType: "none",
|
||||
serviceKinds: [
|
||||
"tts"
|
||||
],
|
||||
mediaPriority: 5,
|
||||
noAuth: true,
|
||||
ttsConfig: {
|
||||
baseUrl: "edge-tts",
|
||||
authType: "none",
|
||||
authHeader: "none",
|
||||
format: "edge-tts",
|
||||
models: []
|
||||
}
|
||||
};
|
||||
35
open-sse/providers/registry/elevenlabs.js
Normal file
35
open-sse/providers/registry/elevenlabs.js
Normal file
@@ -0,0 +1,35 @@
|
||||
export default {
|
||||
id: "elevenlabs",
|
||||
alias: "el",
|
||||
display: {
|
||||
name: "ElevenLabs",
|
||||
icon: "record_voice_over",
|
||||
color: "#6C47FF",
|
||||
textIcon: "EL",
|
||||
website: "https://elevenlabs.io",
|
||||
notice: {
|
||||
apiKeyUrl: "https://elevenlabs.io/app/settings/api-keys"
|
||||
}
|
||||
},
|
||||
category: "apikey",
|
||||
authType: "apikey",
|
||||
serviceKinds: [
|
||||
"tts"
|
||||
],
|
||||
ttsConfig: {
|
||||
baseUrl: "https://api.elevenlabs.io/v1/text-to-speech",
|
||||
authType: "apikey",
|
||||
authHeader: "xi-api-key",
|
||||
format: "elevenlabs",
|
||||
models: [
|
||||
{
|
||||
id: "eleven_multilingual_v2",
|
||||
name: "Eleven Multilingual v2"
|
||||
},
|
||||
{
|
||||
id: "eleven_turbo_v2_5",
|
||||
name: "Eleven Turbo v2.5"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
50
open-sse/providers/registry/exa.js
Normal file
50
open-sse/providers/registry/exa.js
Normal file
@@ -0,0 +1,50 @@
|
||||
export default {
|
||||
id: "exa",
|
||||
alias: "exa",
|
||||
display: {
|
||||
name: "Exa",
|
||||
icon: "manage_search",
|
||||
color: "#2563EB",
|
||||
textIcon: "EX",
|
||||
website: "https://exa.ai",
|
||||
notice: {
|
||||
apiKeyUrl: "https://dashboard.exa.ai/api-keys"
|
||||
}
|
||||
},
|
||||
category: "apikey",
|
||||
authType: "apikey",
|
||||
serviceKinds: [
|
||||
"webSearch",
|
||||
"webFetch"
|
||||
],
|
||||
searchConfig: {
|
||||
baseUrl: "https://api.exa.ai/search",
|
||||
method: "POST",
|
||||
authType: "apikey",
|
||||
authHeader: "x-api-key",
|
||||
costPerQuery: 0.007,
|
||||
freeMonthlyQuota: 1000,
|
||||
searchTypes: [
|
||||
"web",
|
||||
"news"
|
||||
],
|
||||
defaultMaxResults: 5,
|
||||
maxMaxResults: 100,
|
||||
timeoutMs: 10000,
|
||||
cacheTTLMs: 300000
|
||||
},
|
||||
fetchConfig: {
|
||||
baseUrl: "https://api.exa.ai/contents",
|
||||
method: "POST",
|
||||
authType: "apikey",
|
||||
authHeader: "x-api-key",
|
||||
costPerQuery: 0.001,
|
||||
freeMonthlyQuota: 1000,
|
||||
formats: [
|
||||
"text",
|
||||
"markdown"
|
||||
],
|
||||
maxCharacters: 100000,
|
||||
timeoutMs: 15000
|
||||
}
|
||||
};
|
||||
34
open-sse/providers/registry/fal-ai.js
Normal file
34
open-sse/providers/registry/fal-ai.js
Normal 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" },
|
||||
};
|
||||
34
open-sse/providers/registry/firecrawl.js
Normal file
34
open-sse/providers/registry/firecrawl.js
Normal file
@@ -0,0 +1,34 @@
|
||||
export default {
|
||||
id: "firecrawl",
|
||||
alias: "firecrawl",
|
||||
display: {
|
||||
name: "Firecrawl",
|
||||
icon: "local_fire_department",
|
||||
color: "#F59E0B",
|
||||
textIcon: "FC",
|
||||
website: "https://firecrawl.dev",
|
||||
notice: {
|
||||
apiKeyUrl: "https://www.firecrawl.dev/app/api-keys"
|
||||
}
|
||||
},
|
||||
category: "apikey",
|
||||
authType: "apikey",
|
||||
serviceKinds: [
|
||||
"webFetch"
|
||||
],
|
||||
fetchConfig: {
|
||||
baseUrl: "https://api.firecrawl.dev/v1/scrape",
|
||||
method: "POST",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
costPerQuery: 0.002,
|
||||
freeMonthlyQuota: 500,
|
||||
formats: [
|
||||
"markdown",
|
||||
"html",
|
||||
"text"
|
||||
],
|
||||
maxCharacters: 200000,
|
||||
timeoutMs: 30000
|
||||
}
|
||||
};
|
||||
29
open-sse/providers/registry/fireworks.js
Normal file
29
open-sse/providers/registry/fireworks.js
Normal 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" },
|
||||
};
|
||||
58
open-sse/providers/registry/gemini-cli.js
Normal file
58
open-sse/providers/registry/gemini-cli.js
Normal 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,
|
||||
},
|
||||
};
|
||||
80
open-sse/providers/registry/gemini.js
Normal file
80
open-sse/providers/registry/gemini.js
Normal 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.",
|
||||
},
|
||||
};
|
||||
82
open-sse/providers/registry/github.js
Normal file
82
open-sse/providers/registry/github.js
Normal 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,
|
||||
},
|
||||
};
|
||||
32
open-sse/providers/registry/gitlab.js
Normal file
32
open-sse/providers/registry/gitlab.js
Normal 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",
|
||||
},
|
||||
};
|
||||
35
open-sse/providers/registry/glm-cn.js
Normal file
35
open-sse/providers/registry/glm-cn.js
Normal 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,
|
||||
},
|
||||
};
|
||||
58
open-sse/providers/registry/glm.js
Normal file
58
open-sse/providers/registry/glm.js
Normal 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,
|
||||
},
|
||||
};
|
||||
35
open-sse/providers/registry/google-pse.js
Normal file
35
open-sse/providers/registry/google-pse.js
Normal file
@@ -0,0 +1,35 @@
|
||||
export default {
|
||||
id: "google-pse",
|
||||
alias: "gpse",
|
||||
display: {
|
||||
name: "Google PSE",
|
||||
icon: "search",
|
||||
color: "#4285F4",
|
||||
textIcon: "GP",
|
||||
website: "https://programmablesearchengine.google.com",
|
||||
notice: {
|
||||
apiKeyUrl: "https://programmablesearchengine.google.com/controlpanel/create"
|
||||
}
|
||||
},
|
||||
category: "apikey",
|
||||
authType: "apikey",
|
||||
serviceKinds: [
|
||||
"webSearch"
|
||||
],
|
||||
searchConfig: {
|
||||
baseUrl: "https://www.googleapis.com/customsearch/v1",
|
||||
method: "GET",
|
||||
authType: "apikey",
|
||||
authHeader: "key",
|
||||
costPerQuery: 0.005,
|
||||
freeMonthlyQuota: 3000,
|
||||
searchTypes: [
|
||||
"web",
|
||||
"news"
|
||||
],
|
||||
defaultMaxResults: 5,
|
||||
maxMaxResults: 10,
|
||||
timeoutMs: 10000,
|
||||
cacheTTLMs: 300000
|
||||
}
|
||||
};
|
||||
24
open-sse/providers/registry/google-tts.js
Normal file
24
open-sse/providers/registry/google-tts.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export default {
|
||||
id: "google-tts",
|
||||
alias: "google-tts",
|
||||
display: {
|
||||
name: "Google TTS",
|
||||
icon: "record_voice_over",
|
||||
color: "#4285F4",
|
||||
textIcon: "GT"
|
||||
},
|
||||
category: "freeTier",
|
||||
authType: "none",
|
||||
serviceKinds: [
|
||||
"tts"
|
||||
],
|
||||
mediaPriority: 5,
|
||||
noAuth: true,
|
||||
ttsConfig: {
|
||||
baseUrl: "google-tts",
|
||||
authType: "none",
|
||||
authHeader: "none",
|
||||
format: "google-tts",
|
||||
models: []
|
||||
}
|
||||
};
|
||||
39
open-sse/providers/registry/grok-web.js
Normal file
39
open-sse/providers/registry/grok-web.js
Normal 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
Reference in New Issue
Block a user