Merge branch 'master' of https://github.com/decolua/9router
Some checks failed
Deploy GitBook to 9router.github.io / build-deploy (push) Has been cancelled
Some checks failed
Deploy GitBook to 9router.github.io / build-deploy (push) Has been cancelled
# Conflicts: # open-sse/utils/usageTracking.js
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -72,3 +72,6 @@ ecosystem.config.*
|
|||||||
scripts/agSniffer/*
|
scripts/agSniffer/*
|
||||||
gitbooks/*
|
gitbooks/*
|
||||||
gitbook/README.md
|
gitbook/README.md
|
||||||
|
|
||||||
|
# Refactor backup reference (do not bundle/lint)
|
||||||
|
open-sse.old/
|
||||||
|
|||||||
132
CHANGELOG.md
132
CHANGELOG.md
@@ -1,3 +1,104 @@
|
|||||||
|
# v0.5.12 (2026-06-26)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Add token-saver dashboard page — decolua
|
||||||
|
- Add bulk delete for provider connections — teddytkz
|
||||||
|
- Resolve GitHub Copilot model catalog from upstream — caiqinzhou
|
||||||
|
- Add Venice AI provider — Brokenc0de
|
||||||
|
- Add Kiro external_idp import for Microsoft SSO (CLIProxyAPI) — Stevanus Pangau
|
||||||
|
- Overhaul Blackbox provider catalog + WebUI test support — suryacagur
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
- Provider thinking compatibility (DeepSeek/Gemini) — Mink Nguyen
|
||||||
|
- Stop double-counting streaming usage at source — decolua
|
||||||
|
- Usage logging dedupe to reduce stats churn — Mink Nguyen
|
||||||
|
- Prevent non-JSON SSE lines / duplicate [DONE] from breaking clients (PR #2046) — qianze
|
||||||
|
- Resolve Gemini TTS models from catalog — nguyenha935
|
||||||
|
- Support Kiro IDC (organization) token import — quanturbo
|
||||||
|
- Preserve forced streaming for JSON clients (#2031) — Joseph Yaksich
|
||||||
|
- Preserve Responses text format (Codex) — tenglong
|
||||||
|
- Support Gemini native TTS generateContent endpoint — nguyenha935
|
||||||
|
- Add missing zh-CN endpoint key label (i18n) — weimaozhen
|
||||||
|
- CodeBuddy: only send reasoning params when client requests reasoning (#2071) — Rex
|
||||||
|
- Show custom provider models in combo picker — Sapto
|
||||||
|
- Docker: add docker-compose.yml with headroom enabled by default — nitsuahlabs
|
||||||
|
- Clarify token diagnostics vs provider billing (headroom, #1998) — Sutarto Jordan Chrisfivo
|
||||||
|
- Translate openai-responses input through OpenAI for compression (#1998) — Ankit
|
||||||
|
- Kiro: report 1M context window for claude-opus-4.8 — EdisonPVE
|
||||||
|
- Avoid stale redirects after auth changes (#2100) — Emirhan
|
||||||
|
- Mark Claude Opus 4.7 (dashed id) as 1M context — Brokenc0de
|
||||||
|
- Preserve reasoning effort through Codex translations — ntdung6868
|
||||||
|
- Token-saver: full width card layout — decolua
|
||||||
|
- Antigravity: retry transient upstream failures — Sutarto Jordan Chrisfivo
|
||||||
|
- Param-support: handle strip rules without match/drop (#1960) — Joseph Yaksich
|
||||||
|
- Translator: resolve custom provider prefix in debug endpoint (#1083) — hamsa0x7
|
||||||
|
|
||||||
|
# v0.5.8 (2026-06-21)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- **Antigravity**: native image generation support (image models tagged kind:image, hiển thị trong media-providers UI)
|
||||||
|
- **CodeBuddy CN**: API key auth + credit quota tracker
|
||||||
|
- **CodeBuddy CN**: short model prefix alias "cbcn"
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
- **MiniMax-M3**: enable vision capability
|
||||||
|
- **Headroom**: support Docker sidecar proxy
|
||||||
|
- **Antigravity**: image executor fixes
|
||||||
|
- **mimo-free**: Chrome User-Agent rotation to bypass anti-abuse gate
|
||||||
|
- **cloudflare-ai**: flatten content-part arrays to string to avoid oneOf 400 (#1926)
|
||||||
|
- **Translator**: normalize tools to Anthropic-native shape for non-Anthropic providers
|
||||||
|
- **CLI**: handle Next.js 16 nested standalone output path (#1940)
|
||||||
|
- **Codex**: preserve custom tools during request normalization
|
||||||
|
- **next.config**: add new route for responses endpoint to API
|
||||||
|
|
||||||
|
# v0.5.6 (2026-06-20)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- **Ponytail**: minimalist code generation feature
|
||||||
|
- **Headroom**: proxy lifecycle management + dashboard UI (one-click start/stop, install detection, status probing, token saver, claude↔openai shape conversion)
|
||||||
|
- **CodeBuddy CN**: new OAuth provider (copilot.tencent.com) — 15-model catalog, /v2 inference, forced streaming, OpenAI-style reasoning
|
||||||
|
- **OpenCode-Go**: align models with official endpoints; route Qwen 3.7 MiniMax via /v1/messages, GLM/Kimi/DeepSeek/MiMo via /chat/completions
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
- **Anthropic-compatible validation**: use POST /v1/messages (GET /models not spec, false "invalid" for valid keys)
|
||||||
|
- **CLI tools**: tolerate JSONC configs in all 8 settings routes (opencode, openclaw, kilo, droid, cowork, copilot, claude, cline)
|
||||||
|
- **Gemini/Antigravity**: preserve 'pattern' in tool schema translation (glob/grep)
|
||||||
|
- **Combo/Fusion**: flatten Anthropic-style tool messages in panel calls (prevent 503)
|
||||||
|
- **Models**: store provider custom models by provider scope
|
||||||
|
- **Perplexity**: use /v1/models endpoint for key validation
|
||||||
|
|
||||||
|
# v0.5.4 (2026-06-18)
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
- **Kiro**: honor thinking effort budgets
|
||||||
|
- **AG/Kiro/Xiaomi**: provider fixes
|
||||||
|
- **Combo/Fusion**: flatten tool history in panel calls to prevent 503
|
||||||
|
- **LLM selector**: show custom vision models in selector and model list
|
||||||
|
- **Image**: prevent compatible nodes from shadowing provider aliases
|
||||||
|
|
||||||
|
# v0.5.2 (2026-06-17)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- **Combo Fusion strategy** — fans the prompt out to all member models in parallel, then a configurable judge model synthesizes one final answer (quorum-grace, anonymized sources, graceful degradation)
|
||||||
|
- **Per-combo strategy selector** — pick `fallback` / `round-robin` / `fusion` / `capacity` per combo (replaces the old round-robin toggle), with a judge picker for fusion
|
||||||
|
- **Capacity auto-switch** — reorders models per request so images/PDFs route to capable models first
|
||||||
|
- **Kiro headless API-key auth** (`ksk_`) + direct `claude↔kiro` route that avoids the lossy OpenAI two-hop pivot
|
||||||
|
- **Claude auto-ping** — warms the 5h quota window right after reset so a fresh window starts immediately (per-connection toggle)
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
- **Claude 429**: stop hammering the OAuth usage endpoint — cache resetAt, throttle quota refresh to 3 min, cool down after a 429 (chat unaffected)
|
||||||
|
- **Usage logs always empty**: missing `await` on `getAdapter()` in `getRecentLogs` made `/api/usage/logs` & `/api/usage/request-logs` return nothing
|
||||||
|
- **Executors**: strip params unsupported by the provider/model (drops deprecated `temperature` for claude-opus-4 → Anthropic 400)
|
||||||
|
- **Translator**: derive deterministic tool_call ids for gemini/antigravity → OpenAI so function call/response pair correctly (fixes tool-pairing 400s)
|
||||||
|
- **Antigravity**: strip `optional` from tool schemas before sending to Gemini
|
||||||
|
- **Claude-to-OpenAI**: handle OpenAI-format responses in the non-streaming path (e.g. xiaomi-tokenplan)
|
||||||
|
- **Usage views**: show edited connection names consistently across Providers & Quota Tracker
|
||||||
|
- **Security**: hardened reverse-proxy local-access trust
|
||||||
|
- **Security**: SSRF hardening on web fetch
|
||||||
|
|
||||||
|
## Internal
|
||||||
|
- Large **open-sse / translator refactor** (~40 commits): unified provider/model registry (LiteLLM-style `models[]` + `kind` field, 100 co-located registry files), single-sourced media/OAuth/refresh/token URLs, registry-based dispatch for usage & token-refresh, DRY translator concerns (buildUsage, encodeDataUri, finishReasonMap, chunkBuilder, reasoningDelta…), ESM-safe registry init, large-file splits, dead-code removal, and golden/no-regression test gates
|
||||||
|
|
||||||
# v0.4.80 (2026-06-13)
|
# v0.4.80 (2026-06-13)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@@ -220,34 +321,3 @@
|
|||||||
## Fixes
|
## Fixes
|
||||||
- fix(docker): restore `/app/server.js` (v0.4.38 regression)
|
- fix(docker): restore `/app/server.js` (v0.4.38 regression)
|
||||||
|
|
||||||
# v0.4.38 (2026-05-13)
|
|
||||||
|
|
||||||
## Features
|
|
||||||
- Add DeepSeek TUI as CLI tool in dashboard (#1088)
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
- Fix broken Docker image in v0.4.36/v0.4.37 (#1096, #1097)
|
|
||||||
|
|
||||||
## Improvements
|
|
||||||
- Clean Docker tags + clearer pulls badge
|
|
||||||
|
|
||||||
# v0.4.37 (2026-05-13)
|
|
||||||
|
|
||||||
## Improvements
|
|
||||||
- Security hardening — upgrade recommended
|
|
||||||
|
|
||||||
# v0.4.36 (2026-05-13)
|
|
||||||
|
|
||||||
## Features
|
|
||||||
- Add MiniMax TTS provider support (#1043)
|
|
||||||
- Docker images now published on both Docker Hub (`decolua/9router`) and GHCR — pull from your preferred registry
|
|
||||||
|
|
||||||
## Improvements
|
|
||||||
- Replace browser confirm dialogs with custom ConfirmModal (#1060)
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
- Fix Docker `Cannot find module 'next'` error in standalone build
|
|
||||||
- Restore /app/server.js in Docker standalone build (#1064, #1067)
|
|
||||||
- Fix CLI TUI menu arrow-key escape sequences leaking (^[[A^[[B)
|
|
||||||
- Switch macOS/Linux tray to systray2 fork (fixes Kaspersky AV false-positive) (#1080)
|
|
||||||
- Fix zoom controls contrast in topology view (#1066)
|
|
||||||
28
DOCKER.md
28
DOCKER.md
@@ -64,6 +64,34 @@ docker run -d \
|
|||||||
decolua/9router:latest
|
decolua/9router:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Optional Headroom sidecar
|
||||||
|
|
||||||
|
The 9Router image does not bundle Python or Headroom. To use Headroom in Docker, run it as a separate service and point 9Router at that proxy:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
9router:
|
||||||
|
image: decolua/9router:latest
|
||||||
|
ports:
|
||||||
|
- "20128:20128"
|
||||||
|
volumes:
|
||||||
|
- "$HOME/.9router:/app/data"
|
||||||
|
environment:
|
||||||
|
DATA_DIR: /app/data
|
||||||
|
HEADROOM_URL: http://headroom:8787
|
||||||
|
depends_on:
|
||||||
|
- headroom
|
||||||
|
|
||||||
|
headroom:
|
||||||
|
image: ghcr.io/chopratejas/headroom:latest
|
||||||
|
ports:
|
||||||
|
- "8787:8787"
|
||||||
|
```
|
||||||
|
|
||||||
|
In the dashboard, open `Endpoint` → `Token Saver` → `Headroom`, confirm the URL is `http://headroom:8787`, recheck status, then enable Headroom.
|
||||||
|
|
||||||
|
If Headroom runs on the Docker host instead of as a sidecar, use `http://host.docker.internal:8787` on macOS/Windows. On Linux, add `--add-host=host.docker.internal:host-gateway` or the equivalent compose `extra_hosts` entry.
|
||||||
|
|
||||||
## Update to latest
|
## Update to latest
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
56
README.md
56
README.md
@@ -170,7 +170,7 @@ Default URLs:
|
|||||||
<sub>FREE OpenClaw + Claude Opus 4.6<br/>by <a href="https://www.youtube.com/@BuildAIWithHamid">Build AI With Hamid</a></sub>
|
<sub>FREE OpenClaw + Claude Opus 4.6<br/>by <a href="https://www.youtube.com/@BuildAIWithHamid">Build AI With Hamid</a></sub>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" width="320">
|
<td align="center" width="320">
|
||||||
<a href="https://www.youtube.com/watch?v=CkVZZUSTXAI">
|
<a href="https://www.youtube.com/watch?v=CkVZZUSTXAI">
|
||||||
<img src="https://img.youtube.com/vi/CkVZZUSTXAI/mqdefault.jpg" alt="Claude CLI Free Setup" width="300"/>
|
<img src="https://img.youtube.com/vi/CkVZZUSTXAI/mqdefault.jpg" alt="Claude CLI Free Setup" width="300"/>
|
||||||
@@ -178,6 +178,13 @@ Default URLs:
|
|||||||
<b>🇮🇩 Indonesia</b><br/>
|
<b>🇮🇩 Indonesia</b><br/>
|
||||||
<sub>Koding 24 Jam Anti Rate Limit! Hemat Token AI 65% | Tutorial Quick Setup 9Router 🚀<br/>by <a href="https://www.youtube.com/@krisswuh">Krisswuh</a></sub>
|
<sub>Koding 24 Jam Anti Rate Limit! Hemat Token AI 65% | Tutorial Quick Setup 9Router 🚀<br/>by <a href="https://www.youtube.com/@krisswuh">Krisswuh</a></sub>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" width="320">
|
||||||
|
<a href="https://www.youtube.com/watch?v=TXGv4eofe1I">
|
||||||
|
<img src="https://img.youtube.com/vi/TXGv4eofe1I/mqdefault.jpg" alt="Cara Deploy 9Router di Hugging Face GRATIS Non-Stop! | Alternatif VPS RAM 16GB" width="300"/>
|
||||||
|
</a><br/>
|
||||||
|
<b>🇮🇩 Indonesia</b><br/>
|
||||||
|
<sub>Cara Deploy 9Router di Hugging Face GRATIS Non-Stop! | Alternatif VPS RAM 16GB<br/>by <a href="https://www.youtube.com/@krisswuh">Krisswuh</a></sub>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -400,7 +407,9 @@ Default URLs:
|
|||||||
| Feature | What It Does | Why It Matters |
|
| Feature | What It Does | Why It Matters |
|
||||||
|---------|--------------|----------------|
|
|---------|--------------|----------------|
|
||||||
| 🚀 **RTK Token Saver** ([RTK](https://github.com/rtk-ai/rtk) ⭐40K) | Compress tool outputs (`git diff`, `grep`, `ls`, `tree`...) before sending to LLM | Save **20-40% input tokens** per request |
|
| 🚀 **RTK Token Saver** ([RTK](https://github.com/rtk-ai/rtk) ⭐40K) | Compress tool outputs (`git diff`, `grep`, `ls`, `tree`...) before sending to LLM | Save **20-40% input tokens** per request |
|
||||||
|
| 🧠 **Headroom Token Saver** ([Headroom](https://github.com/chopratejas/headroom)) | Optional external `/v1/compress` proxy before provider routing | Save more context tokens without changing clients |
|
||||||
| 🪨 **Caveman Mode** ([Caveman](https://github.com/JuliusBrussee/caveman) ⭐52K) | Inject caveman-speak prompt → LLM replies terse, technical substance preserved | Save **up to 65% output tokens** |
|
| 🪨 **Caveman Mode** ([Caveman](https://github.com/JuliusBrussee/caveman) ⭐52K) | Inject caveman-speak prompt → LLM replies terse, technical substance preserved | Save **up to 65% output tokens** |
|
||||||
|
| 🐴 **Ponytail** ([Ponytail](https://github.com/DietrichGebert/ponytail)) | Inject "lazy senior dev" prompt → LLM writes minimal, YAGNI-first code (Lite/Full/Ultra) | **Fewer output tokens, less refactoring** |
|
||||||
| 🎯 **Smart 3-Tier Fallback** | Auto-route: Subscription → Cheap → Free | Never stop coding, zero downtime |
|
| 🎯 **Smart 3-Tier Fallback** | Auto-route: Subscription → Cheap → Free | Never stop coding, zero downtime |
|
||||||
| 📊 **Real-Time Quota Tracking** | Live token count + reset countdown | Maximize subscription value |
|
| 📊 **Real-Time Quota Tracking** | Live token count + reset countdown | Maximize subscription value |
|
||||||
| 🔄 **Format Translation** | OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro ↔ Vertex | Works with any CLI tool |
|
| 🔄 **Format Translation** | OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro ↔ Vertex | Works with any CLI tool |
|
||||||
@@ -430,6 +439,50 @@ Without RTK: 47K tokens sent to LLM
|
|||||||
With RTK: 28K tokens sent to LLM (40% saved · same context · same answer)
|
With RTK: 28K tokens sent to LLM (40% saved · same context · same answer)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 🧠 Headroom Token Saver
|
||||||
|
|
||||||
|
Headroom is optional and runs separately. 9Router calls Headroom's local `/v1/compress` endpoint, then keeps normal routing, fallback, auth, and usage tracking:
|
||||||
|
|
||||||
|
```
|
||||||
|
Client → 9Router → Headroom /v1/compress → 9Router → provider
|
||||||
|
```
|
||||||
|
|
||||||
|
Local setup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install "headroom-ai[proxy]"
|
||||||
|
headroom proxy --port 8787
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable in Dashboard → Endpoint → Token Saver → Headroom. Default URL: `http://localhost:8787`.
|
||||||
|
|
||||||
|
Docker examples:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Headroom service in same Docker network
|
||||||
|
http://headroom:8787
|
||||||
|
|
||||||
|
# Headroom running on host machine
|
||||||
|
http://host.docker.internal:8787
|
||||||
|
```
|
||||||
|
|
||||||
|
If Headroom is down or returns an error, 9Router fails open and sends the original request.
|
||||||
|
|
||||||
|
### 🐴 Ponytail (Lazy Senior Dev)
|
||||||
|
|
||||||
|
Ponytail injects a *"lazy senior dev"* system prompt into every request, biasing the LLM toward minimal, YAGNI-first code — deletion over addition, stdlib over new deps, one-liners over abstractions. Adapted from [DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail).
|
||||||
|
|
||||||
|
- **Lite** — Build what's asked, name the lazier alternative.
|
||||||
|
- **Full** — YAGNI ladder enforced: stdlib → native → existing deps → one-liner → minimal code.
|
||||||
|
- **Ultra** — YAGNI extremist: deletion first, ship the one-liner, challenge the rest of the requirement in the same response.
|
||||||
|
|
||||||
|
```
|
||||||
|
Without Ponytail: verbose code, extra abstractions, "just in case" scaffolding
|
||||||
|
With Ponytail: shortest working diff, no unrequested abstractions, fewer tokens
|
||||||
|
```
|
||||||
|
|
||||||
|
Never trades away: input validation, error handling that prevents data loss, security, accessibility, or anything explicitly requested. Enable in Dashboard → Endpoint → Ponytail. Stacks with Caveman (output terseness) and RTK (input compression).
|
||||||
|
|
||||||
### 🎯 Smart 3-Tier Fallback
|
### 🎯 Smart 3-Tier Fallback
|
||||||
|
|
||||||
Create combos with automatic fallback:
|
Create combos with automatic fallback:
|
||||||
@@ -1311,6 +1364,7 @@ Built on the shoulders of giants:
|
|||||||
- **[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)** — original Go implementation that inspired this JavaScript port.
|
- **[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)** — original Go implementation that inspired this JavaScript port.
|
||||||
- **[RTK](https://github.com/rtk-ai/rtk)**  — Rust token-saver. 9Router ports its compression pipeline to JS → **−20-40% input tokens** on every request.
|
- **[RTK](https://github.com/rtk-ai/rtk)**  — Rust token-saver. 9Router ports its compression pipeline to JS → **−20-40% input tokens** on every request.
|
||||||
- **[Caveman](https://github.com/JuliusBrussee/caveman)**  by **[@JuliusBrussee](https://github.com/JuliusBrussee)** — viral *"why use many token when few token do trick"*. 9Router adapts its prompt → **−65% output tokens**.
|
- **[Caveman](https://github.com/JuliusBrussee/caveman)**  by **[@JuliusBrussee](https://github.com/JuliusBrussee)** — viral *"why use many token when few token do trick"*. 9Router adapts its prompt → **−65% output tokens**.
|
||||||
|
- **[Ponytail](https://github.com/DietrichGebert/ponytail)**  by **[@DietrichGebert](https://github.com/DietrichGebert)** — *"lazy senior dev"* skill. 9Router injects its YAGNI-first ladder → **fewer tokens, less code, shorter diffs**.
|
||||||
|
|
||||||
Huge thanks to these authors — without their work, 9Router's token-saving features wouldn't exist. ⭐ them on GitHub!
|
Huge thanks to these authors — without their work, 9Router's token-saving features wouldn't exist. ⭐ them on GitHub!
|
||||||
|
|
||||||
|
|||||||
24
cli/cli.js
24
cli/cli.js
@@ -61,6 +61,21 @@ const INSTALL_CMD_LATEST = `npm i -g ${APP_NAME}@latest --prefer-online`;
|
|||||||
|
|
||||||
const DEFAULT_PORT = 20128;
|
const DEFAULT_PORT = 20128;
|
||||||
const DEFAULT_HOST = "0.0.0.0";
|
const DEFAULT_HOST = "0.0.0.0";
|
||||||
|
|
||||||
|
// First non-internal IPv4 — the address remote peers actually reach when bound to 0.0.0.0.
|
||||||
|
function getLanIp() {
|
||||||
|
for (const ifaces of Object.values(os.networkInterfaces())) {
|
||||||
|
for (const i of ifaces || []) {
|
||||||
|
if (i.family === "IPv4" && !i.internal) return i.address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local URL stays "localhost"; warn separately when bound to all interfaces (network-exposed).
|
||||||
|
function getDisplayHost() {
|
||||||
|
return host === DEFAULT_HOST ? "localhost" : host;
|
||||||
|
}
|
||||||
const MAX_PORT_ATTEMPTS = 10;
|
const MAX_PORT_ATTEMPTS = 10;
|
||||||
// Identifiers for killAllAppProcesses - only kill 9router specifically
|
// Identifiers for killAllAppProcesses - only kill 9router specifically
|
||||||
const PROCESS_IDENTIFIERS = [
|
const PROCESS_IDENTIFIERS = [
|
||||||
@@ -501,7 +516,7 @@ async function showInterfaceMenu(latestVersion) {
|
|||||||
|
|
||||||
clearScreen();
|
clearScreen();
|
||||||
|
|
||||||
const displayHost = host === DEFAULT_HOST ? "localhost" : host;
|
const displayHost = getDisplayHost();
|
||||||
|
|
||||||
// Detect tunnel/local mode for server URL display
|
// Detect tunnel/local mode for server URL display
|
||||||
let serverUrl;
|
let serverUrl;
|
||||||
@@ -542,8 +557,13 @@ const MAX_RESTARTS = 2;
|
|||||||
const RESTART_RESET_MS = 30000; // Reset counter if alive > 30s
|
const RESTART_RESET_MS = 30000; // Reset counter if alive > 30s
|
||||||
|
|
||||||
function startServer(latestVersion) {
|
function startServer(latestVersion) {
|
||||||
const displayHost = host === DEFAULT_HOST ? "localhost" : host;
|
const displayHost = getDisplayHost();
|
||||||
const url = `http://${displayHost}:${port}/dashboard`;
|
const url = `http://${displayHost}:${port}/dashboard`;
|
||||||
|
// Surface real network exposure when bound to all interfaces (default 0.0.0.0).
|
||||||
|
if (host === DEFAULT_HOST) {
|
||||||
|
const lanIp = getLanIp();
|
||||||
|
if (lanIp) console.log(`\x1b[33m⚠ Network-exposed: reachable at http://${lanIp}:${port} (bound 0.0.0.0). Use --host 127.0.0.1 for local-only.\x1b[0m`);
|
||||||
|
}
|
||||||
|
|
||||||
let restartCount = 0;
|
let restartCount = 0;
|
||||||
let serverStartTime = Date.now();
|
let serverStartTime = Date.now();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const os = require("os");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const BETTER_SQLITE3_VERSION = "12.6.2";
|
const BETTER_SQLITE3_VERSION = "12.6.2";
|
||||||
|
const SQL_JS_VERSION = "1.14.1";
|
||||||
|
|
||||||
function getDataDir() {
|
function getDataDir() {
|
||||||
if (process.env.DATA_DIR) return process.env.DATA_DIR;
|
if (process.env.DATA_DIR) return process.env.DATA_DIR;
|
||||||
@@ -102,20 +103,36 @@ function npmInstall(pkgs, opts = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Public: ensure better-sqlite3 native module is installed in user-writable
|
// Public: ensure better-sqlite3 native module is installed in user-writable
|
||||||
// runtime dir. sql.js is bundled in bin/app already; node:sqlite is built-in.
|
// runtime dir. sql.js may be bundled in bin/app, but npm publish strips .wasm
|
||||||
// This is purely a *speed optimization* — app works without it via fallbacks.
|
// from nested node_modules — verify and reinstall if missing. node:sqlite is
|
||||||
|
// built-in. This is purely a *speed optimization* — app works without
|
||||||
|
// better-sqlite3 via fallbacks.
|
||||||
|
function isSqlJsWasmValid() {
|
||||||
|
const bundledWasm = path.join(__dirname, "..", "app", "node_modules", "sql.js", "dist", "sql-wasm.wasm");
|
||||||
|
if (fs.existsSync(bundledWasm)) return true;
|
||||||
|
const runtimeWasm = path.join(getRuntimeNodeModules(), "sql.js", "dist", "sql-wasm.wasm");
|
||||||
|
return fs.existsSync(runtimeWasm);
|
||||||
|
}
|
||||||
|
|
||||||
function ensureSqliteRuntime({ silent = false } = {}) {
|
function ensureSqliteRuntime({ silent = false } = {}) {
|
||||||
ensureRuntimeDir();
|
ensureRuntimeDir();
|
||||||
|
|
||||||
|
let sqlJsOk = isSqlJsWasmValid();
|
||||||
|
if (!sqlJsOk) {
|
||||||
|
sqlJsOk = npmInstall([`sql.js@${SQL_JS_VERSION}`], { silent });
|
||||||
|
if (sqlJsOk) sqlJsOk = isSqlJsWasmValid();
|
||||||
|
}
|
||||||
|
|
||||||
const needBetterSqlite = !hasModule("better-sqlite3") || !isBetterSqliteBinaryValid();
|
const needBetterSqlite = !hasModule("better-sqlite3") || !isBetterSqliteBinaryValid();
|
||||||
if (!needBetterSqlite) {
|
if (!needBetterSqlite) {
|
||||||
if (!silent) console.log("✅ SQLite engine ready");
|
if (!silent) console.log("✅ SQLite engine ready");
|
||||||
return { betterSqlite: true };
|
return { betterSqlite: true, sqlJs: sqlJsOk };
|
||||||
}
|
}
|
||||||
|
|
||||||
const ok = npmInstall([`better-sqlite3@${BETTER_SQLITE3_VERSION}`], { optional: true, silent });
|
const ok = npmInstall([`better-sqlite3@${BETTER_SQLITE3_VERSION}`], { optional: true, silent });
|
||||||
return {
|
return {
|
||||||
betterSqlite: ok && hasModule("better-sqlite3") && isBetterSqliteBinaryValid(),
|
betterSqlite: ok && hasModule("better-sqlite3") && isBetterSqliteBinaryValid(),
|
||||||
|
sqlJs: sqlJsOk,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "9router",
|
"name": "9router",
|
||||||
"version": "0.4.80",
|
"version": "0.5.12",
|
||||||
"description": "9Router CLI - Start and manage 9Router server",
|
"description": "9Router CLI - Start and manage 9Router server",
|
||||||
"bin": {
|
"bin": {
|
||||||
"9router": "./cli.js"
|
"9router": "./cli.js"
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ const EXCLUDE_PATTERNS = [
|
|||||||
"@img", // Sharp image processing (not needed with unoptimized images)
|
"@img", // Sharp image processing (not needed with unoptimized images)
|
||||||
"sharp", // Sharp core lib (not needed with unoptimized images)
|
"sharp", // Sharp core lib (not needed with unoptimized images)
|
||||||
"detect-libc", // Sharp dependency
|
"detect-libc", // Sharp dependency
|
||||||
"logs", // Runtime logs
|
|
||||||
".env", // Environment files
|
".env", // Environment files
|
||||||
".env.local",
|
".env.local",
|
||||||
".env.*.local",
|
".env.*.local",
|
||||||
@@ -136,7 +135,15 @@ console.log("✅ Cleaned\n");
|
|||||||
console.log("3️⃣ Copying Next.js standalone build to app/cli/app...");
|
console.log("3️⃣ Copying Next.js standalone build to app/cli/app...");
|
||||||
const standaloneRoot = path.join(appDir, ".next", "standalone");
|
const standaloneRoot = path.join(appDir, ".next", "standalone");
|
||||||
const standaloneRootResolved = path.join(buildDistDir, "standalone");
|
const standaloneRootResolved = path.join(buildDistDir, "standalone");
|
||||||
const standaloneRootToUse = fs.existsSync(standaloneRootResolved) ? standaloneRootResolved : standaloneRoot;
|
let standaloneRootToUse = fs.existsSync(standaloneRootResolved) ? standaloneRootResolved : standaloneRoot;
|
||||||
|
// Next.js 16 nests standalone output under the project name when NEXT_TRACING_ROOT_MODE=workspace
|
||||||
|
// e.g. .next-cli-build/standalone/9router/server.js
|
||||||
|
const pkgName = path.basename(appDir);
|
||||||
|
const nestedRoot = path.join(standaloneRootToUse, pkgName);
|
||||||
|
if (fs.existsSync(path.join(nestedRoot, "server.js")) && !fs.existsSync(path.join(standaloneRootToUse, "server.js"))) {
|
||||||
|
console.log(`ℹ️ Detected nested standalone output: ${pkgName}/`);
|
||||||
|
standaloneRootToUse = nestedRoot;
|
||||||
|
}
|
||||||
const standaloneApp = fs.existsSync(path.join(standaloneRootToUse, "server.js"))
|
const standaloneApp = fs.existsSync(path.join(standaloneRootToUse, "server.js"))
|
||||||
? standaloneRootToUse
|
? standaloneRootToUse
|
||||||
: path.join(standaloneRootToUse, "app");
|
: path.join(standaloneRootToUse, "app");
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ async function showSettingsMenu(breadcrumb = []) {
|
|||||||
// RTK section
|
// RTK section
|
||||||
const rtkOn = data?.settings?.rtkEnabled !== false;
|
const rtkOn = data?.settings?.rtkEnabled !== false;
|
||||||
lines.push(` RTK: ${rtkOn ? `${COLORS.green}ON${COLORS.reset}` : `${COLORS.red}OFF${COLORS.reset}`} ${COLORS.dim}(Token Saver)${COLORS.reset}`);
|
lines.push(` RTK: ${rtkOn ? `${COLORS.green}ON${COLORS.reset}` : `${COLORS.red}OFF${COLORS.reset}`} ${COLORS.dim}(Token Saver)${COLORS.reset}`);
|
||||||
|
const headroomOn = data?.settings?.headroomEnabled === true;
|
||||||
|
lines.push(` Headroom: ${headroomOn ? `${COLORS.green}ON${COLORS.reset}` : `${COLORS.red}OFF${COLORS.reset}`} ${COLORS.dim}(${data?.settings?.headroomUrl || "http://localhost:8787"})${COLORS.reset}`);
|
||||||
|
|
||||||
// Auth mode section
|
// Auth mode section
|
||||||
const authMode = data?.settings?.authMode || "password";
|
const authMode = data?.settings?.authMode || "password";
|
||||||
@@ -73,6 +75,13 @@ async function showSettingsMenu(breadcrumb = []) {
|
|||||||
},
|
},
|
||||||
action: async (d) => { await toggleRtk(d?.settings?.rtkEnabled !== false); return true; }
|
action: async (d) => { await toggleRtk(d?.settings?.rtkEnabled !== false); return true; }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: (d) => {
|
||||||
|
const on = d?.settings?.headroomEnabled === true;
|
||||||
|
return `Token Saver (Headroom): ${on ? "ON" : "OFF"} → toggle`;
|
||||||
|
},
|
||||||
|
action: async (d) => { await toggleHeadroom(d?.settings?.headroomEnabled === true); return true; }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "🔑 Reset Password to Default",
|
label: "🔑 Reset Password to Default",
|
||||||
action: async () => { await resetPassword(); return true; }
|
action: async () => { await resetPassword(); return true; }
|
||||||
@@ -160,6 +169,17 @@ async function toggleRtk(currentlyOn) {
|
|||||||
await pause();
|
await pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleHeadroom(currentlyOn) {
|
||||||
|
const next = !currentlyOn;
|
||||||
|
const result = await api.updateSettings({ headroomEnabled: next });
|
||||||
|
if (result.success) {
|
||||||
|
showStatus(`Headroom ${next ? "enabled" : "disabled"}`, "success");
|
||||||
|
} else {
|
||||||
|
showStatus(`Failed: ${result.error}`, "error");
|
||||||
|
}
|
||||||
|
await pause();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset dashboard password to default via server API (writes the live SQLite DB).
|
* Reset dashboard password to default via server API (writes the live SQLite DB).
|
||||||
* After reset, user can log in with the default password "123456".
|
* After reset, user can log in with the default password "123456".
|
||||||
|
|||||||
@@ -10,10 +10,20 @@ http.createServer = (...args) => {
|
|||||||
const rest = args.filter((a) => typeof a !== "function");
|
const rest = args.filter((a) => typeof a !== "function");
|
||||||
if (!handler) return origCreate(...args);
|
if (!handler) return origCreate(...args);
|
||||||
const wrapped = (req, res) => {
|
const wrapped = (req, res) => {
|
||||||
const ip = req.socket && req.socket.remoteAddress ? req.socket.remoteAddress : "";
|
const socketIp = req.socket && req.socket.remoteAddress ? req.socket.remoteAddress : "";
|
||||||
|
const xff = req.headers["x-forwarded-for"];
|
||||||
|
const xRealIp = req.headers["x-real-ip"];
|
||||||
|
const viaProxy = !!(xff || xRealIp);
|
||||||
|
const isLoopbackProxy = socketIp === "127.0.0.1" || socketIp === "::1" || socketIp === "::ffff:127.0.0.1";
|
||||||
|
// Trust forwarding headers only when the TCP peer is a local reverse proxy.
|
||||||
|
// Direct/public sockets remain keyed by the unspoofable peer address.
|
||||||
|
const proxyIp = xRealIp || (xff ? String(xff).split(",")[0].trim() : "");
|
||||||
|
const ip = isLoopbackProxy && proxyIp ? proxyIp : socketIp;
|
||||||
delete req.headers["x-9r-real-ip"];
|
delete req.headers["x-9r-real-ip"];
|
||||||
delete req.headers["x-forwarded-for"];
|
delete req.headers["x-forwarded-for"];
|
||||||
|
delete req.headers["x-9r-via-proxy"];
|
||||||
req.headers["x-9r-real-ip"] = ip;
|
req.headers["x-9r-real-ip"] = ip;
|
||||||
|
if (viaProxy) req.headers["x-9r-via-proxy"] = "1";
|
||||||
return handler(req, res);
|
return handler(req, res);
|
||||||
};
|
};
|
||||||
return origCreate(...rest, wrapped);
|
return origCreate(...rest, wrapped);
|
||||||
|
|||||||
30
docker-compose.yml
Normal file
30
docker-compose.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
services:
|
||||||
|
9router:
|
||||||
|
image: decolua/9router:latest
|
||||||
|
container_name: 9router
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "20128:20128"
|
||||||
|
volumes:
|
||||||
|
- 9router-data:/app/data
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
DATA_DIR: /app/data
|
||||||
|
PORT: "20128"
|
||||||
|
HOSTNAME: "0.0.0.0"
|
||||||
|
NODE_ENV: production
|
||||||
|
HEADROOM_URL: http://headroom:8787
|
||||||
|
depends_on:
|
||||||
|
- headroom
|
||||||
|
|
||||||
|
headroom:
|
||||||
|
image: ghcr.io/chopratejas/headroom:latest
|
||||||
|
container_name: headroom
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8787:8787"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
9router-data:
|
||||||
|
name: 9router-data
|
||||||
BIN
images/fusion-combo-ui.png
Normal file
BIN
images/fusion-combo-ui.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@@ -28,6 +28,8 @@ const nextConfig = {
|
|||||||
experimental: {
|
experimental: {
|
||||||
// #1529/#1572: LLM clients can send long context or base64 image payloads through /v1 rewrites.
|
// #1529/#1572: LLM clients can send long context or base64 image payloads through /v1 rewrites.
|
||||||
proxyClientMaxBodySize,
|
proxyClientMaxBodySize,
|
||||||
|
// Cache fetch responses across HMR refreshes for faster dev reloads.
|
||||||
|
serverComponentsHmrCache: true,
|
||||||
},
|
},
|
||||||
webpack: (config, { isServer }) => {
|
webpack: (config, { isServer }) => {
|
||||||
// Ignore fs/path modules in browser bundle
|
// Ignore fs/path modules in browser bundle
|
||||||
@@ -38,8 +40,12 @@ const nextConfig = {
|
|||||||
path: false,
|
path: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// Exclude logs, .next, gitbook subapp from watcher
|
// Exclude non-source dirs from watcher to reduce inotify load
|
||||||
config.watchOptions = { ...config.watchOptions, ignored: /[\\/](logs|\.next|gitbook|cli)[\\/]/ };
|
config.watchOptions = {
|
||||||
|
...config.watchOptions,
|
||||||
|
aggregateTimeout: 300,
|
||||||
|
ignored: /[\\/](node_modules|\.git|logs|\.next|\.next-cli-build|gitbook|cli|open-sse\.old|tests|docs)[\\/]/,
|
||||||
|
};
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
@@ -56,6 +62,18 @@ const nextConfig = {
|
|||||||
source: "/codex/:path*",
|
source: "/codex/:path*",
|
||||||
destination: "/api/v1/responses"
|
destination: "/api/v1/responses"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: "/responses",
|
||||||
|
destination: "/api/v1/responses"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/v1beta/:path*",
|
||||||
|
destination: "/api/v1beta/:path*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/v1beta",
|
||||||
|
destination: "/api/v1beta"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
source: "/v1/:path*",
|
source: "/v1/:path*",
|
||||||
destination: "/api/v1/:path*"
|
destination: "/api/v1/:path*"
|
||||||
|
|||||||
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 { platform, arch } from "os";
|
||||||
|
import { PROVIDERS, PROVIDER_OAUTH } from "./providers.js";
|
||||||
|
|
||||||
// === Gemini CLI ===
|
// === Gemini CLI === derive từ registry gemini-cli.transport
|
||||||
export const GEMINI_CLI_VERSION = "0.34.0";
|
export const GEMINI_CLI_VERSION = PROVIDERS["gemini-cli"]?.cliVersion;
|
||||||
export const GEMINI_CLI_API_CLIENT = "google-genai-sdk/1.41.0 gl-node/v22.19.0";
|
export const GEMINI_CLI_API_CLIENT = PROVIDERS["gemini-cli"]?.apiClient;
|
||||||
|
|
||||||
// Map Node arch to Gemini CLI arch string (x64/x86/arm64/...)
|
// Map Node arch to Gemini CLI arch string (x64/x86/arm64/...)
|
||||||
function geminiCLIArch() {
|
function geminiCLIArch() {
|
||||||
@@ -16,11 +17,13 @@ export function geminiCLIUserAgent(model = "unknown") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// === GitHub Copilot ===
|
// === GitHub Copilot ===
|
||||||
|
// Derive từ registry github.transport.copilot
|
||||||
|
const _ghCopilot = PROVIDERS.github?.copilot || {};
|
||||||
export const GITHUB_COPILOT = {
|
export const GITHUB_COPILOT = {
|
||||||
VSCODE_VERSION: "1.110.0",
|
VSCODE_VERSION: _ghCopilot.vscodeVersion,
|
||||||
COPILOT_CHAT_VERSION: "0.38.0",
|
COPILOT_CHAT_VERSION: _ghCopilot.chatVersion,
|
||||||
USER_AGENT: "GitHubCopilotChat/0.38.0",
|
USER_AGENT: _ghCopilot.userAgent,
|
||||||
API_VERSION: "2025-04-01",
|
API_VERSION: _ghCopilot.apiVersion,
|
||||||
};
|
};
|
||||||
|
|
||||||
// === Antigravity enums ===
|
// === 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 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**";
|
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)
|
// Derive từ registry oauth.refreshLeadMs
|
||||||
export const REFRESH_LEAD_MS = {
|
export const REFRESH_LEAD_MS = Object.fromEntries(
|
||||||
codex: 5 * 24 * 60 * 60 * 1000, // 5 days
|
Object.entries(PROVIDER_OAUTH).filter(([, o]) => o.refreshLeadMs).map(([id, o]) => [id, o.refreshLeadMs])
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
// OAuth endpoints
|
// OAuth endpoints
|
||||||
export const OAUTH_ENDPOINTS = {
|
export const OAUTH_ENDPOINTS = {
|
||||||
google: {
|
google: { token: "https://oauth2.googleapis.com/token", auth: "https://accounts.google.com/o/oauth2/auth" },
|
||||||
token: "https://oauth2.googleapis.com/token",
|
openai: { token: PROVIDER_OAUTH["codex"]?.tokenUrl, auth: PROVIDER_OAUTH["codex"]?.authorizeUrl },
|
||||||
auth: "https://accounts.google.com/o/oauth2/auth"
|
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 },
|
||||||
openai: {
|
iflow: { token: PROVIDER_OAUTH["iflow"]?.tokenUrl, auth: PROVIDER_OAUTH["iflow"]?.authorizeUrl },
|
||||||
token: "https://auth.openai.com/oauth/token",
|
github: { token: PROVIDER_OAUTH["github"]?.tokenUrl, auth: PROVIDER_OAUTH["github"]?.authorizeUrl, deviceCode: PROVIDER_OAUTH["github"]?.deviceCodeUrl },
|
||||||
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"
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate Kimi OAuth custom headers
|
// Generate Kimi OAuth custom headers
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
* fiction. The suffix is stripped before the request leaves this process.
|
* 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_AGENTIC_SUFFIX = "-agentic";
|
||||||
export const KIRO_THINKING_SUFFIX = "-thinking";
|
export const KIRO_THINKING_SUFFIX = "-thinking";
|
||||||
|
|
||||||
@@ -89,16 +92,48 @@ REMEMBER: When in doubt, write LESS per operation. Multiple small operations > o
|
|||||||
`.trim();
|
`.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):
|
* Reuses the shared thinkingUnified parser (extractThinking) so every client
|
||||||
* - HTTP header `Anthropic-Beta: ...interleaved-thinking...`
|
* shape (Claude output_config.effort / thinking.budget_tokens, OpenAI
|
||||||
* - JSON `thinking.type === "enabled"` (Claude Messages API)
|
* reasoning_effort / reasoning.effort, Gemini, Qwen) maps consistently. Explicit
|
||||||
* - JSON `reasoning_effort` in {low, medium, high, auto} (OpenAI o1/o3)
|
* `none`/`off`/disabled wins and returns null (no prefix injected).
|
||||||
* - JSON `reasoning.effort` in {low, medium, high, auto} (OpenAI Responses)
|
* buildThinkingSystemPrefix performs Kiro's final 1..32000 clamp.
|
||||||
* - System prompt contains `<thinking_mode>enabled</thinking_mode>` or
|
*
|
||||||
* `<thinking_mode>interleaved</thinking_mode>` (AMP / Cursor)
|
* @param {object} body OpenAI/Claude-shaped request body
|
||||||
* - Model name contains `thinking` or `-reason`
|
* @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} body OpenAI-shaped request body (post-translation)
|
||||||
* @param {object} [headers] Original inbound HTTP headers (case-insensitive)
|
* @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}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function isThinkingEnabled(body, headers, model) {
|
export function isThinkingEnabled(body, headers, model) {
|
||||||
if (headers) {
|
return resolveKiroThinkingBudget(body, headers, model) !== null;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
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 { 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
|
export { PROVIDER_MODELS };
|
||||||
// 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)
|
|
||||||
|
|
||||||
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
|
// Helper functions
|
||||||
export function getProviderModels(aliasOrId) {
|
export function getProviderModels(aliasOrId) {
|
||||||
@@ -868,15 +35,14 @@ export function findModelName(aliasOrId, modelId) {
|
|||||||
export function getModelTargetFormat(aliasOrId, modelId) {
|
export function getModelTargetFormat(aliasOrId, modelId) {
|
||||||
const models = PROVIDER_MODELS[aliasOrId];
|
const models = PROVIDER_MODELS[aliasOrId];
|
||||||
if (!models) return null;
|
if (!models) return null;
|
||||||
const found = models.find(m => m.id === modelId);
|
return modelTargetFormat(models.find(m => m.id === modelId));
|
||||||
return found?.targetFormat || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getModelType(aliasOrId, modelId) {
|
export function getModelType(aliasOrId, modelId) {
|
||||||
const models = PROVIDER_MODELS[aliasOrId];
|
const models = PROVIDER_MODELS[aliasOrId];
|
||||||
if (!models) return null;
|
if (!models) return null;
|
||||||
const found = models.find(m => m.id === modelId);
|
const found = models.find(m => m.id === modelId);
|
||||||
return found?.type || null;
|
return found?.kind || found?.type || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getModelUpstreamId(aliasOrId, modelId) {
|
export function getModelUpstreamId(aliasOrId, modelId) {
|
||||||
@@ -891,30 +57,14 @@ export function getModelUpstreamId(aliasOrId, modelId) {
|
|||||||
|
|
||||||
export function getModelQuotaFamily(aliasOrId, modelId) {
|
export function getModelQuotaFamily(aliasOrId, modelId) {
|
||||||
const models = PROVIDER_MODELS[aliasOrId];
|
const models = PROVIDER_MODELS[aliasOrId];
|
||||||
const found = models?.find(m => m.id === modelId);
|
return modelQuotaFamily(models?.find(m => m.id === modelId));
|
||||||
return found?.quotaFamily || "normal";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OAuth providers that use short aliases (everything else: alias = id)
|
// OAuth short aliases — derived from registry `alias` (single source). everything else: alias = id.
|
||||||
const OAUTH_ALIASES = {
|
// vertex/vertex-partner keep alias=id (kept via the `|| id` fallback in consumers).
|
||||||
claude: "cc",
|
export const OAUTH_ALIASES = Object.fromEntries(
|
||||||
codex: "cx",
|
REGISTRY.filter(r => r.alias && r.alias !== r.id).map(r => [r.id, r.alias])
|
||||||
"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",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Derived from PROVIDERS — no need to maintain manually
|
// Derived from PROVIDERS — no need to maintain manually
|
||||||
export const PROVIDER_ID_TO_ALIAS = Object.fromEntries(
|
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)
|
// Get strip list for a model entry (explicit opt-in only)
|
||||||
// Returns array of content types to strip, e.g. ["image", "audio"]
|
// Returns array of content types to strip, e.g. ["image", "audio"]
|
||||||
export function getModelStrip(alias, modelId) {
|
export function getModelStrip(alias, modelId) {
|
||||||
const entry = PROVIDER_MODELS[alias]?.find(m => m.id === modelId);
|
return modelStrip(PROVIDER_MODELS[alias]?.find(m => m.id === modelId));
|
||||||
return entry?.strip || [];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,461 +1,6 @@
|
|||||||
import { platform, arch } from "os";
|
// Barrel: PROVIDERS now built from providers/registry (transport co-located with models)
|
||||||
|
import { PROVIDERS } from "../providers/index.js";
|
||||||
// === OS/Arch helpers ===
|
export { PROVIDERS, PROVIDER_OAUTH } from "../providers/index.js";
|
||||||
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" },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const OLLAMA_LOCAL_DEFAULT_HOST = "http://localhost:11434";
|
export const OLLAMA_LOCAL_DEFAULT_HOST = "http://localhost:11434";
|
||||||
|
|
||||||
@@ -464,12 +9,9 @@ export function resolveOllamaLocalHost(credentials) {
|
|||||||
return (raw || OLLAMA_LOCAL_DEFAULT_HOST).replace(/\/$/, "");
|
return (raw || OLLAMA_LOCAL_DEFAULT_HOST).replace(/\/$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export const XIAOMI_TOKENPLAN_REGIONS = {
|
// Region URLs single-source from registry xiaomi-tokenplan.transport
|
||||||
sgp: "https://token-plan-sgp.xiaomimimo.com/v1",
|
export const XIAOMI_TOKENPLAN_REGIONS = PROVIDERS["xiaomi-tokenplan"]?.regions || {};
|
||||||
cn: "https://token-plan-cn.xiaomimimo.com/v1",
|
export const XIAOMI_TOKENPLAN_DEFAULT_REGION = PROVIDERS["xiaomi-tokenplan"]?.defaultRegion;
|
||||||
ams: "https://token-plan-ams.xiaomimimo.com/v1"
|
|
||||||
};
|
|
||||||
export const XIAOMI_TOKENPLAN_DEFAULT_REGION = "sgp";
|
|
||||||
|
|
||||||
export function resolveXiaomiTokenplanBaseUrl(credentials) {
|
export function resolveXiaomiTokenplanBaseUrl(credentials) {
|
||||||
const region = credentials?.providerSpecificData?.region;
|
const region = credentials?.providerSpecificData?.region;
|
||||||
|
|||||||
@@ -31,11 +31,26 @@ export const MEMORY_CONFIG = {
|
|||||||
proxyDispatchersMaxSize: 20,
|
proxyDispatchersMaxSize: 20,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Stream stall timeout: abort if no chunk received within this duration
|
// Parse a positive integer env override, falling back to a default.
|
||||||
export const STREAM_STALL_TIMEOUT_MS = 60 * 1000;
|
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
|
// 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);
|
||||||
|
|
||||||
|
// Gemini native TTS fetch timeout: abort if Google does not return response headers in time.
|
||||||
|
export const GEMINI_NATIVE_TTS_FETCH_TIMEOUT_MS = envMs("GEMINI_NATIVE_TTS_FETCH_TIMEOUT_MS", 45 * 1000);
|
||||||
|
|
||||||
// Default token limits
|
// Default token limits
|
||||||
export const DEFAULT_MAX_TOKENS = 64000;
|
export const DEFAULT_MAX_TOKENS = 64000;
|
||||||
|
|||||||
@@ -96,10 +96,12 @@ export const TTS_MODELS_CONFIG = {
|
|||||||
},
|
},
|
||||||
gemini: {
|
gemini: {
|
||||||
models: [
|
models: [
|
||||||
|
{ id: "gemini-3.1-flash-tts-preview", name: "Gemini 3.1 Flash TTS", type: "tts" },
|
||||||
{ id: "gemini-2.5-flash-preview-tts", name: "Gemini 2.5 Flash TTS", type: "tts" },
|
{ id: "gemini-2.5-flash-preview-tts", name: "Gemini 2.5 Flash TTS", type: "tts" },
|
||||||
{ id: "gemini-2.5-pro-preview-tts", name: "Gemini 2.5 Pro TTS", type: "tts" },
|
{ id: "gemini-2.5-pro-preview-tts", name: "Gemini 2.5 Pro TTS", type: "tts" },
|
||||||
],
|
],
|
||||||
voices: {
|
voices: {
|
||||||
|
"gemini-3.1-flash-tts-preview": GEMINI_VOICES,
|
||||||
"gemini-2.5-flash-preview-tts": GEMINI_VOICES,
|
"gemini-2.5-flash-preview-tts": GEMINI_VOICES,
|
||||||
"gemini-2.5-pro-preview-tts": GEMINI_VOICES,
|
"gemini-2.5-pro-preview-tts": GEMINI_VOICES,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { BaseExecutor } from "./base.js";
|
|||||||
import { PROVIDERS } from "../config/providers.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 { 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 { 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 { 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}
|
// Sanitize function name: Gemini requires [a-zA-Z_][a-zA-Z0-9_.:\-]{0,63}
|
||||||
function sanitizeFunctionName(name) {
|
function sanitizeFunctionName(name) {
|
||||||
@@ -16,8 +16,76 @@ function sanitizeFunctionName(name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MAX_RETRY_AFTER_MS = 10000;
|
const MAX_RETRY_AFTER_MS = 10000;
|
||||||
|
const ANTIGRAVITY_TRANSIENT_RETRY_MAX_MS = 15000;
|
||||||
const MAX_ANTIGRAVITY_OUTPUT_TOKENS = 16384;
|
const MAX_ANTIGRAVITY_OUTPUT_TOKENS = 16384;
|
||||||
|
|
||||||
|
const ANTIGRAVITY_TRANSIENT_ERROR_PATTERNS = [
|
||||||
|
/high\s+traffic/i,
|
||||||
|
/agent\s+(execution\s+)?terminated\s+due\s+to\s+error/i,
|
||||||
|
/capacity/i,
|
||||||
|
/temporarily\s+unavailable/i,
|
||||||
|
/timeout/i,
|
||||||
|
/stream\s+(ended|closed|terminated|interrupted)/i,
|
||||||
|
/empty\s+response/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
const ANTIGRAVITY_TRANSIENT_STATUSES = new Set([
|
||||||
|
HTTP_STATUS.SERVER_ERROR,
|
||||||
|
HTTP_STATUS.BAD_GATEWAY,
|
||||||
|
HTTP_STATUS.SERVICE_UNAVAILABLE,
|
||||||
|
HTTP_STATUS.GATEWAY_TIMEOUT,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 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 {
|
export class AntigravityExecutor extends BaseExecutor {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("antigravity", PROVIDERS.antigravity);
|
super("antigravity", PROVIDERS.antigravity);
|
||||||
@@ -26,17 +94,22 @@ export class AntigravityExecutor extends BaseExecutor {
|
|||||||
buildUrl(model, stream, urlIndex = 0) {
|
buildUrl(model, stream, urlIndex = 0) {
|
||||||
const baseUrls = this.getBaseUrls();
|
const baseUrls = this.getBaseUrls();
|
||||||
const baseUrl = baseUrls[urlIndex] || baseUrls[0];
|
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}`;
|
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) {
|
buildHeaders(credentials, stream = true, sessionId = null) {
|
||||||
|
const sid = sessionId || this._lastSessionId;
|
||||||
return {
|
return {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Authorization": `Bearer ${credentials.accessToken}`,
|
"Authorization": `Bearer ${credentials.accessToken}`,
|
||||||
"User-Agent": this.config.headers?.["User-Agent"] || ANTIGRAVITY_HEADERS["User-Agent"],
|
"User-Agent": this.config.headers?.["User-Agent"] || ANTIGRAVITY_HEADERS["User-Agent"],
|
||||||
[INTERNAL_REQUEST_HEADER.name]: INTERNAL_REQUEST_HEADER.value,
|
[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"
|
"Accept": stream ? "text/event-stream" : "application/json"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -44,6 +117,53 @@ export class AntigravityExecutor extends BaseExecutor {
|
|||||||
transformRequest(model, body, stream, credentials) {
|
transformRequest(model, body, stream, credentials) {
|
||||||
const projectId = credentials?.projectId || this.generateProjectId();
|
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
|
// Fix contents for Claude models via Antigravity
|
||||||
const contents = body.request?.contents?.map(c => {
|
const contents = body.request?.contents?.map(c => {
|
||||||
let role = c.role;
|
let role = c.role;
|
||||||
@@ -68,19 +188,28 @@ export class AntigravityExecutor extends BaseExecutor {
|
|||||||
|
|
||||||
if (tools && tools.length > 0) {
|
if (tools && tools.length > 0) {
|
||||||
// Merge all groups into a single functionDeclarations group (Gemini expects 1 group)
|
// Merge all groups into a single functionDeclarations group (Gemini expects 1 group)
|
||||||
const allDeclarations = tools.flatMap(group =>
|
const seenToolNames = new Set();
|
||||||
(group.functionDeclarations || []).map(fn => ({
|
const allDeclarations = [];
|
||||||
...fn,
|
for (const group of tools) {
|
||||||
name: sanitizeFunctionName(fn.name),
|
for (const fn of group.functionDeclarations || []) {
|
||||||
parameters: fn.parameters
|
const name = sanitizeFunctionName(fn.name);
|
||||||
? cleanJSONSchemaForAntigravity(structuredClone(fn.parameters))
|
if (seenToolNames.has(name)) continue;
|
||||||
: { type: "object", properties: { reason: { type: "string", description: "Brief explanation" } }, required: ["reason"] }
|
seenToolNames.add(name);
|
||||||
}))
|
allDeclarations.push({
|
||||||
);
|
...fn,
|
||||||
|
name,
|
||||||
|
parameters: fn.parameters
|
||||||
|
? cleanJSONSchemaForAntigravity(structuredClone(fn.parameters))
|
||||||
|
: { type: "object", properties: { reason: { type: "string", description: "Brief explanation" } }, required: ["reason"] }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
tools = allDeclarations.length > 0 ? [{ functionDeclarations: allDeclarations }] : [];
|
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 || {};
|
const { tools: _originalTools, toolConfig: _originalToolConfig, ...requestWithoutTools } = body.request || {};
|
||||||
|
stripBlacklisted(requestWithoutTools);
|
||||||
const generationConfig = { ...(requestWithoutTools.generationConfig || {}) };
|
const generationConfig = { ...(requestWithoutTools.generationConfig || {}) };
|
||||||
if (generationConfig.maxOutputTokens > MAX_ANTIGRAVITY_OUTPUT_TOKENS) {
|
if (generationConfig.maxOutputTokens > MAX_ANTIGRAVITY_OUTPUT_TOKENS) {
|
||||||
generationConfig.maxOutputTokens = MAX_ANTIGRAVITY_OUTPUT_TOKENS;
|
generationConfig.maxOutputTokens = MAX_ANTIGRAVITY_OUTPUT_TOKENS;
|
||||||
@@ -91,11 +220,16 @@ export class AntigravityExecutor extends BaseExecutor {
|
|||||||
generationConfig,
|
generationConfig,
|
||||||
...(contents && { contents }),
|
...(contents && { contents }),
|
||||||
...(tools && { tools }),
|
...(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,
|
safetySettings: undefined,
|
||||||
...(tools?.length > 0 && { toolConfig: { functionCallingConfig: { mode: "VALIDATED" } } })
|
...(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 {
|
return {
|
||||||
...body,
|
...body,
|
||||||
project: projectId,
|
project: projectId,
|
||||||
@@ -196,98 +330,49 @@ export class AntigravityExecutor extends BaseExecutor {
|
|||||||
return totalMs > 0 ? totalMs : null;
|
return totalMs > 0 ? totalMs : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute({ model, body, stream, credentials, signal, log, proxyOptions = null }) {
|
extractErrorMessage(errorJson, bodyText = "") {
|
||||||
const fallbackCount = this.getFallbackCount();
|
return [
|
||||||
let lastError = null;
|
errorJson?.error?.message,
|
||||||
let lastStatus = 0;
|
errorJson?.message,
|
||||||
const MAX_AUTO_RETRIES = 3;
|
errorJson?.error,
|
||||||
const MAX_RETRY_AFTER_RETRIES = 3;
|
bodyText,
|
||||||
const retryAttemptsByUrl = {}; // Track retry attempts per URL
|
].filter(Boolean).map(v => typeof v === "string" ? v : JSON.stringify(v)).join("\n");
|
||||||
const retryAfterAttemptsByUrl = {}; // Track Retry-After retries per URL
|
}
|
||||||
|
|
||||||
for (let urlIndex = 0; urlIndex < fallbackCount; urlIndex++) {
|
isTransientAntigravityError(status, message) {
|
||||||
const url = this.buildUrl(model, stream, urlIndex);
|
if (status === HTTP_STATUS.RATE_LIMITED) return true;
|
||||||
const transformedBody = this.transformRequest(model, body, stream, credentials);
|
if (ANTIGRAVITY_TRANSIENT_STATUSES.has(status)) return true;
|
||||||
const sessionId = transformedBody.request?.sessionId;
|
return ANTIGRAVITY_TRANSIENT_ERROR_PATTERNS.some(pattern => pattern.test(message || ""));
|
||||||
const headers = this.buildHeaders(credentials, stream, sessionId);
|
}
|
||||||
|
|
||||||
// Initialize retry counters for this URL
|
// Hook called by BaseExecutor.tryRetry: derive delay from Retry-After (header → body),
|
||||||
if (!retryAttemptsByUrl[urlIndex]) {
|
// cap at MAX_RETRY_AFTER_MS, else retry transient Antigravity failures with backoff.
|
||||||
retryAttemptsByUrl[urlIndex] = 0;
|
// Return false to veto (fallback URL / final error).
|
||||||
}
|
async computeRetryDelay(response, attempt) {
|
||||||
if (!retryAfterAttemptsByUrl[urlIndex]) {
|
let bodyText = "";
|
||||||
retryAfterAttemptsByUrl[urlIndex] = 0;
|
let errorJson = null;
|
||||||
}
|
let retryMs = this.parseRetryHeaders(response.headers);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await proxyAwareFetch(url, {
|
bodyText = await response.clone().text();
|
||||||
method: "POST",
|
errorJson = bodyText ? JSON.parse(bodyText) : null;
|
||||||
headers,
|
} catch {
|
||||||
body: JSON.stringify(transformedBody),
|
// ignore parse errors → fall through to status/message based retry
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw lastError || new Error(`All ${fallbackCount} URLs failed with status ${lastStatus}`);
|
const errorMessage = this.extractErrorMessage(errorJson, bodyText);
|
||||||
|
|
||||||
|
if (!retryMs) {
|
||||||
|
retryMs = this.parseRetryFromErrorMessage(errorMessage);
|
||||||
|
}
|
||||||
|
if (retryMs) return retryMs <= MAX_RETRY_AFTER_MS ? retryMs : false;
|
||||||
|
|
||||||
|
if (!this.isTransientAntigravityError(response.status, errorMessage)) return false;
|
||||||
|
|
||||||
|
const cap = response.status === HTTP_STATUS.RATE_LIMITED
|
||||||
|
? MAX_RETRY_AFTER_MS
|
||||||
|
: ANTIGRAVITY_TRANSIENT_RETRY_MAX_MS;
|
||||||
|
return Math.min(1000 * (2 ** attempt), cap); // exponential backoff
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { HTTP_STATUS, RETRY_CONFIG, DEFAULT_RETRY_CONFIG, resolveRetryEntry, FET
|
|||||||
import { shouldRefreshCredentials } from "../services/oauthCredentialManager.js";
|
import { shouldRefreshCredentials } from "../services/oauthCredentialManager.js";
|
||||||
import { proxyAwareFetch } from "../utils/proxyFetch.js";
|
import { proxyAwareFetch } from "../utils/proxyFetch.js";
|
||||||
import { dbg } from "../utils/debugLog.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
|
* BaseExecutor - Base class for provider executors
|
||||||
@@ -27,13 +28,13 @@ export class BaseExecutor {
|
|||||||
|
|
||||||
buildUrl(model, stream, urlIndex = 0, credentials = null) {
|
buildUrl(model, stream, urlIndex = 0, credentials = null) {
|
||||||
if (this.provider?.startsWith?.("openai-compatible-")) {
|
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 normalized = baseUrl.replace(/\/$/, "");
|
||||||
const path = this.provider.includes("responses") ? "/responses" : "/chat/completions";
|
const path = this.provider.includes("responses") ? "/responses" : "/chat/completions";
|
||||||
return `${normalized}${path}`;
|
return `${normalized}${path}`;
|
||||||
}
|
}
|
||||||
if (this.provider?.startsWith?.("anthropic-compatible-")) {
|
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(/\/$/, "");
|
const normalized = baseUrl.replace(/\/$/, "");
|
||||||
return `${normalized}/messages`;
|
return `${normalized}/messages`;
|
||||||
}
|
}
|
||||||
@@ -55,7 +56,7 @@ export class BaseExecutor {
|
|||||||
headers["Authorization"] = `Bearer ${credentials.accessToken}`;
|
headers["Authorization"] = `Bearer ${credentials.accessToken}`;
|
||||||
}
|
}
|
||||||
if (!headers["anthropic-version"]) {
|
if (!headers["anthropic-version"]) {
|
||||||
headers["anthropic-version"] = "2023-06-01";
|
headers["anthropic-version"] = ANTHROPIC_API_VERSION;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Standard Bearer token auth for other providers
|
// Standard Bearer token auth for other providers
|
||||||
@@ -105,12 +106,20 @@ export class BaseExecutor {
|
|||||||
const retryConfig = { ...DEFAULT_RETRY_CONFIG, ...this.config.retry };
|
const retryConfig = { ...DEFAULT_RETRY_CONFIG, ...this.config.retry };
|
||||||
|
|
||||||
// Schedule retry via retryConfig[statusKey]. Returns true when caller should `urlIndex--; continue`
|
// 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]);
|
const { attempts, delayMs } = resolveRetryEntry(retryConfig[statusKey]);
|
||||||
if (attempts <= 0 || retryAttemptsByUrl[urlIndex] >= attempts) return false;
|
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]++;
|
retryAttemptsByUrl[urlIndex]++;
|
||||||
log?.debug?.("RETRY", `${reason} retry ${retryAttemptsByUrl[urlIndex]}/${attempts} after ${delayMs / 1000}s`);
|
log?.debug?.("RETRY", `${reason} retry ${retryAttemptsByUrl[urlIndex]}/${attempts} after ${waitMs / 1000}s`);
|
||||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
await new Promise(resolve => setTimeout(resolve, waitMs));
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -142,7 +151,7 @@ export class BaseExecutor {
|
|||||||
const cl = response.headers?.get?.("content-length") || "?";
|
const cl = response.headers?.get?.("content-length") || "?";
|
||||||
dbg("FETCH", `${this.provider.toUpperCase()} ← ${response.status} | ttft=${Date.now() - fetchT0}ms | ct=${ct} | cl=${cl}`);
|
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)) {
|
if (this.shouldRetry(response.status, urlIndex)) {
|
||||||
log?.debug?.("RETRY", `${response.status} on ${url}, trying fallback ${urlIndex + 1}`);
|
log?.debug?.("RETRY", `${response.status} on ${url}, trying fallback ${urlIndex + 1}`);
|
||||||
|
|||||||
40
open-sse/executors/codebuddy-cn.js
Normal file
40
open-sse/executors/codebuddy-cn.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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) {
|
||||||
|
// Client explicitly asked for reasoning — mirror the CLI's reasoning_summary
|
||||||
|
// so CodeBuddy surfaces the model's reasoning.
|
||||||
|
transformed.reasoning_summary = "auto";
|
||||||
|
}
|
||||||
|
// No reasoning requested: leave both unset. Forcing reasoning_effort:"medium"
|
||||||
|
// + reasoning_summary on plain requests makes CodeBuddy trip its content
|
||||||
|
// filter and return an error (#2071).
|
||||||
|
return transformed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CodeBuddyExecutor;
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { createHash } from "crypto";
|
|
||||||
import { BaseExecutor } from "./base.js";
|
import { BaseExecutor } from "./base.js";
|
||||||
import { CODEX_DEFAULT_INSTRUCTIONS } from "../config/codexInstructions.js";
|
import { CODEX_DEFAULT_INSTRUCTIONS } from "../config/codexInstructions.js";
|
||||||
import { PROVIDERS } from "../config/providers.js";
|
import { PROVIDERS } from "../config/providers.js";
|
||||||
@@ -6,34 +5,35 @@ import {
|
|||||||
refreshProviderCredentials,
|
refreshProviderCredentials,
|
||||||
shouldRefreshCredentials,
|
shouldRefreshCredentials,
|
||||||
} from "../services/oauthCredentialManager.js";
|
} from "../services/oauthCredentialManager.js";
|
||||||
import { normalizeResponsesInput } from "../translator/helpers/responsesApiHelper.js";
|
import { normalizeResponsesInput } from "../translator/formats/responsesApi.js";
|
||||||
import { fetchImageAsBase64 } from "../translator/helpers/imageHelper.js";
|
import { fetchImageAsBase64 } from "../translator/concerns/image.js";
|
||||||
import { getModelUpstreamId } from "../config/providerModels.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 { DEFAULT_RETRY_CONFIG, resolveRetryEntry } from "../config/runtimeConfig.js";
|
||||||
import { dbg } from "../utils/debugLog.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
|
// 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_OVERLOADED_PATTERNS = ["server_is_overloaded", "service_unavailable_error"];
|
||||||
const CODEX_SSE_PEEK_BYTES = 4096;
|
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
|
// Server-generated item id prefixes that Codex /responses cannot resolve when store=false
|
||||||
const SERVER_ID_PATTERN = /^(rs|fc|resp|msg)_/;
|
const SERVER_ID_PATTERN = /^(rs|fc|resp|msg)_/;
|
||||||
|
|
||||||
// Hosted tool types that Codex/OpenAI Responses executes server-side
|
// Hosted tool types that Codex/OpenAI Responses executes server-side
|
||||||
const CODEX_HOSTED_TOOL_TYPES = new Set([
|
const CODEX_HOSTED_TOOL_TYPES = new Set([
|
||||||
"image_generation", "web_search", "web_search_preview", "file_search",
|
"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
|
// Allowlist of fields accepted by Codex Responses API — anything else is stripped
|
||||||
const RESPONSES_API_ALLOWLIST = new Set([
|
const RESPONSES_API_ALLOWLIST = new Set([
|
||||||
"model", "input", "instructions", "tools", "tool_choice", "stream", "store",
|
"model", "input", "instructions", "tools", "tool_choice", "stream", "store",
|
||||||
"reasoning", "service_tier", "include", "prompt_cache_key", "client_metadata"
|
"reasoning", "service_tier", "include", "prompt_cache_key", "client_metadata",
|
||||||
|
"text"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Convert role=system → role=developer in body.input (keeps content in cacheable prefix)
|
// Convert role=system → role=developer in body.input (keeps content in cacheable prefix)
|
||||||
@@ -76,6 +76,7 @@ function normalizeCodexTools(body) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (type !== "function") {
|
if (type !== "function") {
|
||||||
|
if (CODEX_PASSTHROUGH_TOOL_TYPES.has(type)) return true;
|
||||||
if (!type || tool.function || typeof tool.name === "string") return false;
|
if (!type || tool.function || typeof tool.name === "string") return false;
|
||||||
return CODEX_HOSTED_TOOL_TYPES.has(type);
|
return CODEX_HOSTED_TOOL_TYPES.has(type);
|
||||||
}
|
}
|
||||||
@@ -104,86 +105,17 @@ function normalizeCodexTools(body) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache machine ID at module level (resolved once)
|
// Resolve prompt-cache session id: client session → assistant-text-hash → workspaceId → connection
|
||||||
let cachedMachineId = null;
|
function resolveCacheSessionId(body, credentials) {
|
||||||
getConsistentMachineId().then(id => { cachedMachineId = id; });
|
return resolveSessionId({
|
||||||
|
headers: credentials?.rawHeaders,
|
||||||
function hashContent(text) {
|
body,
|
||||||
return createHash("sha256").update(text).digest("hex").slice(0, 16);
|
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)
|
* Codex Executor - handles OpenAI Codex API (Responses API format)
|
||||||
* Automatically injects default instructions if missing
|
* Automatically injects default instructions if missing
|
||||||
@@ -377,7 +309,7 @@ export class CodexExecutor extends BaseExecutor {
|
|||||||
this._isCompact = !!body._compact;
|
this._isCompact = !!body._compact;
|
||||||
delete body._compact;
|
delete body._compact;
|
||||||
// Resolve conversation-stable session_id (priority: body → assistant-text → workspace → machine)
|
// 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)
|
// Convert string input to array format (Codex API requires input as array)
|
||||||
const normalized = normalizeResponsesInput(body.input);
|
const normalized = normalizeResponsesInput(body.input);
|
||||||
if (normalized) body.input = normalized;
|
if (normalized) body.input = normalized;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
import { BaseExecutor } from "./base.js";
|
import { BaseExecutor } from "./base.js";
|
||||||
import { PROVIDERS } from "../config/providers.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
|
* CommandCodeExecutor — talks to https://api.commandcode.ai/alpha/generate
|
||||||
@@ -70,15 +71,15 @@ function wrapNdjsonAsOpenAISse(originalResponse, model) {
|
|||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
if (!trimmed) continue;
|
if (!trimmed) continue;
|
||||||
// Translate AI SDK v5 NDJSON line to one or more OpenAI chunks
|
// Translate AI SDK v5 NDJSON line to one or more OpenAI chunks
|
||||||
emitChunks(convertCommandCodeToOpenAI(trimmed, state), controller);
|
emitChunks(commandCodeToOpenAIResponse(trimmed, state), controller);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
flush(controller) {
|
flush(controller) {
|
||||||
const trimmed = buffer.trim();
|
const trimmed = buffer.trim();
|
||||||
if (trimmed) {
|
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";
|
} from "../utils/cursorProtobuf.js";
|
||||||
import { buildCursorHeaders } from "../utils/cursorChecksum.js";
|
import { buildCursorHeaders } from "../utils/cursorChecksum.js";
|
||||||
import { estimateUsage } from "../utils/usageTracking.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 { FORMATS } from "../translator/formats.js";
|
||||||
import { proxyAwareFetch } from "../utils/proxyFetch.js";
|
import { proxyAwareFetch } from "../utils/proxyFetch.js";
|
||||||
import zlib from "zlib";
|
import zlib from "zlib";
|
||||||
@@ -98,6 +100,32 @@ function decompressPayload(payload, flags) {
|
|||||||
return payload;
|
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) {
|
function createErrorResponse(jsonError) {
|
||||||
const errorMsg = jsonError?.error?.details?.[0]?.debug?.details?.title
|
const errorMsg = jsonError?.error?.details?.[0]?.debug?.details?.title
|
||||||
|| jsonError?.error?.details?.[0]?.debug?.details?.detail
|
|| jsonError?.error?.details?.[0]?.debug?.details?.detail
|
||||||
@@ -141,7 +169,7 @@ export class CursorExecutor extends BaseExecutor {
|
|||||||
|
|
||||||
transformRequest(model, body, stream, credentials) {
|
transformRequest(model, body, stream, credentials) {
|
||||||
// Messages are already translated by chatCore (claude→openai→cursor)
|
// 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 messages = body.messages || [];
|
||||||
const tools = body.tools || [];
|
const tools = body.tools || [];
|
||||||
const reasoningEffort = body.reasoning_effort || null;
|
const reasoningEffort = body.reasoning_effort || null;
|
||||||
@@ -286,36 +314,12 @@ export class CursorExecutor extends BaseExecutor {
|
|||||||
debugLog(`[CURSOR BUFFER] Total length: ${buffer.length} bytes`);
|
debugLog(`[CURSOR BUFFER] Total length: ${buffer.length} bytes`);
|
||||||
|
|
||||||
while (offset < buffer.length) {
|
while (offset < buffer.length) {
|
||||||
if (offset + 5 > buffer.length) {
|
const frame = readCursorFrame(buffer, offset, frameCount, "");
|
||||||
debugLog(
|
if (frame.status === "done") break;
|
||||||
`[CURSOR BUFFER] Reached end, offset=${offset}, remaining=${buffer.length - offset}`
|
offset = frame.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;
|
|
||||||
frameCount++;
|
frameCount++;
|
||||||
|
if (frame.status === "skip") continue;
|
||||||
payload = decompressPayload(payload, flags);
|
const payload = frame.payload;
|
||||||
if (!payload) {
|
|
||||||
debugLog(`[CURSOR BUFFER] Frame ${frameCount}: decompression failed, skipping`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for JSON error frames (byte guard: skip toString on non-JSON frames)
|
// Check for JSON error frames (byte guard: skip toString on non-JSON frames)
|
||||||
if (payload.length > 0 && payload[0] === 0x7b) {
|
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`);
|
debugLog(`[CURSOR BUFFER SSE] Total length: ${buffer.length} bytes`);
|
||||||
|
|
||||||
while (offset < buffer.length) {
|
while (offset < buffer.length) {
|
||||||
if (offset + 5 > buffer.length) {
|
const frame = readCursorFrame(buffer, offset, frameCount, " SSE");
|
||||||
debugLog(
|
if (frame.status === "done") break;
|
||||||
`[CURSOR BUFFER SSE] Reached end, offset=${offset}, remaining=${buffer.length - offset}`
|
offset = frame.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;
|
|
||||||
frameCount++;
|
frameCount++;
|
||||||
|
if (frame.status === "skip") continue;
|
||||||
payload = decompressPayload(payload, flags);
|
const payload = frame.payload;
|
||||||
if (!payload) {
|
|
||||||
debugLog(`[CURSOR BUFFER SSE] Frame ${frameCount}: decompression failed, skipping`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for JSON error frames (byte-guard: only decode if starts with '{')
|
// Check for JSON error frames (byte-guard: only decode if starts with '{')
|
||||||
if (payload[0] === 0x7b) {
|
if (payload[0] === 0x7b) {
|
||||||
@@ -542,21 +522,7 @@ export class CursorExecutor extends BaseExecutor {
|
|||||||
const tc = result.toolCall;
|
const tc = result.toolCall;
|
||||||
|
|
||||||
if (chunks.length === 0) {
|
if (chunks.length === 0) {
|
||||||
chunks.push(
|
chunks.push(chatChunkSse({ id: responseId, created, model, delta: { role: "assistant", content: "" } }));
|
||||||
`data: ${JSON.stringify({
|
|
||||||
id: responseId,
|
|
||||||
object: "chat.completion.chunk",
|
|
||||||
created,
|
|
||||||
model,
|
|
||||||
choices: [
|
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
delta: { role: "assistant", content: "" },
|
|
||||||
finish_reason: null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})}\n\n`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolCallsMap.has(tc.id)) {
|
if (toolCallsMap.has(tc.id)) {
|
||||||
@@ -569,33 +535,22 @@ export class CursorExecutor extends BaseExecutor {
|
|||||||
// Stream the delta arguments
|
// Stream the delta arguments
|
||||||
if (tc.function.arguments) {
|
if (tc.function.arguments) {
|
||||||
emittedToolCallIds.add(tc.id);
|
emittedToolCallIds.add(tc.id);
|
||||||
chunks.push(
|
chunks.push(chatChunkSse({
|
||||||
`data: ${JSON.stringify({
|
id: responseId, created, model,
|
||||||
id: responseId,
|
delta: {
|
||||||
object: "chat.completion.chunk",
|
tool_calls: [
|
||||||
created,
|
|
||||||
model,
|
|
||||||
choices: [
|
|
||||||
{
|
{
|
||||||
index: 0,
|
index: existing.index,
|
||||||
delta: {
|
id: tc.id,
|
||||||
tool_calls: [
|
type: "function",
|
||||||
{
|
function: {
|
||||||
index: existing.index,
|
name: tc.function.name,
|
||||||
id: tc.id,
|
arguments: tc.function.arguments
|
||||||
type: "function",
|
}
|
||||||
function: {
|
|
||||||
name: tc.function.name,
|
|
||||||
arguments: tc.function.arguments
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
finish_reason: null
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})}\n\n`
|
}
|
||||||
);
|
}));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// New tool call - assign index and add to map
|
// New tool call - assign index and add to map
|
||||||
@@ -606,56 +561,34 @@ export class CursorExecutor extends BaseExecutor {
|
|||||||
|
|
||||||
// Stream initial tool call with name
|
// Stream initial tool call with name
|
||||||
emittedToolCallIds.add(tc.id);
|
emittedToolCallIds.add(tc.id);
|
||||||
chunks.push(
|
chunks.push(chatChunkSse({
|
||||||
`data: ${JSON.stringify({
|
id: responseId, created, model,
|
||||||
id: responseId,
|
delta: {
|
||||||
object: "chat.completion.chunk",
|
tool_calls: [
|
||||||
created,
|
|
||||||
model,
|
|
||||||
choices: [
|
|
||||||
{
|
{
|
||||||
index: 0,
|
index: toolCallIndex,
|
||||||
delta: {
|
id: tc.id,
|
||||||
tool_calls: [
|
type: "function",
|
||||||
{
|
function: {
|
||||||
index: toolCallIndex,
|
name: tc.function.name,
|
||||||
id: tc.id,
|
arguments: tc.function.arguments
|
||||||
type: "function",
|
}
|
||||||
function: {
|
|
||||||
name: tc.function.name,
|
|
||||||
arguments: tc.function.arguments
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
finish_reason: null
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})}\n\n`
|
}
|
||||||
);
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.text) {
|
if (result.text) {
|
||||||
totalContent += result.text;
|
totalContent += result.text;
|
||||||
chunks.push(
|
chunks.push(chatChunkSse({
|
||||||
`data: ${JSON.stringify({
|
id: responseId, created, model,
|
||||||
id: responseId,
|
delta:
|
||||||
object: "chat.completion.chunk",
|
chunks.length === 0 && toolCalls.length === 0
|
||||||
created,
|
? { role: "assistant", content: result.text }
|
||||||
model,
|
: { content: result.text }
|
||||||
choices: [
|
}));
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
delta:
|
|
||||||
chunks.length === 0 && toolCalls.length === 0
|
|
||||||
? { role: "assistant", content: result.text }
|
|
||||||
: { content: result.text },
|
|
||||||
finish_reason: null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})}\n\n`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isComposerModel(model) && result.thinking) {
|
if (isComposerModel(model) && result.thinking) {
|
||||||
@@ -665,24 +598,13 @@ export class CursorExecutor extends BaseExecutor {
|
|||||||
const deltaContent = visibleContent.slice(emittedComposerThinkingContentLength);
|
const deltaContent = visibleContent.slice(emittedComposerThinkingContentLength);
|
||||||
emittedComposerThinkingContentLength = visibleContent.length;
|
emittedComposerThinkingContentLength = visibleContent.length;
|
||||||
totalContent += deltaContent;
|
totalContent += deltaContent;
|
||||||
chunks.push(
|
chunks.push(chatChunkSse({
|
||||||
`data: ${JSON.stringify({
|
id: responseId, created, model,
|
||||||
id: responseId,
|
delta:
|
||||||
object: "chat.completion.chunk",
|
chunks.length === 0 && toolCalls.length === 0
|
||||||
created,
|
? { role: "assistant", content: deltaContent }
|
||||||
model,
|
: { content: deltaContent }
|
||||||
choices: [
|
}));
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
delta:
|
|
||||||
chunks.length === 0 && toolCalls.length === 0
|
|
||||||
? { role: "assistant", content: deltaContent }
|
|
||||||
: { content: deltaContent },
|
|
||||||
finish_reason: null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})}\n\n`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -708,53 +630,28 @@ export class CursorExecutor extends BaseExecutor {
|
|||||||
|
|
||||||
// Emit SSE chunk for the finalized tool call if not already emitted
|
// Emit SSE chunk for the finalized tool call if not already emitted
|
||||||
if (!emittedToolCallIds.has(tc.id)) {
|
if (!emittedToolCallIds.has(tc.id)) {
|
||||||
chunks.push(
|
chunks.push(chatChunkSse({
|
||||||
`data: ${JSON.stringify({
|
id: responseId, created, model,
|
||||||
id: responseId,
|
delta: {
|
||||||
object: "chat.completion.chunk",
|
tool_calls: [
|
||||||
created,
|
|
||||||
model,
|
|
||||||
choices: [
|
|
||||||
{
|
{
|
||||||
index: 0,
|
index: toolCallIndex,
|
||||||
delta: {
|
id: tc.id,
|
||||||
tool_calls: [
|
type: "function",
|
||||||
{
|
function: {
|
||||||
index: toolCallIndex,
|
name: tc.function.name,
|
||||||
id: tc.id,
|
arguments: tc.function.arguments
|
||||||
type: "function",
|
}
|
||||||
function: {
|
|
||||||
name: tc.function.name,
|
|
||||||
arguments: tc.function.arguments
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
finish_reason: null
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})}\n\n`
|
}
|
||||||
);
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chunks.length === 0 && toolCalls.length === 0) {
|
if (chunks.length === 0 && toolCalls.length === 0) {
|
||||||
chunks.push(
|
chunks.push(chatChunkSse({ id: responseId, created, model, delta: { role: "assistant", content: "" } }));
|
||||||
`data: ${JSON.stringify({
|
|
||||||
id: responseId,
|
|
||||||
object: "chat.completion.chunk",
|
|
||||||
created,
|
|
||||||
model,
|
|
||||||
choices: [
|
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
delta: { role: "assistant", content: "" },
|
|
||||||
finish_reason: null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})}\n\n`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const usage = estimateUsage(body, totalContent.length, FORMATS.OPENAI);
|
const usage = estimateUsage(body, totalContent.length, FORMATS.OPENAI);
|
||||||
@@ -775,15 +672,11 @@ export class CursorExecutor extends BaseExecutor {
|
|||||||
usage
|
usage
|
||||||
})}\n\n`
|
})}\n\n`
|
||||||
);
|
);
|
||||||
chunks.push("data: [DONE]\n\n");
|
chunks.push(SSE_DONE);
|
||||||
|
|
||||||
return new Response(chunks.join(""), {
|
return new Response(chunks.join(""), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: { ...SSE_HEADERS }
|
||||||
"Content-Type": "text/event-stream",
|
|
||||||
"Cache-Control": "no-cache",
|
|
||||||
"Connection": "keep-alive"
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,80 @@
|
|||||||
import { BaseExecutor } from "./base.js";
|
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 { 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 { getCachedClaudeHeaders } from "../utils/claudeHeaderCache.js";
|
||||||
import { proxyAwareFetch } from "../utils/proxyFetch.js";
|
import { proxyAwareFetch } from "../utils/proxyFetch.js";
|
||||||
import { injectReasoningContent } from "../utils/reasoningContentInjector.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 {
|
export class DefaultExecutor extends BaseExecutor {
|
||||||
constructor(provider) {
|
constructor(provider) {
|
||||||
@@ -15,9 +85,11 @@ export class DefaultExecutor extends BaseExecutor {
|
|||||||
const transformed = this.applyJsonSchemaFallback(body);
|
const transformed = this.applyJsonSchemaFallback(body);
|
||||||
|
|
||||||
if (transformed && typeof transformed === "object") {
|
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;
|
delete transformed.client_metadata;
|
||||||
}
|
}
|
||||||
|
stripUnsupportedParams(this.provider, model, transformed);
|
||||||
}
|
}
|
||||||
|
|
||||||
return injectReasoningContent({ provider: this.provider, model, body: 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) {
|
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-")) {
|
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 normalized = baseUrl.replace(/\/$/, "");
|
||||||
const path = this.provider.includes("responses") ? "/responses" : "/chat/completions";
|
const path = this.provider.includes("responses") ? "/responses" : "/chat/completions";
|
||||||
return `${normalized}${path}`;
|
return `${normalized}${path}`;
|
||||||
}
|
}
|
||||||
if (this.provider?.startsWith?.("anthropic-compatible-")) {
|
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(/\/$/, "");
|
const normalized = baseUrl.replace(/\/$/, "");
|
||||||
return `${normalized}/messages`;
|
return `${normalized}/messages`;
|
||||||
}
|
}
|
||||||
switch (this.provider) {
|
// gemini-format: build :streamGenerateContent / :generateContent path
|
||||||
case "claude":
|
if (this.config.format === "gemini") {
|
||||||
case "glm":
|
return `${this.config.baseUrl}/${model}:${stream ? "streamGenerateContent?alt=sse" : "generateContent"}`;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// 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) {
|
buildHeaders(credentials, stream = true) {
|
||||||
const headers = { "Content-Type": "application/json", ...this.config.headers };
|
const rt = credentials?.runtimeTransport;
|
||||||
|
const headers = { "Content-Type": "application/json", ...(rt ? rt.headers : this.config.headers) };
|
||||||
switch (this.provider) {
|
const desc = rt?.auth || AUTH_DESCRIPTORS[this.provider] || this.resolveAuthDescriptor();
|
||||||
case "gemini":
|
// Hooks run BEFORE auth so dynamic overlays (claude cached headers) can't clobber the token.
|
||||||
credentials.apiKey ? headers["x-goog-api-key"] = credentials.apiKey : headers["Authorization"] = `Bearer ${credentials.accessToken}`;
|
for (const hook of desc.hooks || []) HEADER_HOOKS[hook]?.(headers, credentials);
|
||||||
break;
|
applyAuth(headers, desc, credentials);
|
||||||
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}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip first-party Claude Code identity headers for non-Anthropic anthropic-compatible upstreams
|
// Strip first-party Claude Code identity headers for non-Anthropic anthropic-compatible upstreams
|
||||||
if (this.provider?.startsWith?.("anthropic-compatible-")) {
|
if (this.provider?.startsWith?.("anthropic-compatible-")) {
|
||||||
@@ -196,15 +205,25 @@ export class DefaultExecutor extends BaseExecutor {
|
|||||||
return headers;
|
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) {
|
async refreshCredentials(credentials, log, proxyOptions = null) {
|
||||||
if (!credentials.refreshToken) return null;
|
if (!credentials.refreshToken) return null;
|
||||||
|
|
||||||
const refreshers = {
|
const refreshers = {
|
||||||
claude: () => this.refreshWithJSON(OAUTH_ENDPOINTS.anthropic.token, { grant_type: "refresh_token", refresh_token: credentials.refreshToken, client_id: PROVIDERS.claude.clientId }, proxyOptions),
|
claude: () => this.refreshFromGrant(credentials, 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),
|
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),
|
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),
|
iflow: () => this.refreshIflow(credentials.refreshToken, proxyOptions),
|
||||||
gemini: () => this.refreshGoogle(credentials.refreshToken, proxyOptions),
|
gemini: () => this.refreshFromGrant(credentials, proxyOptions),
|
||||||
kiro: () => this.refreshKiro(credentials.refreshToken, proxyOptions),
|
kiro: () => this.refreshKiro(credentials.refreshToken, proxyOptions),
|
||||||
cline: () => this.refreshCline(credentials.refreshToken, proxyOptions),
|
cline: () => this.refreshCline(credentials.refreshToken, proxyOptions),
|
||||||
"kimi-coding": () => this.refreshKimiCoding(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 };
|
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) {
|
async refreshKiro(refreshToken, proxyOptions = null) {
|
||||||
const response = await proxyAwareFetch(PROVIDERS.kiro.tokenUrl, {
|
const response = await proxyAwareFetch(PROVIDERS.kiro.tokenUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -281,37 +289,29 @@ export class DefaultExecutor extends BaseExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async refreshCline(refreshToken, proxyOptions = null) {
|
async refreshCline(refreshToken, proxyOptions = null) {
|
||||||
console.log('[DEBUG] Refreshing Cline token, refreshToken length:', refreshToken?.length);
|
const response = await proxyAwareFetch(PROVIDERS.cline.refreshUrl, {
|
||||||
const response = await proxyAwareFetch("https://api.cline.bot/api/v1/auth/refresh", {
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
||||||
body: JSON.stringify({ refreshToken, grantType: "refresh_token", clientType: "extension" })
|
body: JSON.stringify({ refreshToken, grantType: "refresh_token", clientType: "extension" })
|
||||||
}, proxyOptions);
|
}, proxyOptions);
|
||||||
console.log('[DEBUG] Cline refresh response status:', response.status);
|
if (!response.ok) return null;
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
console.log('[DEBUG] Cline refresh error:', errorText);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const payload = await response.json();
|
const payload = await response.json();
|
||||||
console.log('[DEBUG] Cline refresh payload:', JSON.stringify(payload).substring(0, 200));
|
|
||||||
const data = payload?.data || payload;
|
const data = payload?.data || payload;
|
||||||
const expiresAtIso = data?.expiresAt;
|
const expiresAtIso = data?.expiresAt;
|
||||||
const expiresIn = expiresAtIso ? Math.max(1, Math.floor((new Date(expiresAtIso).getTime() - Date.now()) / 1000)) : undefined;
|
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 };
|
return { accessToken: data?.accessToken, refreshToken: data?.refreshToken || refreshToken, expiresIn };
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshKimiCoding(refreshToken, proxyOptions = null) {
|
async refreshKimiCoding(refreshToken, proxyOptions = null) {
|
||||||
const kimiHeaders = buildKimiHeaders();
|
const kimiHeaders = buildKimiHeaders();
|
||||||
const response = await proxyAwareFetch("https://auth.kimi.com/api/oauth/token", {
|
const response = await proxyAwareFetch(PROVIDERS["kimi-coding"].refreshUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
...kimiHeaders
|
...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);
|
}, proxyOptions);
|
||||||
if (!response.ok) return null;
|
if (!response.ok) return null;
|
||||||
const tokens = await response.json();
|
const tokens = await response.json();
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { openaiResponsesToOpenAIResponse } from "../translator/response/openai-r
|
|||||||
import { initState } from "../translator/index.js";
|
import { initState } from "../translator/index.js";
|
||||||
import { parseSSELine, formatSSE } from "../utils/streamHelpers.js";
|
import { parseSSELine, formatSSE } from "../utils/streamHelpers.js";
|
||||||
import { proxyAwareFetch } from "../utils/proxyFetch.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";
|
import crypto from "crypto";
|
||||||
|
|
||||||
export class GithubExecutor extends BaseExecutor {
|
export class GithubExecutor extends BaseExecutor {
|
||||||
@@ -108,54 +110,18 @@ export class GithubExecutor extends BaseExecutor {
|
|||||||
return /gpt-5|o[134]-/i.test(model);
|
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) {
|
transformRequest(model, body, stream, credentials) {
|
||||||
const transformed = { ...body };
|
const transformed = { ...body };
|
||||||
if (this.requiresMaxCompletionTokens(model) && transformed.max_tokens !== undefined) {
|
if (this.requiresMaxCompletionTokens(model) && transformed.max_tokens !== undefined) {
|
||||||
transformed.max_completion_tokens = transformed.max_tokens;
|
transformed.max_completion_tokens = transformed.max_tokens;
|
||||||
delete 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
|
// "none" means no thinking — strip it so models that don't support "none" don't 400
|
||||||
if (transformed.reasoning_effort === "none") {
|
if (transformed.reasoning_effort === "none") {
|
||||||
delete transformed.reasoning_effort;
|
delete transformed.reasoning_effort;
|
||||||
}
|
}
|
||||||
// Strip reasoning_effort only for models that reject it
|
// Config-driven strip of params unsupported by this provider/model
|
||||||
if (!this.supportsReasoningEffort(model) && transformed.reasoning_effort !== undefined) {
|
stripUnsupportedParams("github", model, transformed);
|
||||||
delete transformed.reasoning_effort;
|
|
||||||
}
|
|
||||||
return transformed;
|
return transformed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +210,7 @@ export class GithubExecutor extends BaseExecutor {
|
|||||||
if (!parsed) continue;
|
if (!parsed) continue;
|
||||||
|
|
||||||
if (parsed.done && stream === true) {
|
if (parsed.done && stream === true) {
|
||||||
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"));
|
controller.enqueue(new TextEncoder().encode(SSE_DONE));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { BaseExecutor } from "./base.js";
|
import { BaseExecutor } from "./base.js";
|
||||||
import { PROVIDERS } from "../config/providers.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_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";
|
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 };
|
yield { done: true, fingerprint, responseId };
|
||||||
}
|
}
|
||||||
|
|
||||||
function sseChunk(data) {
|
|
||||||
return `data: ${JSON.stringify(data)}\n\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildStreamingResponse(eventStream, model, cid, created, isThinkingModel, signal) {
|
function buildStreamingResponse(eventStream, model, cid, created, isThinkingModel, signal) {
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
return new ReadableStream({
|
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,
|
id: cid, object: "chat.completion.chunk", created, model, system_fingerprint: fp || null,
|
||||||
choices: [{ index: 0, delta: {}, finish_reason: "stop", logprobs: 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) {
|
} catch (err) {
|
||||||
controller.enqueue(encoder.encode(sseChunk({
|
controller.enqueue(encoder.encode(sseChunk({
|
||||||
id: cid, object: "chat.completion.chunk", created, model, system_fingerprint: null,
|
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 }],
|
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 {
|
} finally {
|
||||||
controller.close();
|
controller.close();
|
||||||
}
|
}
|
||||||
@@ -333,7 +331,7 @@ export class GrokWebExecutor extends BaseExecutor {
|
|||||||
const sseStream = buildStreamingResponse(response.body, model, cid, created, isThinking, signal);
|
const sseStream = buildStreamingResponse(response.body, model, cid, created, isThinking, signal);
|
||||||
finalResponse = new Response(sseStream, {
|
finalResponse = new Response(sseStream, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "X-Accel-Buffering": "no" },
|
headers: { ...SSE_HEADERS_NO_BUFFER },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
finalResponse = await buildNonStreamingResponse(response.body, model, cid, created, isThinking, signal);
|
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 { CommandCodeExecutor } from "./commandcode.js";
|
||||||
import { XiaomiTokenplanExecutor } from "./xiaomi-tokenplan.js";
|
import { XiaomiTokenplanExecutor } from "./xiaomi-tokenplan.js";
|
||||||
import { MimoFreeExecutor } from "./mimo-free.js";
|
import { MimoFreeExecutor } from "./mimo-free.js";
|
||||||
|
import { CodeBuddyExecutor } from "./codebuddy-cn.js";
|
||||||
import { DefaultExecutor } from "./default.js";
|
import { DefaultExecutor } from "./default.js";
|
||||||
|
|
||||||
const executors = {
|
const executors = {
|
||||||
@@ -42,6 +43,7 @@ const executors = {
|
|||||||
"xiaomi-tokenplan": new XiaomiTokenplanExecutor(),
|
"xiaomi-tokenplan": new XiaomiTokenplanExecutor(),
|
||||||
"mimo-free": new MimoFreeExecutor(),
|
"mimo-free": new MimoFreeExecutor(),
|
||||||
mmf: new MimoFreeExecutor(), // Alias for mimo-free
|
mmf: new MimoFreeExecutor(), // Alias for mimo-free
|
||||||
|
"codebuddy-cn": new CodeBuddyExecutor(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultCache = new Map();
|
const defaultCache = new Map();
|
||||||
@@ -77,3 +79,4 @@ export { OllamaLocalExecutor } from "./ollama-local.js";
|
|||||||
export { CommandCodeExecutor } from "./commandcode.js";
|
export { CommandCodeExecutor } from "./commandcode.js";
|
||||||
export { XiaomiTokenplanExecutor } from "./xiaomi-tokenplan.js";
|
export { XiaomiTokenplanExecutor } from "./xiaomi-tokenplan.js";
|
||||||
export { MimoFreeExecutor } from "./mimo-free.js";
|
export { MimoFreeExecutor } from "./mimo-free.js";
|
||||||
|
export { CodeBuddyExecutor } from "./codebuddy-cn.js";
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { BaseExecutor } from "./base.js";
|
import { BaseExecutor } from "./base.js";
|
||||||
import { PROVIDERS } from "../config/providers.js";
|
import { PROVIDERS } from "../config/providers.js";
|
||||||
|
import { resolveKiroModel } from "../config/kiroConstants.js";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { refreshKiroToken } from "../services/tokenRefresh.js";
|
import { refreshKiroToken } from "../services/tokenRefresh.js";
|
||||||
|
import { SSE_DONE, SSE_HEADERS } from "../utils/sseConstants.js";
|
||||||
|
import { getCapabilitiesForModel } from "../providers/capabilities.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KiroExecutor - Executor for Kiro AI (AWS CodeWhisperer)
|
* KiroExecutor - Executor for Kiro AI (AWS CodeWhisperer)
|
||||||
@@ -19,13 +22,60 @@ export class KiroExecutor extends BaseExecutor {
|
|||||||
"Amz-Sdk-Invocation-Id": uuidv4()
|
"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.
|
||||||
|
// Enterprise / Microsoft Entra (external_idp) tokens are OAuth access tokens,
|
||||||
|
// but CodeWhisperer requires TokenType=EXTERNAL_IDP to bind them to profiles.
|
||||||
|
const authMethod = credentials?.providerSpecificData?.authMethod;
|
||||||
|
const isApiKey = authMethod === "api_key";
|
||||||
|
const isExternalIdp = authMethod === "external_idp";
|
||||||
|
|
||||||
|
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}`;
|
headers["Authorization"] = `Bearer ${credentials.accessToken}`;
|
||||||
|
if (isExternalIdp) {
|
||||||
|
headers["TokenType"] = "EXTERNAL_IDP";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return headers;
|
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. External IdP enterprise tokens also
|
||||||
|
* use the CodeWhisperer surface, with the `TokenType: EXTERNAL_IDP` header.
|
||||||
|
* Other OAuth methods keep the default order (kiro.dev first) since their
|
||||||
|
* tokens are what that gateway accepts.
|
||||||
|
*/
|
||||||
|
getOrderedBaseUrls(credentials) {
|
||||||
|
const baseUrls = this.getBaseUrls();
|
||||||
|
const authMethod = credentials?.providerSpecificData?.authMethod;
|
||||||
|
const isCodeWhispererSurface = authMethod === "api_key" || authMethod === "external_idp";
|
||||||
|
if (!isCodeWhispererSurface) 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) {
|
transformRequest(model, body, stream, credentials) {
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
@@ -37,6 +87,8 @@ export class KiroExecutor extends BaseExecutor {
|
|||||||
* BaseExecutor.execute() walks config.baseUrls (runtime.us-east-1.kiro.dev →
|
* BaseExecutor.execute() walks config.baseUrls (runtime.us-east-1.kiro.dev →
|
||||||
* codewhisperer → q) advancing to the next host on 429 (shouldRetry) and on
|
* 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}`.
|
* 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
|
* 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
|
* 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.
|
* spreading is handled upstream by account rotation in sse/handlers/chat.js.
|
||||||
@@ -61,6 +113,8 @@ export class KiroExecutor extends BaseExecutor {
|
|||||||
let chunkIndex = 0;
|
let chunkIndex = 0;
|
||||||
const responseId = `chatcmpl-${Date.now()}`;
|
const responseId = `chatcmpl-${Date.now()}`;
|
||||||
const created = Math.floor(Date.now() / 1000);
|
const created = Math.floor(Date.now() / 1000);
|
||||||
|
const capabilityModel = resolveKiroModel(model).upstream;
|
||||||
|
const contextWindow = getCapabilitiesForModel("kiro", capabilityModel).contextWindow || 200000;
|
||||||
const state = {
|
const state = {
|
||||||
endDetected: false,
|
endDetected: false,
|
||||||
finishEmitted: false,
|
finishEmitted: false,
|
||||||
@@ -73,6 +127,8 @@ export class KiroExecutor extends BaseExecutor {
|
|||||||
|
|
||||||
const transformStream = new TransformStream({
|
const transformStream = new TransformStream({
|
||||||
async transform(chunk, controller) {
|
async transform(chunk, controller) {
|
||||||
|
// Track output so we can emit a keepalive if this frame yields no chunk.
|
||||||
|
const enqueueCountBefore = chunkIndex;
|
||||||
// Append to buffer
|
// Append to buffer
|
||||||
const newBuffer = new Uint8Array(buffer.length + chunk.length);
|
const newBuffer = new Uint8Array(buffer.length + chunk.length);
|
||||||
newBuffer.set(buffer);
|
newBuffer.set(buffer);
|
||||||
@@ -96,7 +152,7 @@ export class KiroExecutor extends BaseExecutor {
|
|||||||
if (!event) continue;
|
if (!event) continue;
|
||||||
|
|
||||||
const eventType = event.headers[":event-type"] || "";
|
const eventType = event.headers[":event-type"] || "";
|
||||||
|
|
||||||
// Track total content length for token estimation
|
// Track total content length for token estimation
|
||||||
if (!state.totalContentLength) state.totalContentLength = 0;
|
if (!state.totalContentLength) state.totalContentLength = 0;
|
||||||
if (!state.contextUsagePercentage) state.contextUsagePercentage = 0;
|
if (!state.contextUsagePercentage) state.contextUsagePercentage = 0;
|
||||||
@@ -105,7 +161,7 @@ export class KiroExecutor extends BaseExecutor {
|
|||||||
if (eventType === "assistantResponseEvent" && event.payload?.content) {
|
if (eventType === "assistantResponseEvent" && event.payload?.content) {
|
||||||
const content = event.payload.content;
|
const content = event.payload.content;
|
||||||
state.totalContentLength += content.length;
|
state.totalContentLength += content.length;
|
||||||
|
|
||||||
const chunk = {
|
const chunk = {
|
||||||
id: responseId,
|
id: responseId,
|
||||||
object: "chat.completion.chunk",
|
object: "chat.completion.chunk",
|
||||||
@@ -292,7 +348,7 @@ export class KiroExecutor extends BaseExecutor {
|
|||||||
if (metrics && typeof metrics === 'object') {
|
if (metrics && typeof metrics === 'object') {
|
||||||
const inputTokens = metrics.inputTokens || 0;
|
const inputTokens = metrics.inputTokens || 0;
|
||||||
const outputTokens = metrics.outputTokens || 0;
|
const outputTokens = metrics.outputTokens || 0;
|
||||||
|
|
||||||
if (inputTokens > 0 || outputTokens > 0) {
|
if (inputTokens > 0 || outputTokens > 0) {
|
||||||
state.usage = {
|
state.usage = {
|
||||||
prompt_tokens: inputTokens,
|
prompt_tokens: inputTokens,
|
||||||
@@ -306,27 +362,26 @@ export class KiroExecutor extends BaseExecutor {
|
|||||||
// Emit final chunk only after receiving BOTH meteringEvent AND contextUsageEvent
|
// Emit final chunk only after receiving BOTH meteringEvent AND contextUsageEvent
|
||||||
if (state.hasMeteringEvent && state.hasContextUsage && !state.finishEmitted) {
|
if (state.hasMeteringEvent && state.hasContextUsage && !state.finishEmitted) {
|
||||||
state.finishEmitted = true;
|
state.finishEmitted = true;
|
||||||
|
|
||||||
// Estimate tokens if not available from events
|
// Estimate tokens if not available from events
|
||||||
if (!state.usage) {
|
if (!state.usage) {
|
||||||
// Estimate output tokens from content length
|
// Estimate output tokens from content length
|
||||||
const estimatedOutputTokens = state.totalContentLength > 0
|
const estimatedOutputTokens = state.totalContentLength > 0
|
||||||
? Math.max(1, Math.floor(state.totalContentLength / 4))
|
? Math.max(1, Math.floor(state.totalContentLength / 4))
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
// Estimate input tokens from contextUsagePercentage
|
// Estimate input tokens from contextUsagePercentage
|
||||||
// Kiro models typically have 200k context window
|
|
||||||
const estimatedInputTokens = state.contextUsagePercentage > 0
|
const estimatedInputTokens = state.contextUsagePercentage > 0
|
||||||
? Math.floor(state.contextUsagePercentage * 200000 / 100)
|
? Math.floor(state.contextUsagePercentage * contextWindow / 100)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
state.usage = {
|
state.usage = {
|
||||||
prompt_tokens: estimatedInputTokens,
|
prompt_tokens: estimatedInputTokens,
|
||||||
completion_tokens: estimatedOutputTokens,
|
completion_tokens: estimatedOutputTokens,
|
||||||
total_tokens: estimatedInputTokens + estimatedOutputTokens
|
total_tokens: estimatedInputTokens + estimatedOutputTokens
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const finishChunk = {
|
const finishChunk = {
|
||||||
id: responseId,
|
id: responseId,
|
||||||
object: "chat.completion.chunk",
|
object: "chat.completion.chunk",
|
||||||
@@ -338,12 +393,12 @@ export class KiroExecutor extends BaseExecutor {
|
|||||||
finish_reason: state.hasToolCalls ? "tool_calls" : "stop"
|
finish_reason: state.hasToolCalls ? "tool_calls" : "stop"
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Include usage in final chunk if available
|
// Include usage in final chunk if available
|
||||||
if (state.usage) {
|
if (state.usage) {
|
||||||
finishChunk.usage = state.usage;
|
finishChunk.usage = state.usage;
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(finishChunk)}\n\n`));
|
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(finishChunk)}\n\n`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -351,6 +406,12 @@ export class KiroExecutor extends BaseExecutor {
|
|||||||
if (iterations >= maxIterations) {
|
if (iterations >= maxIterations) {
|
||||||
console.warn("[Kiro] Max iterations reached in event parsing");
|
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) {
|
flush(controller) {
|
||||||
@@ -372,24 +433,20 @@ export class KiroExecutor extends BaseExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send final done message
|
// 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
|
// Pipe response body through transform stream
|
||||||
if (!response.body) {
|
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);
|
const transformedStream = response.body.pipeThrough(transformStream);
|
||||||
|
|
||||||
return new Response(transformedStream, {
|
return new Response(transformedStream, {
|
||||||
status: response.status,
|
status: response.status,
|
||||||
statusText: response.statusText,
|
statusText: response.statusText,
|
||||||
headers: {
|
headers: { ...SSE_HEADERS }
|
||||||
"Content-Type": "text/event-stream",
|
|
||||||
"Cache-Control": "no-cache",
|
|
||||||
"Connection": "keep-alive"
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,20 @@ import { createHash } from "crypto";
|
|||||||
import os from "os";
|
import os from "os";
|
||||||
|
|
||||||
const BOOTSTRAP_URL = "https://api.xiaomimimo.com/api/free-ai/bootstrap";
|
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_AFFINITY_PREFIX = "ses_";
|
||||||
const SESSION_ID_LENGTH = 24;
|
const SESSION_ID_LENGTH = 24;
|
||||||
const JWT_FALLBACK_TTL_SEC = 3000;
|
const JWT_FALLBACK_TTL_SEC = 3000;
|
||||||
const JWT_EXPIRY_BUFFER_MS = 300000;
|
const JWT_EXPIRY_BUFFER_MS = 300000;
|
||||||
const SESSION_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
|
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"
|
// Anti-abuse gate marker: the free chat endpoint returns 403 "Illegal access"
|
||||||
// unless a system message contains this exact MiMoCode signature substring.
|
// unless a system message contains this exact MiMoCode signature substring.
|
||||||
export const MIMO_SYSTEM_MARKER =
|
export const MIMO_SYSTEM_MARKER =
|
||||||
@@ -76,7 +83,10 @@ async function bootstrapJwt(proxyOptions = null) {
|
|||||||
|
|
||||||
const response = await proxyAwareFetch(BOOTSTRAP_URL, {
|
const response = await proxyAwareFetch(BOOTSTRAP_URL, {
|
||||||
method: "POST",
|
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() }),
|
body: JSON.stringify({ client: generateFingerprint() }),
|
||||||
}, proxyOptions);
|
}, proxyOptions);
|
||||||
|
|
||||||
@@ -108,6 +118,7 @@ export class MimoFreeExecutor extends BaseExecutor {
|
|||||||
return {
|
return {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-Mimo-Source": "mimocode-cli-free",
|
"X-Mimo-Source": "mimocode-cli-free",
|
||||||
|
"User-Agent": USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)],
|
||||||
"x-session-affinity": this.sessionId,
|
"x-session-affinity": this.sessionId,
|
||||||
"Accept": stream ? "text/event-stream" : "application/json",
|
"Accept": stream ? "text/event-stream" : "application/json",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import { BaseExecutor } from "./base.js";
|
import { BaseExecutor } from "./base.js";
|
||||||
import { PROVIDERS } from "../config/providers.js";
|
import { PROVIDERS } from "../config/providers.js";
|
||||||
import { injectReasoningContent } from "../utils/reasoningContentInjector.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)
|
// 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";
|
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 runs before buildHeaders in BaseExecutor.execute, cache model here
|
||||||
buildUrl(model) {
|
buildUrl(model) {
|
||||||
this._lastModel = model;
|
this._lastModel = model;
|
||||||
return CLAUDE_FORMAT_MODELS.has(model)
|
return MESSAGES_FORMAT_MODELS.has(model)
|
||||||
? `${BASE}/messages`
|
? `${BASE}/messages`
|
||||||
: `${BASE}/chat/completions`;
|
: `${BASE}/chat/completions`;
|
||||||
}
|
}
|
||||||
@@ -24,9 +32,9 @@ export class OpenCodeGoExecutor extends BaseExecutor {
|
|||||||
const key = credentials?.apiKey || credentials?.accessToken;
|
const key = credentials?.apiKey || credentials?.accessToken;
|
||||||
const headers = { "Content-Type": "application/json" };
|
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["x-api-key"] = key;
|
||||||
headers["anthropic-version"] = "2023-06-01";
|
headers["anthropic-version"] = ANTHROPIC_API_VERSION;
|
||||||
} else {
|
} else {
|
||||||
headers["Authorization"] = `Bearer ${key}`;
|
headers["Authorization"] = `Bearer ${key}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export class OpenCodeExecutor extends BaseExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildUrl(model) {
|
buildUrl(model) {
|
||||||
const base = "https://opencode.ai";
|
const base = this.config.baseUrl;
|
||||||
return MESSAGES_MODELS.has(model)
|
return MESSAGES_MODELS.has(model)
|
||||||
? `${base}/zen/v1/messages`
|
? `${base}/zen/v1/messages`
|
||||||
: `${base}/zen/v1/chat/completions`;
|
: `${base}/zen/v1/chat/completions`;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { BaseExecutor } from "./base.js";
|
import { BaseExecutor } from "./base.js";
|
||||||
import { PROVIDERS } from "../config/providers.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_SSE_ENDPOINT = PROVIDERS["perplexity-web"].baseUrl;
|
||||||
const PPLX_API_VERSION = "2.18";
|
const PPLX_API_VERSION = "2.18";
|
||||||
@@ -289,10 +291,6 @@ async function* extractContent(eventStream, signal) {
|
|||||||
yield { delta: "", answer: fullAnswer, backendUuid: backendUuid ?? undefined, done: true };
|
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) {
|
function buildStreamingResponse(eventStream, model, cid, created, history, currentMsg, signal) {
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
return new ReadableStream({
|
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,
|
id: cid, object: "chat.completion.chunk", created, model, system_fingerprint: null,
|
||||||
choices: [{ index: 0, delta: {}, finish_reason: "stop", logprobs: 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);
|
sessionStore(history, currentMsg, cleanResponse(fullAnswer), respBackendUuid);
|
||||||
} catch (err) {
|
} 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,
|
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 }],
|
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 {
|
} finally {
|
||||||
controller.close();
|
controller.close();
|
||||||
}
|
}
|
||||||
@@ -493,7 +491,7 @@ export class PerplexityWebExecutor extends BaseExecutor {
|
|||||||
const sseStream = buildStreamingResponse(response.body, model, cid, created, parsed.history, parsed.currentMsg, signal);
|
const sseStream = buildStreamingResponse(response.body, model, cid, created, parsed.history, parsed.currentMsg, signal);
|
||||||
finalResponse = new Response(sseStream, {
|
finalResponse = new Response(sseStream, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "X-Accel-Buffering": "no" },
|
headers: { ...SSE_HEADERS_NO_BUFFER },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
finalResponse = await buildNonStreamingResponse(response.body, model, cid, created, parsed.history, parsed.currentMsg, signal);
|
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.
|
* different model upstream, so a missing entry is a hard error.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { qoderEncodeBody } from "@/lib/qoder/encoding.js";
|
import { qoderEncodeBody } from "../shared/qoder/encoding.js";
|
||||||
import { buildCosyHeaders } from "@/lib/qoder/cosy.js";
|
import { buildCosyHeaders } from "../shared/qoder/cosy.js";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
|
|
||||||
import { BaseExecutor } from "./base.js";
|
import { BaseExecutor } from "./base.js";
|
||||||
import { PROVIDERS } from "../config/providers.js";
|
import { PROVIDERS } from "../config/providers.js";
|
||||||
import { proxyAwareFetch } from "../utils/proxyFetch.js";
|
import { proxyAwareFetch } from "../utils/proxyFetch.js";
|
||||||
|
import { SSE_DONE } from "../utils/sseConstants.js";
|
||||||
import { FETCH_CONNECT_TIMEOUT_MS } from "../config/runtimeConfig.js";
|
import { FETCH_CONNECT_TIMEOUT_MS } from "../config/runtimeConfig.js";
|
||||||
import {
|
import {
|
||||||
QODER_CHAT_URL_ENCODED,
|
QODER_CHAT_URL_ENCODED,
|
||||||
QODER_MODEL_MAP,
|
QODER_MODEL_MAP,
|
||||||
} from "@/lib/qoder/constants.js";
|
} from "../shared/qoder/constants.js";
|
||||||
import { getQoderModelConfig, resolveQoderModels } from "../services/qoderModels.js";
|
import { getQoderModelConfig, resolveQoderModels } from "../services/qoderModels.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -294,7 +295,7 @@ async function wrapQoderSSE(response, model) {
|
|||||||
|
|
||||||
const data = trimmed.slice(5).trimStart();
|
const data = trimmed.slice(5).trimStart();
|
||||||
if (data === "[DONE]") {
|
if (data === "[DONE]") {
|
||||||
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
controller.enqueue(encoder.encode(SSE_DONE));
|
||||||
doneEmitted = true;
|
doneEmitted = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -313,13 +314,13 @@ async function wrapQoderSSE(response, model) {
|
|||||||
choices: [{ index: 0, delta: { content: `\n[qoder error ${statusVal}: ${truncate(msg, 200)}]` }, finish_reason: "stop" }],
|
choices: [{ index: 0, delta: { content: `\n[qoder error ${statusVal}: ${truncate(msg, 200)}]` }, finish_reason: "stop" }],
|
||||||
});
|
});
|
||||||
controller.enqueue(encoder.encode(`data: ${errChunk}\n\n`));
|
controller.enqueue(encoder.encode(`data: ${errChunk}\n\n`));
|
||||||
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
controller.enqueue(encoder.encode(SSE_DONE));
|
||||||
doneEmitted = true;
|
doneEmitted = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!inner) return;
|
if (!inner) return;
|
||||||
if (inner === "[DONE]") {
|
if (inner === "[DONE]") {
|
||||||
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
controller.enqueue(encoder.encode(SSE_DONE));
|
||||||
doneEmitted = true;
|
doneEmitted = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -344,7 +345,7 @@ async function wrapQoderSSE(response, model) {
|
|||||||
buffer = "";
|
buffer = "";
|
||||||
}
|
}
|
||||||
if (!doneEmitted) {
|
if (!doneEmitted) {
|
||||||
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
controller.enqueue(encoder.encode(SSE_DONE));
|
||||||
doneEmitted = true;
|
doneEmitted = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import { DefaultExecutor } from "./default.js";
|
import { DefaultExecutor } from "./default.js";
|
||||||
import { resolveXiaomiTokenplanBaseUrl } from "../config/providers.js";
|
import { resolveXiaomiTokenplanBaseUrl } from "../config/providers.js";
|
||||||
import { getModelTargetFormat } from "../config/providerModels.js";
|
// import { getModelTargetFormat } from "../config/providerModels.js";
|
||||||
import { FORMATS } from "../translator/formats.js";
|
// import { FORMATS } from "../translator/formats.js";
|
||||||
|
|
||||||
export class XiaomiTokenplanExecutor extends DefaultExecutor {
|
export class XiaomiTokenplanExecutor extends DefaultExecutor {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("xiaomi-tokenplan");
|
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) {
|
buildUrl(model, stream, urlIndex = 0, credentials = null) {
|
||||||
const baseUrl = resolveXiaomiTokenplanBaseUrl(credentials);
|
const baseUrl = resolveXiaomiTokenplanBaseUrl(credentials);
|
||||||
if (getModelTargetFormat(model, model) === FORMATS.CLAUDE) {
|
if (credentials?.runtimeTransport?.format === "claude") {
|
||||||
return `${baseUrl.replace(/\/v1\/?$/, "/anthropic/v1")}/messages`;
|
return `${baseUrl.replace(/\/v1\/?$/, "")}/anthropic/v1/messages`;
|
||||||
}
|
}
|
||||||
return `${baseUrl}/chat/completions`;
|
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 { translateRequest } from "../translator/index.js";
|
||||||
import { FORMATS } from "../translator/formats.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 { COLORS } from "../utils/stream.js";
|
||||||
import { createStreamController } from "../utils/streamHandler.js";
|
import { createStreamController } from "../utils/streamHandler.js";
|
||||||
import { refreshWithRetry } from "../services/tokenRefresh.js";
|
import { refreshWithRetry } from "../services/tokenRefresh.js";
|
||||||
import { createRequestLogger } from "../utils/requestLogger.js";
|
import { createRequestLogger } from "../utils/requestLogger.js";
|
||||||
import { getModelTargetFormat, getModelStrip, getModelUpstreamId, getModelType, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.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 { createErrorResult, parseUpstreamError, formatProviderError } from "../utils/error.js";
|
||||||
import { HTTP_STATUS } from "../config/runtimeConfig.js";
|
import { HTTP_STATUS } from "../config/runtimeConfig.js";
|
||||||
import { handleBypassRequest } from "../utils/bypassHandler.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 { detectClientTool, isNativePassthrough } from "../utils/clientDetector.js";
|
||||||
import { dedupeTools } from "../utils/toolDeduper.js";
|
import { dedupeTools } from "../utils/toolDeduper.js";
|
||||||
import { injectCaveman } from "../rtk/caveman.js";
|
import { injectCaveman } from "../rtk/caveman.js";
|
||||||
|
import { injectPonytail } from "../rtk/ponytail.js";
|
||||||
import { compressMessages, formatRtkLog } from "../rtk/index.js";
|
import { compressMessages, formatRtkLog } from "../rtk/index.js";
|
||||||
|
import { compressWithHeadroom, formatHeadroomLog, formatHeadroomSizeLog, isHeadroomPhantomSavings } 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
|
* 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 {object} options.credentials - Provider credentials
|
||||||
* @param {string} options.sourceFormatOverride - Override detected source format (e.g. "openai-responses")
|
* @param {string} options.sourceFormatOverride - Override detected source format (e.g. "openai-responses")
|
||||||
*/
|
*/
|
||||||
export async function handleChatCore({ body, modelInfo, credentials, log, onCredentialsRefreshed, onRequestSuccess, onDisconnect, clientRawRequest, connectionId, userAgent, apiKey, ccFilterNaming, rtkEnabled, cavemanEnabled, cavemanLevel, sourceFormatOverride, providerThinking }) {
|
export async function handleChatCore({ body, modelInfo, credentials, log, onCredentialsRefreshed, onRequestSuccess, onDisconnect, clientRawRequest, connectionId, userAgent, apiKey, ccFilterNaming, rtkEnabled, headroomEnabled, headroomUrl, headroomCompressUserMessages, cavemanEnabled, cavemanLevel, ponytailEnabled, ponytailLevel, sourceFormatOverride, providerThinking }) {
|
||||||
const { provider, model } = modelInfo;
|
const { provider, model } = modelInfo;
|
||||||
const requestStartTime = Date.now();
|
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 alias = PROVIDER_ID_TO_ALIAS[provider] || provider;
|
||||||
const modelTargetFormat = getModelTargetFormat(alias, model);
|
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 stripList = getModelStrip(alias, model);
|
||||||
const upstreamModel = getModelUpstreamId(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 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);
|
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.
|
// DeepSeek-TUI: interactive TUI panel sends stream:true and needs SSE.
|
||||||
// Non-interactive mode (-p flag) sends without stream and can't parse 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.
|
// Only force non-streaming when client didn't explicitly request it.
|
||||||
@@ -73,7 +89,7 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
|
|||||||
const acceptHeader = clientRawRequest?.headers?.accept || "";
|
const acceptHeader = clientRawRequest?.headers?.accept || "";
|
||||||
const clientPrefersJson = acceptHeader.includes("application/json");
|
const clientPrefersJson = acceptHeader.includes("application/json");
|
||||||
const clientPrefersSSE = acceptHeader.includes("text/event-stream");
|
const clientPrefersSSE = acceptHeader.includes("text/event-stream");
|
||||||
if (clientPrefersJson && !clientPrefersSSE && body.stream !== true) {
|
if (clientPrefersJson && !clientPrefersSSE && body.stream !== true && !providerRequiresStreaming) {
|
||||||
stream = false;
|
stream = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +103,22 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
|
|||||||
const clientTool = detectClientTool(clientRawRequest?.headers || {}, body);
|
const clientTool = detectClientTool(clientRawRequest?.headers || {}, body);
|
||||||
const passthrough = isNativePassthrough(clientTool, provider);
|
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 translatedBody;
|
||||||
let toolNameMap;
|
let toolNameMap;
|
||||||
if (passthrough) {
|
if (passthrough) {
|
||||||
@@ -129,12 +161,30 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
|
|||||||
const rtkLine = formatRtkLog(rtkStats);
|
const rtkLine = formatRtkLog(rtkStats);
|
||||||
if (rtkLine) console.log(rtkLine);
|
if (rtkLine) console.log(rtkLine);
|
||||||
|
|
||||||
|
// Headroom: optional external proxy compression; fail open if proxy is absent.
|
||||||
|
const headroomDiagnostics = {};
|
||||||
|
const headroomStats = await compressWithHeadroom(translatedBody, { enabled: headroomEnabled, url: headroomUrl, model: upstreamModel, format: finalFormat, compressUserMessages: headroomCompressUserMessages, diagnostics: headroomDiagnostics });
|
||||||
|
const headroomLine = formatHeadroomLog(headroomStats);
|
||||||
|
const headroomSizeLine = formatHeadroomSizeLog(headroomDiagnostics);
|
||||||
|
if (headroomLine) {
|
||||||
|
log?.info?.("HEADROOM", `${headroomLine}${headroomSizeLine ? ` | ${headroomSizeLine}` : ""}`);
|
||||||
|
if (isHeadroomPhantomSavings(headroomStats, headroomDiagnostics)) {
|
||||||
|
log?.warn?.("HEADROOM", `reported token delta, but outbound JSON shrank <5%; provider may bill near-original payload | ${headroomSizeLine}`);
|
||||||
|
}
|
||||||
|
} else if (headroomEnabled) log?.warn?.("HEADROOM", `skipped: ${headroomDiagnostics.reason || "compression unavailable"}${headroomDiagnostics.endpoint ? ` (${headroomDiagnostics.endpoint})` : ""}`);
|
||||||
|
|
||||||
// Caveman: inject terse-style system prompt
|
// Caveman: inject terse-style system prompt
|
||||||
if (cavemanEnabled && cavemanLevel) {
|
if (cavemanEnabled && cavemanLevel) {
|
||||||
injectCaveman(translatedBody, finalFormat, cavemanLevel);
|
injectCaveman(translatedBody, finalFormat, cavemanLevel);
|
||||||
log?.debug?.("CAVEMAN", `${cavemanLevel} | ${finalFormat}`);
|
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);
|
const executor = getExecutor(provider);
|
||||||
trackPendingRequest(model, provider, connectionId, true);
|
trackPendingRequest(model, provider, connectionId, true);
|
||||||
appendRequestLog({ model, provider, connectionId, status: "PENDING" }).catch(() => { });
|
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 || {}) }
|
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
|
// 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
|
// and returns `content: null`). Returning the raw body would leave the
|
||||||
// OpenAI client without a `choices` array and surface as a UI test error.
|
// 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 = "";
|
let textContent = "", thinkingContent = "";
|
||||||
const toolCalls = [];
|
const toolCalls = [];
|
||||||
@@ -156,7 +167,13 @@ export async function handleNonStreamingResponse({ providerResponse, provider, m
|
|||||||
}
|
}
|
||||||
|
|
||||||
reqLogger.logProviderResponse(providerResponse.status, providerResponse.statusText, providerResponse.headers, responseBody);
|
reqLogger.logProviderResponse(providerResponse.status, providerResponse.statusText, providerResponse.headers, responseBody);
|
||||||
if (onRequestSuccess) await onRequestSuccess();
|
if (onRequestSuccess) {
|
||||||
|
Promise.resolve()
|
||||||
|
.then(onRequestSuccess)
|
||||||
|
.catch(err => {
|
||||||
|
console.error("[ChatCore] onRequestSuccess failed:", err?.message || err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Decloak tool_use names once on raw Claude body, before any translation (INPUT side)
|
// Decloak tool_use names once on raw Claude body, before any translation (INPUT side)
|
||||||
responseBody = decloakToolNames(responseBody, toolNameMap);
|
responseBody = decloakToolNames(responseBody, toolNameMap);
|
||||||
@@ -193,11 +210,14 @@ export async function handleNonStreamingResponse({ providerResponse, provider, m
|
|||||||
translatedResponse.usage = filterUsageForFormat(addBufferToUsage(translatedResponse.usage), sourceFormat);
|
translatedResponse.usage = filterUsageForFormat(addBufferToUsage(translatedResponse.usage), sourceFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip reasoning_content — some clients (e.g. Firecrawl AI SDK) have JSON parsers that
|
// Strip reasoning_content only when content is non-empty.
|
||||||
// break on this non-standard field, even though OpenAI allows it in extensions.
|
// 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) {
|
if (translatedResponse?.choices) {
|
||||||
for (const choice of 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 { createErrorResult } from "../../utils/error.js";
|
||||||
import { HTTP_STATUS } from "../../config/runtimeConfig.js";
|
import { HTTP_STATUS } from "../../config/runtimeConfig.js";
|
||||||
import { FORMATS } from "../../translator/formats.js";
|
import { FORMATS } from "../../translator/formats.js";
|
||||||
|
import { PROVIDERS } from "../../config/providers.js";
|
||||||
import { buildRequestDetail, extractRequestConfig, saveUsageStats } from "./requestDetail.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";
|
import { saveRequestDetail, appendRequestLog } from "@/lib/usageDb.js";
|
||||||
|
|
||||||
function textFromResponsesMessageItem(item) {
|
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 }) {
|
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 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
|
if (!isSSE) return null; // not handled here
|
||||||
|
|
||||||
trackDone();
|
trackDone();
|
||||||
@@ -112,7 +116,7 @@ export async function handleForcedSSEToJson({ providerResponse, sourceFormat, pr
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Codex/Responses API SSE path
|
// Codex/Responses API SSE path
|
||||||
const isCodexResponsesApi = provider === "codex" || sourceFormat === FORMATS.OPENAI_RESPONSES;
|
const isCodexResponsesApi = isResponsesProvider(provider) || sourceFormat === FORMATS.OPENAI_RESPONSES;
|
||||||
if (isCodexResponsesApi) {
|
if (isCodexResponsesApi) {
|
||||||
try {
|
try {
|
||||||
const jsonResponse = await convertResponsesStreamToJson(providerResponse.body);
|
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 { buildAbortedResponsesTerminalBytes } from "../../utils/responsesStreamHelpers.js";
|
||||||
import { buildRequestDetail, extractRequestConfig } from "./requestDetail.js";
|
import { buildRequestDetail, extractRequestConfig } from "./requestDetail.js";
|
||||||
import { saveRequestDetail } from "@/lib/usageDb.js";
|
import { saveRequestDetail } from "@/lib/usageDb.js";
|
||||||
|
import { SSE_HEADERS_CORS as SSE_HEADERS } from "../../utils/sseConstants.js";
|
||||||
|
|
||||||
const SSE_HEADERS = {
|
// Codex returns Responses API SSE → which client format to translate INTO, by request sourceFormat.
|
||||||
"Content-Type": "text/event-stream",
|
// Gemini-family all map to ANTIGRAVITY decoder; unknown sources fall back to OPENAI.
|
||||||
"Cache-Control": "no-cache",
|
const CODEX_SOURCE_TO_TARGET = {
|
||||||
"Connection": "keep-alive",
|
[FORMATS.OPENAI_RESPONSES]: FORMATS.OPENAI_RESPONSES,
|
||||||
"Access-Control-Allow-Origin": "*"
|
[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 }) {
|
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 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) {
|
if (needsCodexTranslation) {
|
||||||
// Codex returns Responses API SSE → translate to client format
|
const codexTarget = CODEX_SOURCE_TO_TARGET[sourceFormat] || FORMATS.OPENAI;
|
||||||
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;
|
|
||||||
return createSSETransformStreamWithLogger(FORMATS.OPENAI_RESPONSES, codexTarget, provider, reqLogger, toolNameMap, model, connectionId, body, onStreamComplete, apiKey);
|
return createSSETransformStreamWithLogger(FORMATS.OPENAI_RESPONSES, codexTarget, provider, reqLogger, toolNameMap, model, connectionId, body, onStreamComplete, apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +44,21 @@ function buildTransformStream({ provider, sourceFormat, targetFormat, userAgent,
|
|||||||
* Handle streaming response — pipe provider SSE through transform stream to client.
|
* Handle streaming response — pipe provider SSE through transform stream to client.
|
||||||
*/
|
*/
|
||||||
export function handleStreamingResponse({ providerResponse, provider, model, sourceFormat, targetFormat, userAgent, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess, reqLogger, toolNameMap, streamController, onStreamComplete }) {
|
export function handleStreamingResponse({ providerResponse, provider, model, sourceFormat, targetFormat, userAgent, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess, reqLogger, toolNameMap, streamController, onStreamComplete }) {
|
||||||
if (onRequestSuccess) onRequestSuccess();
|
if (onRequestSuccess) {
|
||||||
|
Promise.resolve()
|
||||||
|
.then(onRequestSuccess)
|
||||||
|
.catch(err => {
|
||||||
|
console.error("[ChatCore] onRequestSuccess failed:", err?.message || err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn when upstream returns unexpected Content-Type for a streaming response.
|
||||||
|
// This often means the provider returned an HTML error page or plain-text error
|
||||||
|
// that the SSE transform stream would forward as garbage to the client.
|
||||||
|
const upstreamContentType = (providerResponse.headers.get('content-type') || '').toLowerCase();
|
||||||
|
if (upstreamContentType && !upstreamContentType.includes('text/event-stream') && !upstreamContentType.includes('application/json')) {
|
||||||
|
console.warn('[STREAM] ' + provider + ' | ' + model + ' | unexpected Content-Type: ' + upstreamContentType);
|
||||||
|
}
|
||||||
|
|
||||||
const transformStream = buildTransformStream({ provider, sourceFormat, targetFormat, userAgent, reqLogger, toolNameMap, model, connectionId, body, onStreamComplete, apiKey });
|
const transformStream = buildTransformStream({ provider, sourceFormat, targetFormat, userAgent, reqLogger, toolNameMap, model, connectionId, body, onStreamComplete, apiKey });
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,21 @@
|
|||||||
// OpenAI-compatible embeddings adapter (most providers)
|
// OpenAI-compatible embeddings adapter (most providers)
|
||||||
import { bearerAuth } from "./_base.js";
|
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 = {
|
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",
|
"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) {
|
export default function createOpenAIEmbeddingAdapter(providerId) {
|
||||||
|
const cfg = embedCfg(providerId);
|
||||||
return {
|
return {
|
||||||
buildUrl: () => ENDPOINTS[providerId],
|
buildUrl: () => embedUrl(providerId),
|
||||||
buildHeaders: (creds) => {
|
buildHeaders: (creds) => {
|
||||||
const headers = { "Content-Type": "application/json", ...bearerAuth(creds) };
|
return { "Content-Type": "application/json", ...bearerAuth(creds), ...(cfg.headers || {}) };
|
||||||
if (providerId === "openrouter") {
|
|
||||||
headers["HTTP-Referer"] = "https://endpoint-proxy.local";
|
|
||||||
headers["X-Title"] = "Endpoint Proxy";
|
|
||||||
}
|
|
||||||
return headers;
|
|
||||||
},
|
},
|
||||||
buildBody: (model, { input, encoding_format, dimensions }) => {
|
buildBody: (model, { input, encoding_format, dimensions }) => {
|
||||||
const body = { model, input };
|
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 url;
|
||||||
let headers;
|
let headers;
|
||||||
let requestBody;
|
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
|
// Black Forest Labs (FLUX) — async submit + polling_url
|
||||||
import { sleep, nowSec, POLL_INTERVAL_MS, POLL_TIMEOUT_MS } from "./_base.js";
|
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 {
|
export default {
|
||||||
async: true,
|
async: true,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { nowSec, urlToBase64 } from "./_base.js";
|
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([
|
const MULTIPART_MODELS = new Set([
|
||||||
"@cf/black-forest-labs/flux-2-dev",
|
"@cf/black-forest-labs/flux-2-dev",
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
// Codex (ChatGPT Plus/Pro) image generation via Responses API + SSE
|
// Codex (ChatGPT Plus/Pro) image generation via Responses API + SSE
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { nowSec } from "./_base.js";
|
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_USER_AGENT = "codex_cli_rs/0.136.0";
|
||||||
const CODEX_VERSION = "0.136.0";
|
const CODEX_VERSION = "0.136.0";
|
||||||
const CODEX_ORIGINATOR = "codex_cli_rs";
|
const CODEX_ORIGINATOR = "codex_cli_rs";
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
// ComfyUI — local, noAuth (placeholder; full graph workflow not implemented)
|
// 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 {
|
export default {
|
||||||
noAuth: true,
|
noAuth: true,
|
||||||
buildUrl: () => "http://localhost:8188",
|
buildUrl: () => BASE_URL,
|
||||||
buildHeaders: () => ({ "Content-Type": "application/json" }),
|
buildHeaders: () => ({ "Content-Type": "application/json" }),
|
||||||
buildBody: (_model, body) => ({ prompt: body.prompt }),
|
buildBody: (_model, body) => ({ prompt: body.prompt }),
|
||||||
normalize: (responseBody) => responseBody,
|
normalize: (responseBody) => responseBody,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// Fal.ai — async submit + queue polling
|
// Fal.ai — async submit + queue polling
|
||||||
import { sleep, nowSec, sizeToAspectRatio, POLL_INTERVAL_MS, POLL_TIMEOUT_MS } from "./_base.js";
|
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 {
|
export default {
|
||||||
async: true,
|
async: true,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// Google Gemini adapter (Nano Banana models)
|
// Google Gemini adapter (Nano Banana models)
|
||||||
import { nowSec } from "./_base.js";
|
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 {
|
export default {
|
||||||
buildUrl: (model, creds) => {
|
buildUrl: (model, creds) => {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// HuggingFace Inference API — returns binary image
|
// HuggingFace Inference API — returns binary image
|
||||||
import { nowSec } from "./_base.js";
|
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 {
|
export default {
|
||||||
buildUrl: (model) => `${BASE_URL}/${model}`,
|
buildUrl: (model) => `${BASE_URL}/${model}`,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import stabilityAi from "./stabilityAi.js";
|
|||||||
import blackForestLabs from "./blackForestLabs.js";
|
import blackForestLabs from "./blackForestLabs.js";
|
||||||
import runwayml from "./runwayml.js";
|
import runwayml from "./runwayml.js";
|
||||||
import cloudflareAi from "./cloudflareAi.js";
|
import cloudflareAi from "./cloudflareAi.js";
|
||||||
|
import antigravity from "./antigravity.js";
|
||||||
|
|
||||||
const ADAPTERS = {
|
const ADAPTERS = {
|
||||||
openai: createOpenAIAdapter("openai"),
|
openai: createOpenAIAdapter("openai"),
|
||||||
@@ -25,6 +26,7 @@ const ADAPTERS = {
|
|||||||
comfyui,
|
comfyui,
|
||||||
huggingface,
|
huggingface,
|
||||||
nanobanana,
|
nanobanana,
|
||||||
|
antigravity,
|
||||||
"fal-ai": falAi,
|
"fal-ai": falAi,
|
||||||
"stability-ai": stabilityAi,
|
"stability-ai": stabilityAi,
|
||||||
"black-forest-labs": blackForestLabs,
|
"black-forest-labs": blackForestLabs,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// NanoBanana API — async submit + poll record-info
|
// NanoBanana API — async submit + poll record-info
|
||||||
import { sleep, nowSec, sizeToAspectRatio, POLL_INTERVAL_MS, POLL_TIMEOUT_MS } from "./_base.js";
|
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 IMG_CFG = PROVIDER_MEDIA["nanobanana"]?.imageConfig || {};
|
||||||
const POLL_BASE = "https://api.nanobananaapi.ai/api/v1/nanobanana/record-info";
|
const SUBMIT_URL = IMG_CFG.baseUrl;
|
||||||
|
const POLL_BASE = IMG_CFG.pollUrl;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async: true,
|
async: true,
|
||||||
|
|||||||
@@ -1,40 +1,32 @@
|
|||||||
// OpenAI-compatible adapter (used by openai, minimax, openrouter, recraft)
|
// OpenAI-compatible adapter (used by openai, minimax, openrouter, recraft)
|
||||||
|
import { PROVIDER_MEDIA } from "../../providers/index.js";
|
||||||
|
|
||||||
const ENDPOINTS = {
|
const imageCfg = (id) => PROVIDER_MEDIA[id]?.imageConfig || {};
|
||||||
openai: "https://api.openai.com/v1/images/generations",
|
const imageUrl = (id) => imageCfg(id).baseUrl;
|
||||||
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",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function createOpenAIAdapter(providerId) {
|
export default function createOpenAIAdapter(providerId) {
|
||||||
|
const cfg = imageCfg(providerId);
|
||||||
return {
|
return {
|
||||||
buildUrl: () => ENDPOINTS[providerId],
|
buildUrl: () => imageUrl(providerId),
|
||||||
buildHeaders: (creds) => {
|
buildHeaders: (creds) => {
|
||||||
const headers = { "Content-Type": "application/json" };
|
const headers = { "Content-Type": "application/json", ...(cfg.headers || {}) };
|
||||||
const key = creds?.apiKey || creds?.accessToken;
|
const key = creds?.apiKey || creds?.accessToken;
|
||||||
if (key) headers["Authorization"] = `Bearer ${key}`;
|
if (key) headers["Authorization"] = `Bearer ${key}`;
|
||||||
if (providerId === "openrouter") {
|
|
||||||
headers["HTTP-Referer"] = "https://endpoint-proxy.local";
|
|
||||||
headers["X-Title"] = "Endpoint Proxy";
|
|
||||||
}
|
|
||||||
return headers;
|
return headers;
|
||||||
},
|
},
|
||||||
buildBody: (model, body) => {
|
buildBody: (model, body) => {
|
||||||
const { prompt, n = 1, size = "1024x1024", quality, style, response_format } = body;
|
const { prompt, n = 1, size = "1024x1024", quality, style, response_format } = body;
|
||||||
// xAI only accepts prompt, model, n, response_format
|
const full = { model, prompt, n, size };
|
||||||
if (providerId === "xai") {
|
if (quality) full.quality = quality;
|
||||||
const req = { model, prompt, n };
|
if (style) full.style = style;
|
||||||
if (response_format) req.response_format = response_format;
|
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;
|
return req;
|
||||||
}
|
}
|
||||||
const req = { model, prompt, n, size };
|
return full;
|
||||||
if (quality) req.quality = quality;
|
|
||||||
if (style) req.style = style;
|
|
||||||
if (response_format) req.response_format = response_format;
|
|
||||||
return req;
|
|
||||||
},
|
},
|
||||||
normalize: (responseBody) => responseBody,
|
normalize: (responseBody) => responseBody,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// Runway ML — async submit + /tasks/{id} polling
|
// Runway ML — async submit + /tasks/{id} polling
|
||||||
import { sleep, nowSec, sizeToAspectRatio, POLL_INTERVAL_MS, POLL_TIMEOUT_MS } from "./_base.js";
|
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 {
|
export default {
|
||||||
async: true,
|
async: true,
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
// SD WebUI (AUTOMATIC1111) — local, noAuth
|
// SD WebUI (AUTOMATIC1111) — local, noAuth
|
||||||
import { nowSec } from "./_base.js";
|
import { nowSec } from "./_base.js";
|
||||||
|
import { PROVIDER_MEDIA } from "../../providers/index.js";
|
||||||
|
|
||||||
|
const BASE_URL = PROVIDER_MEDIA["sdwebui"]?.imageConfig?.baseUrl;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
noAuth: true,
|
noAuth: true,
|
||||||
buildUrl: () => "http://localhost:7860/sdapi/v1/txt2img",
|
buildUrl: () => BASE_URL,
|
||||||
buildHeaders: () => ({ "Content-Type": "application/json" }),
|
buildHeaders: () => ({ "Content-Type": "application/json" }),
|
||||||
buildBody: (_model, body) => {
|
buildBody: (_model, body) => {
|
||||||
const { prompt, n = 1, size = "1024x1024" } = body;
|
const { prompt, n = 1, size = "1024x1024" } = body;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// Stability AI v2 — sync, returns { image: "<b64>" }
|
// Stability AI v2 — sync, returns { image: "<b64>" }
|
||||||
import { nowSec, sizeToAspectRatio } from "./_base.js";
|
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
|
// Map model id → endpoint segment
|
||||||
function modelToEndpoint(model) {
|
function modelToEndpoint(model) {
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { handleChatCore } from "./chatCore.js";
|
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 { createResponsesApiTransformStream } from "../transformer/responsesTransformer.js";
|
||||||
import { convertResponsesStreamToJson } from "../transformer/streamToJsonConverter.js";
|
import { convertResponsesStreamToJson } from "../transformer/streamToJsonConverter.js";
|
||||||
|
import { SSE_HEADERS_CORS } from "../utils/sseConstants.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle /v1/responses request
|
* Handle /v1/responses request
|
||||||
@@ -87,12 +88,7 @@ export async function handleResponsesCore({ body, modelInfo, credentials, log, o
|
|||||||
success: true,
|
success: true,
|
||||||
response: new Response(transformedBody, {
|
response: new Response(transformedBody, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: { ...SSE_HEADERS_CORS }
|
||||||
"Content-Type": "text/event-stream",
|
|
||||||
"Cache-Control": "no-cache",
|
|
||||||
"Connection": "keep-alive",
|
|
||||||
"Access-Control-Allow-Origin": "*"
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
* Wrap chat-completions endpoints (with built-in web search) into the unified
|
* Wrap chat-completions endpoints (with built-in web search) into the unified
|
||||||
* /v1/search response format. Supports gemini, openai, xai, kimi, minimax, perplexity.
|
* /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 REQUEST_TIMEOUT_MS = 15000;
|
||||||
const DEFAULT_MAX_RESULTS = 10;
|
const DEFAULT_MAX_RESULTS = 10;
|
||||||
@@ -43,9 +49,7 @@ function normalizeCitation(c) {
|
|||||||
*/
|
*/
|
||||||
const CHAT_SEARCH_CONFIG = {
|
const CHAT_SEARCH_CONFIG = {
|
||||||
gemini: {
|
gemini: {
|
||||||
endpoint: (model) =>
|
endpoint: (model) => searchEndpoint("gemini", model),
|
||||||
`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`,
|
|
||||||
defaultModel: "gemini-2.5-flash",
|
|
||||||
buildBody: (query) => ({
|
buildBody: (query) => ({
|
||||||
contents: [{ role: "user", parts: [{ text: query }] }],
|
contents: [{ role: "user", parts: [{ text: query }] }],
|
||||||
tools: [{ google_search: {} }]
|
tools: [{ google_search: {} }]
|
||||||
@@ -70,8 +74,7 @@ const CHAT_SEARCH_CONFIG = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
openai: {
|
openai: {
|
||||||
endpoint: () => "https://api.openai.com/v1/chat/completions",
|
endpoint: () => searchEndpoint("openai"),
|
||||||
defaultModel: "gpt-4o-mini",
|
|
||||||
buildBody: (query, model) => {
|
buildBody: (query, model) => {
|
||||||
const body = {
|
const body = {
|
||||||
model,
|
model,
|
||||||
@@ -105,8 +108,7 @@ const CHAT_SEARCH_CONFIG = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
xai: {
|
xai: {
|
||||||
endpoint: () => "https://api.x.ai/v1/responses",
|
endpoint: () => searchEndpoint("xai"),
|
||||||
defaultModel: "grok-4.20-reasoning",
|
|
||||||
buildBody: (query, model) => ({
|
buildBody: (query, model) => ({
|
||||||
model,
|
model,
|
||||||
input: [{ role: "user", content: query }],
|
input: [{ role: "user", content: query }],
|
||||||
@@ -145,8 +147,7 @@ const CHAT_SEARCH_CONFIG = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
kimi: {
|
kimi: {
|
||||||
endpoint: () => "https://api.moonshot.cn/v1/chat/completions",
|
endpoint: () => searchEndpoint("kimi"),
|
||||||
defaultModel: "kimi-k2.5",
|
|
||||||
buildBody: (query, model) => ({
|
buildBody: (query, model) => ({
|
||||||
model,
|
model,
|
||||||
messages: [{ role: "user", content: query }],
|
messages: [{ role: "user", content: query }],
|
||||||
@@ -195,8 +196,7 @@ const CHAT_SEARCH_CONFIG = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
minimax: {
|
minimax: {
|
||||||
endpoint: () => "https://api.minimaxi.com/v1/text/chatcompletion_v2",
|
endpoint: () => searchEndpoint("minimax"),
|
||||||
defaultModel: "MiniMax-M2.7",
|
|
||||||
buildBody: (query, model) => ({
|
buildBody: (query, model) => ({
|
||||||
model,
|
model,
|
||||||
messages: [{ role: "user", content: query }],
|
messages: [{ role: "user", content: query }],
|
||||||
@@ -254,8 +254,7 @@ const CHAT_SEARCH_CONFIG = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
perplexity: {
|
perplexity: {
|
||||||
endpoint: () => "https://api.perplexity.ai/chat/completions",
|
endpoint: () => searchEndpoint("perplexity"),
|
||||||
defaultModel: "sonar",
|
|
||||||
buildBody: (query, model) => ({
|
buildBody: (query, model) => ({
|
||||||
model,
|
model,
|
||||||
messages: [{ role: "user", content: query }]
|
messages: [{ role: "user", content: query }]
|
||||||
@@ -324,7 +323,7 @@ export async function handleChatSearch({
|
|||||||
Number.isFinite(maxResults) && maxResults > 0
|
Number.isFinite(maxResults) && maxResults > 0
|
||||||
? Math.floor(maxResults)
|
? Math.floor(maxResults)
|
||||||
: DEFAULT_MAX_RESULTS;
|
: DEFAULT_MAX_RESULTS;
|
||||||
const useModel = model || cfg.defaultModel;
|
const useModel = model || searchModel(provider);
|
||||||
const url = cfg.endpoint(useModel);
|
const url = cfg.endpoint(useModel);
|
||||||
const body = cfg.buildBody(query, useModel);
|
const body = cfg.buildBody(query, useModel);
|
||||||
const headers = cfg.buildHeaders(token);
|
const headers = cfg.buildHeaders(token);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Buffer } from "node:buffer";
|
import { Buffer } from "node:buffer";
|
||||||
import { createErrorResult } from "../utils/error.js";
|
import { createErrorResult } from "../utils/error.js";
|
||||||
import { HTTP_STATUS } from "../config/runtimeConfig.js";
|
import { HTTP_STATUS } from "../config/runtimeConfig.js";
|
||||||
import { AI_PROVIDERS } from "../../src/shared/constants/providers.js";
|
|
||||||
|
|
||||||
// Build auth headers from sttConfig + token
|
// Build auth headers from sttConfig + token
|
||||||
function buildAuthHeaders(cfg, token) {
|
function buildAuthHeaders(cfg, token) {
|
||||||
@@ -167,11 +166,11 @@ function jsonResponse(obj) {
|
|||||||
* STT core handler — dispatch by sttConfig.format.
|
* STT core handler — dispatch by sttConfig.format.
|
||||||
* @returns {Promise<{success, response, status?, error?}>}
|
* @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");
|
const file = formData.get("file");
|
||||||
if (!file) return createErrorResult(HTTP_STATUS.BAD_REQUEST, "Missing required field: 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`);
|
if (!cfg) return createErrorResult(HTTP_STATUS.BAD_REQUEST, `Provider '${provider}' does not support STT`);
|
||||||
|
|
||||||
const token = cfg.authType === "none" ? null : (credentials?.apiKey || credentials?.accessToken);
|
const token = cfg.authType === "none" ? null : (credentials?.apiKey || credentials?.accessToken);
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
// Gemini TTS — generateContent with AUDIO modality returns PCM L16, wrap as WAV
|
// Gemini TTS — generateContent with AUDIO modality returns PCM L16, wrap as WAV
|
||||||
import { Buffer } from "node:buffer";
|
import { Buffer } from "node:buffer";
|
||||||
|
import { PROVIDER_MEDIA, PROVIDER_MODELS } 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 FALLBACK_MODEL = "gemini-3.1-flash-tts-preview";
|
||||||
|
const KNOWN_MODELS = [
|
||||||
|
...(TTS_CFG.models || []),
|
||||||
|
...(PROVIDER_MODELS["gemini-tts-models"] || []),
|
||||||
|
...(PROVIDER_MODELS.gemini || []).filter((m) => (m.kind || m.type) === "tts"),
|
||||||
|
]
|
||||||
|
.map((m) => m?.id)
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter((id, index, list) => list.indexOf(id) === index);
|
||||||
|
const DEFAULT_MODEL = KNOWN_MODELS[0] || FALLBACK_MODEL;
|
||||||
const DEFAULT_VOICE = "Kore";
|
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
|
// Parse "model/voice" — if input doesn't match a known TTS model, treat it as voice with default model
|
||||||
function parseGeminiModelVoice(input) {
|
function parseGeminiModelVoice(input) {
|
||||||
@@ -51,7 +62,7 @@ export default {
|
|||||||
async synthesize(text, model, credentials, _responseFormat, opts = {}) {
|
async synthesize(text, model, credentials, _responseFormat, opts = {}) {
|
||||||
if (!credentials?.apiKey) throw new Error("No Gemini API key configured");
|
if (!credentials?.apiKey) throw new Error("No Gemini API key configured");
|
||||||
const { modelId, voiceId } = parseGeminiModelVoice(model);
|
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, {
|
const res = await fetch(url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
|||||||
@@ -33,8 +33,10 @@ export async function synthesizeViaConfig(provider, text, model, credentials) {
|
|||||||
if (!handler) return null;
|
if (!handler) return null;
|
||||||
const apiKey = credentials?.apiKey;
|
const apiKey = credentials?.apiKey;
|
||||||
if (cfg.authType !== "none" && !apiKey) throw new Error(`${provider} API key required`);
|
if (cfg.authType !== "none" && !apiKey) throw new Error(`${provider} API key required`);
|
||||||
const defaultModel = cfg.models?.[0]?.id || "";
|
const { PROVIDER_MODELS } = await import("open-sse/config/providerModels.js");
|
||||||
const { modelId, voiceId } = parseModelVoice(model, defaultModel, "", cfg.models || []);
|
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 });
|
return handler({ baseUrl: cfg.baseUrl, apiKey, text, modelId, voiceId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
// OpenAI TTS — model format: "tts-model/voice"
|
// OpenAI TTS — model format: "tts-model/voice"
|
||||||
import { Buffer } from "node:buffer";
|
import { Buffer } from "node:buffer";
|
||||||
|
import { PROVIDER_MEDIA } from "../../providers/index.js";
|
||||||
|
|
||||||
|
const DEFAULT_TTS_MODEL = PROVIDER_MEDIA["openai"]?.ttsConfig?.defaultModel;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async synthesize(text, model, credentials) {
|
async synthesize(text, model, credentials) {
|
||||||
if (!credentials?.apiKey) throw new Error("No OpenAI API key configured");
|
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";
|
let voice = "alloy";
|
||||||
if (model && model.includes("/")) {
|
if (model && model.includes("/")) {
|
||||||
const parts = model.split("/");
|
const parts = model.split("/");
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
// OpenRouter TTS — via chat completions + audio modality (SSE stream)
|
// 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 {
|
export default {
|
||||||
async synthesize(text, model, credentials) {
|
async synthesize(text, model, credentials) {
|
||||||
if (!credentials?.apiKey) throw new Error("No OpenRouter API key configured");
|
if (!credentials?.apiKey) throw new Error("No OpenRouter API key configured");
|
||||||
|
|
||||||
// model format: "tts-model/voice" e.g. "openai/gpt-4o-mini-tts/alloy"
|
// 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";
|
let voice = "alloy";
|
||||||
if (model && model.includes("/")) {
|
if (model && model.includes("/")) {
|
||||||
const lastSlash = model.lastIndexOf("/");
|
const lastSlash = model.lastIndexOf("/");
|
||||||
@@ -20,13 +24,12 @@ export default {
|
|||||||
voice = model;
|
voice = model;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
const res = await fetch(TTS_CFG.baseUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Authorization": `Bearer ${credentials.apiKey}`,
|
"Authorization": `Bearer ${credentials.apiKey}`,
|
||||||
"HTTP-Referer": "https://endpoint-proxy.local",
|
...(TTS_CFG.headers || {}),
|
||||||
"X-Title": "Endpoint Proxy",
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: ttsModel,
|
model: ttsModel,
|
||||||
|
|||||||
@@ -30,9 +30,6 @@ export {
|
|||||||
// Services
|
// Services
|
||||||
export {
|
export {
|
||||||
detectFormat,
|
detectFormat,
|
||||||
getProviderConfig,
|
|
||||||
buildProviderUrl,
|
|
||||||
buildProviderHeaders,
|
|
||||||
getTargetFormat
|
getTargetFormat
|
||||||
} from "./services/provider.js";
|
} 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,
|
||||||
|
};
|
||||||
274
open-sse/providers/capabilities.js
Normal file
274
open-sse/providers/capabilities.js
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
// 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/4.8 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-7": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 },
|
||||||
|
"claude-opus-4.8": { 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-opus-4-8": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 },
|
||||||
|
"claude-opus-4.8-thinking": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 },
|
||||||
|
"claude-opus-4-8-thinking": { 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: 128000 },
|
||||||
|
"claude-sonnet-4-6": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 },
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
@@ -207,10 +207,11 @@ export const PATTERN_PRICING = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Match a model ID against a glob pattern (* = wildcard).
|
* Match a model ID against a glob pattern (* = wildcard). Case-insensitive:
|
||||||
|
* registry ids mix casing (e.g. "MiniMax-M2.5" vs "minimax-m2.5").
|
||||||
*/
|
*/
|
||||||
function matchPattern(pattern, model) {
|
export function matchPattern(pattern, model) {
|
||||||
const regex = new RegExp("^" + pattern.split("*").map(s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$");
|
const regex = new RegExp("^" + pattern.split("*").map(s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$", "i");
|
||||||
return regex.test(model);
|
return regex.test(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
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"],
|
||||||
|
};
|
||||||
85
open-sse/providers/registry/antigravity.js
Normal file
85
open-sse/providers/registry/antigravity.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
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" },
|
||||||
|
};
|
||||||
41
open-sse/providers/registry/blackbox.js
Normal file
41
open-sse/providers/registry/blackbox.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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",
|
||||||
|
serviceKinds: ["llm"],
|
||||||
|
thinkingConfig: {
|
||||||
|
options: ["auto", "none", "low", "medium", "high", "xhigh"],
|
||||||
|
defaultMode: "auto",
|
||||||
|
},
|
||||||
|
transport: {
|
||||||
|
baseUrl: "https://api.blackbox.ai/v1/chat/completions",
|
||||||
|
thinkingFormat: "openai",
|
||||||
|
},
|
||||||
|
models: [
|
||||||
|
{ id: "claude-fable-5", name: "Claude Fable 5", upstreamModelId: "blackboxai/anthropic/claude-fable-5" },
|
||||||
|
{ id: "claude-opus-4.8", name: "Claude Opus 4.8", upstreamModelId: "blackboxai/anthropic/claude-opus-4.8" },
|
||||||
|
{ id: "claude-sonnet-4.6", name: "Claude Sonnet 4.6", upstreamModelId: "blackboxai/anthropic/claude-sonnet-4.6" },
|
||||||
|
{ id: "gpt-5.5", name: "GPT-5.5", upstreamModelId: "blackboxai/openai/gpt-5.5" },
|
||||||
|
{ id: "gpt-5.4-pro", name: "GPT-5.4 Pro", upstreamModelId: "blackboxai/openai/gpt-5.4-pro" },
|
||||||
|
{ id: "gpt-5.4", name: "GPT-5.4", upstreamModelId: "blackboxai/openai/gpt-5.4" },
|
||||||
|
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex", upstreamModelId: "blackboxai/openai/gpt-5.3-codex" },
|
||||||
|
{ id: "gpt-5.4-nano", name: "GPT-5.4 Nano", upstreamModelId: "blackboxai/openai/gpt-5.4-nano" },
|
||||||
|
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", upstreamModelId: "blackboxai/deepseek/deepseek-v4-flash" },
|
||||||
|
{ id: "grok-4.3", name: "Grok 4.3", upstreamModelId: "blackboxai/x-ai/grok-4.3" },
|
||||||
|
],
|
||||||
|
};
|
||||||
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: []
|
||||||
|
}
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user