mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 02:42:24 +03:00
fix(providers): retire common ChatGPT Web provider
This commit is contained in:
committed by
Markus Hartung
parent
825f8fe425
commit
9c5dd6760c
46
.env.example
46
.env.example
@@ -612,11 +612,9 @@ CLOUD_URL=
|
||||
# Default: http://localhost:20128
|
||||
NEXT_PUBLIC_BASE_URL=http://localhost:20128
|
||||
|
||||
# Browser-facing OmniRoute origin for generated assets in API responses.
|
||||
# Highest-priority public origin override; also used by non-dashboard public-origin validation.
|
||||
# Used by: chatgpt-web image generation cache URLs (/v1/chatgpt-web/image/<id>).
|
||||
# Set this when OpenWebUI or another relay reaches OmniRoute by an internal URL
|
||||
# but the user's browser must fetch images from a LAN, tunnel, or public origin.
|
||||
# Highest-priority OmniRoute public origin override, also used by non-dashboard
|
||||
# public-origin validation. Set it when external clients reach OmniRoute through
|
||||
# a stable LAN, tunnel, or public origin that differs from its internal URL.
|
||||
# Do not include /v1; if included accidentally it will be normalized away.
|
||||
# OMNIROUTE_PUBLIC_BASE_URL=http://192.168.0.15:20128
|
||||
|
||||
@@ -629,28 +627,6 @@ NEXT_PUBLIC_BASE_URL=http://localhost:20128
|
||||
# Used by: open-sse/config/providerPluginManifestUrl.ts. Defaults to http.
|
||||
# OMNIROUTE_PUBLIC_PROTOCOL=http
|
||||
|
||||
# Max wait time for an async chatgpt-web image to land via the celsius
|
||||
# WebSocket, in milliseconds. Default 180000 (3 minutes). Increase during
|
||||
# upstream queue-deep windows ("Lots of people are creating images right now").
|
||||
# OMNIROUTE_CGPT_WEB_IMAGE_TIMEOUT_MS=180000
|
||||
|
||||
# Total in-memory byte budget for the chatgpt-web image cache (used to serve
|
||||
# /v1/chatgpt-web/image/<id>), in megabytes. Default 256. Lower this if you
|
||||
# run OmniRoute on a memory-constrained host; raise it if image generation
|
||||
# is heavy and clients are racing the 30-minute TTL.
|
||||
# OMNIROUTE_CGPT_WEB_IMAGE_CACHE_MAX_MB=256
|
||||
|
||||
# Overall wait budget for a chatgpt-web GPT-5.5 Pro background-poll handoff,
|
||||
# in milliseconds. Default 1200000 (20 minutes). Pro reasoning runs are slow
|
||||
# and complete out-of-band, so OmniRoute polls until the answer lands or this
|
||||
# budget elapses. Raise it if Pro requests time out before finishing.
|
||||
# OMNIROUTE_CGPT_WEB_PRO_TIMEOUT_MS=1200000
|
||||
|
||||
# Interval between chatgpt-web GPT-5.5 Pro background-poll attempts, in
|
||||
# milliseconds. Default 4000 (4 seconds). Lower for snappier completion at the
|
||||
# cost of more upstream polling; raise to reduce request volume.
|
||||
# OMNIROUTE_CGPT_WEB_PRO_POLL_INTERVAL_MS=4000
|
||||
|
||||
# Public cloud URL — client-side mirror of CLOUD_URL.
|
||||
NEXT_PUBLIC_CLOUD_URL=
|
||||
|
||||
@@ -1483,17 +1459,6 @@ CURSOR_USER_AGENT="Cursor/3.4"
|
||||
# FIRECRAWL_BASE_URL=https://api.firecrawl.dev
|
||||
# FIRECRAWL_TIMEOUT_MS=30000 # Per-request timeout (default: 30000 = 30s)
|
||||
|
||||
# ── ChatGPT TLS sidecar (Firefox-fingerprinted client) ──
|
||||
# Used by: open-sse/services/chatgptTlsClient.ts — wire-level timeout for
|
||||
# the bogdanfinn/tls-client koffi binding and the JS-side grace window
|
||||
# layered on top of it when the native library is wedged.
|
||||
# OMNIROUTE_CHATGPT_TLS_TIMEOUT_MS=60000
|
||||
# OMNIROUTE_CHATGPT_TLS_GRACE_MS=10000
|
||||
# Max wait for the FIRST streamed byte from the ChatGPT TLS sidecar before the
|
||||
# request is aborted as a dead stream, in milliseconds. Default 30000 (30s).
|
||||
# Raise it if upstream cold-starts routinely exceed the window.
|
||||
# OMNIROUTE_CHATGPT_STREAM_FIRST_BYTE_TIMEOUT_MS=30000
|
||||
|
||||
# ── Claude TLS sidecar (Chromium-fingerprinted client) ──
|
||||
# Used by: open-sse/services/claudeTlsClient.ts — wire-level timeout for
|
||||
# the bogdanfinn/tls-client koffi binding and the JS-side grace window
|
||||
@@ -2566,11 +2531,6 @@ APP_LOG_TO_FILE=true
|
||||
# Used by: src/lib/jobs/backupScheduleJob.ts
|
||||
# OMNIROUTE_BACKUP_SCHEDULE_JOB_INTERVAL_MS=30000
|
||||
|
||||
# ── TLS sidecar override ──
|
||||
# Used by: open-sse/services/chatgptTlsClient.ts tests. Production deployments
|
||||
# should leave this unset; the sidecar is auto-managed.
|
||||
# OMNIROUTE_TLS_PROXY_URL=
|
||||
|
||||
# ── Skills sandbox (experimental) ──
|
||||
# Used by: src/lib/skills/builtins.ts. All values support comma lists where
|
||||
# noted in the source.
|
||||
|
||||
@@ -104,7 +104,7 @@ RUN test -f package-lock.json \
|
||||
# instead of `npx --yes`, which would install an arbitrary registry version
|
||||
# on-demand and run its lifecycle scripts (Sonar docker:S6505).
|
||||
#
|
||||
# tls-client-node (chatgpt-web/claude-web/grok-web/lmarena/perplexity-web TLS
|
||||
# tls-client-node (claude-web/grok-web/lmarena/perplexity-web TLS
|
||||
# impersonation) hits the same --ignore-scripts wall: its own postinstall.js
|
||||
# fetches a platform .so/.dylib/.dll from the bogdanfinn/tls-client GitHub
|
||||
# Releases API and is never invoked when npm ci skips lifecycle scripts. Unlike
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
|
||||
</div>
|
||||
|
||||
> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute catalogs **455 free-tier entries across 40 recurring pool keys** and computes the token headline from the **20 pools with a published positive monthly budget**, deduplicated by shared pool. The result stays visible on the dashboard (`/dashboard/free-tiers`).
|
||||
> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute catalogs **454 free-tier entries across 40 recurring pool keys** and computes the token headline from the **20 pools with a published positive monthly budget**, deduplicated by shared pool. The result stays visible on the dashboard (`/dashboard/free-tiers`).
|
||||
|
||||
<img src="./docs/diagrams/free-tier-budget.svg" width="100%" alt="OmniRoute free-tier budget card: ~1.51B free tokens per month steady, up to ~2.13B in the first month with signup credits, from 40 documented recurring pool keys covering 455 cataloged free-tier entries behind one endpoint. Honest pool-deduped math — each shared pool counted once, including 20 recurring pools with a published positive monthly token budget; 15 providers are marked avoid in the terms-risk catalog so you decide. Budget bar includes Mistral 1B, LLM7 150M, Nara 150M, Gemini 60M and smaller pools, plus first-month signup credits and permanently-free no-token-cap providers surfaced separately so they never inflate the headline. Live used/remaining on /dashboard/free-tiers."/>
|
||||
<img src="./docs/diagrams/free-tier-budget.svg" width="100%" alt="OmniRoute free-tier budget card: ~1.51B free tokens per month steady, up to ~2.13B in the first month with signup credits, from 40 documented recurring pool keys covering 454 cataloged free-tier entries behind one endpoint. Honest pool-deduped math — each shared pool counted once, including 20 recurring pools with a published positive monthly token budget; 15 providers are marked avoid in the terms-risk catalog so you decide. Budget bar includes Mistral 1B, LLM7 150M, Nara 150M, Gemini 60M and smaller pools, plus first-month signup credits and permanently-free no-token-cap providers surfaced separately so they never inflate the headline. Live used/remaining on /dashboard/free-tiers."/>
|
||||
|
||||
> Animated summary of the live `/dashboard/free-tiers` page. Full methodology (pool dedupe, credit tiers, provider terms): **[docs/reference/FREE_TIERS.md](docs/reference/FREE_TIERS.md)**.
|
||||
>
|
||||
@@ -1265,7 +1265,7 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi
|
||||
<tr><td nowrap><b><a href="docs/architecture/RESILIENCE_GUIDE.md">Resilience Guide</a></b></td><td>Circuit breakers, cooldowns, queue, anti-thundering herd, TLS spoofing</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/routing/AUTO-COMBO.md">Auto-Combo Engine</a></b></td><td>15-factor scoring, mode packs, self-healing</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/ops/PROXY_GUIDE.md">Proxy Guide</a></b></td><td>3-level proxy system, 1proxy marketplace, registry CRUD</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/reference/FREE_TIERS.md">Free Tiers</a></b></td><td>Consolidated directory: 40 documented recurring pools / 455 cataloged free-tier entries</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/reference/FREE_TIERS.md">Free Tiers</a></b></td><td>Consolidated directory: 40 documented recurring pools / 454 cataloged free-tier entries</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/guides/FEATURES.md">Features Gallery</a></b></td><td>Visual dashboard tour with screenshots</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/architecture/CODEBASE_DOCUMENTATION.md">Codebase Documentation</a></b></td><td>Beginner-friendly codebase walkthrough</td></tr>
|
||||
</table>
|
||||
|
||||
1
changelog.d/fixes/pending-retire-chatgpt-web.md
Normal file
1
changelog.d/fixes/pending-retire-chatgpt-web.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(providers):** retire the common `chatgpt-web` / `cgpt-web` provider because its pre-key and proof-of-work provenance could not be cleared ([#PENDING](https://github.com/diegosouzapw/OmniRoute/pull/PENDING)) — explicit chat and image requests, slash-prefixed models, persisted aliases, proxy overrides, and combo targets now fail closed with a sanitized HTTP `410` / `PROVIDER_RETIRED` before upstream dispatch. Migration 163 tombstones matching connections and invalidates their active leases without deleting history or widening API-key allowlists. The independent `chatgpt-web-codex` / `cgpt-codex` provider and its MIT-noticed vendored implementation remain available.
|
||||
@@ -3890,11 +3890,6 @@
|
||||
"count": 31
|
||||
}
|
||||
},
|
||||
"tests/unit/chatgpt-web-tools-5240.test.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"tests/unit/check-docs-symbols.test.ts": {
|
||||
"@typescript-eslint/no-unused-vars": {
|
||||
"count": 3
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"_rebaseline_2026_08_09_9342_network_error_guard": "PR #9342 own growth during the 2026-08-09 rebase: open-sse/services/accountFallback.ts 1978->2008 (+30 = the isQueueTimeout short-circuit plus a per-provider network-error dedup window in recordProviderFailure, keeping one VPN blip from the same provider's combo targets counting once per target). Covered by tests/unit/breaker-network-error-guard.test.ts. (chat.ts stays base-red: upstream tip is already 1918 > frozen 1904, this PR only adds +12 on top; not re-bumped per the no-inherit-ratchet rule.)",
|
||||
"_rebaseline_2026_08_02_9259_rolling_rpm": "PR #9259 (issue #8733) own growth: open-sse/services/rateLimitManager.ts baseline 1060->1167 (+107; final source 1153). The existing withRateLimit chokepoint now composes process-local rolling RPM leases with Bottleneck admission, releases pre-dispatch leases on queue timeout/abort/connection disable, preserves caller abort reasons, and wires 429/header state into the extracted rollingRpmGate.ts. The remaining growth is irreducible lifecycle wiring at the dispatch boundary plus the real watchdog test hooks needed to verify queued-wedge recovery; moving it further would obscure lease ownership and Bottleneck cleanup. Covered by the focused rate-limit manager/sliding-window suite (33/33); distributed multi-instance coordination remains explicitly out of scope.",
|
||||
"_rebaseline_2026_07_24_8470_hyperagent_sticky_thread": "PR #8470 (artickc, fix/hyperagent-tool-loop-thread-sticky) own growth: open-sse/executors/hyperagent.ts 936->1025 (wc -l; check-file-size.mjs counts via split(\"\\n\").length so the gate sees 937->1026, +89, crosses the 1000 cap). Fixes a real bug where a reverse-conversion proxy (text-Intent/JSON to Claude Code native tool_calls) rewrites assistant messages between agentic tool-loop turns, breaking HyperAgent's conversation-prefix fingerprint and cold-starting the thread mid tool-loop. Adds Anthropic tool_use/tool_result flattening to extractMessageText() plus a new rootUserFingerprint()/root-key lookup tier in resolveHyperAgentThreadBinding()/storeHyperAgentThreadAfterTurn() so the thread stays sticky across the tool loop. Cohesive additions inside the existing single-file executor; not extractable without splitting the executor mid-request-flow. Covered by tests/unit/executor-hyperagent.test.ts (19/19, +5 new cases for tool_result/tool_use flattening + root-key stickiness). Pre-merge review flagged a cross-conversation root-key collision risk (tracked in the PR's own mandatory pre-merge checklist, not yet addressed) — unrelated to this file-size ratchet, tracked separately by /fix-prs.",
|
||||
"_rebaseline_2026_07_25_8494_capability_filter_fail_closed": "PR #8494 (fix/capability-filters-fail-closed, #8488) own growth: open-sse/services/combo.ts 3640->3693 (+53) adds a fail-closed guard after filterTargetsByRequestCompatibility() — when every eligible target is excluded by request-capability filtering (vision/tools/etc) instead of quota/health, the combo now returns an explicit `capability_mismatch` 400 (describeCapabilityFilterExhaustion, imported from combo/comboStructure.ts) rather than silently falling through to a generic no-targets error, plus a `compatFilterFailOpen` escape hatch (combo config OR settings) mirrored at both the main/auto and round-robin call sites for symmetry. combo/comboStructure.ts (previously under cap, un-frozen) grows 794->918 (+124) — new home for describeCapabilityFilterExhaustion + providerSupportsEmulatedToolCalling (#5240 emulated tool-calling exemption so fail-closed does not regress prompt-emulation-only combos like all-chatgpt-web). Irreducible orchestration wiring at the existing filter chokepoint (same precedent as #7301's universal-cooldown-retry generalization). Companion test tests/unit/combo-routing-engine.test.ts 3409->3449 (+40, fail-closed/fail-open coverage across both call sites) also rebaselined. Covered by tests/unit/8488-capability-filter-fail-closed.test.ts (new) + 95/95 passing across both files. Structural shrink of combo.ts tracked in #3501.",
|
||||
"_rebaseline_2026_07_25_8494_capability_filter_fail_closed": "PR #8494 (fix/capability-filters-fail-closed, #8488) own growth: open-sse/services/combo.ts 3640->3693 (+53) adds a fail-closed guard after filterTargetsByRequestCompatibility() — when every eligible target is excluded by request-capability filtering (vision/tools/etc) instead of quota/health, the combo now returns an explicit `capability_mismatch` 400 (describeCapabilityFilterExhaustion, imported from combo/comboStructure.ts) rather than silently falling through to a generic no-targets error, plus a `compatFilterFailOpen` escape hatch (combo config OR settings) mirrored at both the main/auto and round-robin call sites for symmetry. combo/comboStructure.ts (previously under cap, un-frozen) grows 794->918 (+124) — new home for describeCapabilityFilterExhaustion + providerSupportsEmulatedToolCalling (#5240 emulated tool-calling exemption so fail-closed does not regress prompt-emulation-only combos). Irreducible orchestration wiring at the existing filter chokepoint (same precedent as #7301's universal-cooldown-retry generalization). Companion test tests/unit/combo-routing-engine.test.ts 3409->3449 (+40, fail-closed/fail-open coverage across both call sites) also rebaselined. Covered by tests/unit/8488-capability-filter-fail-closed.test.ts (new) + 95/95 passing across both files. Structural shrink of combo.ts tracked in #3501.",
|
||||
"_rebaseline_2026_07_25_8499_ts7_result_union_predicates": "PR #8499 (backryun, chore/ts7-types-executor-scattered) own growth: muse-spark-web.ts 1396->1405 (+9, irreducible). Under this workspace's `strictNullChecks: false`, the boolean-literal discriminant on `GraphqlResult` (`{ ok: true } | { ok: false; error: string }`) narrows the positive `.ok===true` branch but leaves `!result.ok` at the full union under TS7, making `.error` unreachable to the checker at the two call sites (warmup, mode-switch). Fixed by adding a single `isGraphqlFailure()` type-predicate helper (doc comment + 3-line body) reused at both call sites instead of duplicating the predicate inline — not extractable to a shared module without splitting a single-file executor's local narrowing helper out of its own file. Covered by the existing muse-spark-web executor test suite (no behavior change, pure narrowing fix).",
|
||||
"_rebaseline_2026_07_22_8131_windowshide_cloudflared_spawn": "PR #8167 (Dingding-leo, fix/windows-hide-child-process, #8131) own growth: src/lib/cloudflaredTunnel.ts 934->935 (+1, irreducible call-site wiring — the single `windowsHide: true` option added to the existing cloudflared spawn() options object so no transient conhost.exe/cmd console window flashes open on Windows). Covered by the pre-merge-fix regression test tests/unit/windows-hide-child-process-spawns-8131.test.ts (added for the two additional spawn() sites the PR missed: ServiceSupervisor.ts, versionManager/processManager.ts) plus the windowsHide assertion added to tests/unit/services/installers/runNpm-shell-5379.test.ts (installers/utils.ts buildNpmExecOptions).",
|
||||
"_rebaseline_2026_07_22_8006_adobe_firefly_media_provider": "PR #8006 (artickc, feat/adobe-firefly-media) own growth: adds Adobe Firefly as a media-only (image + video) provider — unofficial IMS/cookie-session bridge for firefly.adobe.com covering IMS cookie->access_token exchange, discovery-catalog fallback, credits/balance usage, and submit+poll dispatch for both image (nano-banana/gpt-image families) and video (Sora 2/Veo 3.1/Kling 3.0) generation, with 408-under-load retry handling. New leaf open-sse/services/adobeFireflyClient.ts frozen at 1958 (>>cap 800) — a single self-contained upstream client (mirrors the qoderCli.ts precedent for a new provider client that is legitimately large on day one: IMS auth, cookie/JWT normalization, payload builders for 2 media types x multiple model families, SSE-less submit/poll state machine, error sanitization); not extractable without scattering a single upstream integration across artificial module boundaries mid-PR. open-sse/config/imageRegistry.ts (existing, previously under cap) grows 800->821 (+21, the new adobe-firefly IMAGE_PROVIDERS entry + models list, additive registry data at the existing registry chokepoint). src/lib/usage/providerLimits.ts 1000->1003 (+3, adobe-firefly/firefly added to the existing apikey-usage-fetcher allowlist, irreducible call-site wiring mirroring the sibling #7994 PromptQL/HyperAgent entries in the same PR group). Covered by tests/unit/adobe-firefly.test.ts (35/35). Structural shrink tracked in #3501.",
|
||||
@@ -83,7 +83,6 @@
|
||||
"_rebaseline_2026_06_20_reviewprs_mine_r2_filesize": "Reconciliacao file-size pos-lote /review-prs 'apenas minhas' r2: dois frozen cresceram cumulativamente sem bump (cada PR media OK na sua base, mas o crescimento empilhou acima do frozen no tip de merge; o fast-path do release nao roda check:file-size, so release->main). (1) src/shared/constants/pricing.ts 1620->1623 (+3 = linhas de pricing Claude Code (cc) do #4440, sobre o 1620 que o #4447 ja setara para gpt-4.1-mini/nano + o3/o4-mini). (2) open-sse/executors/base.ts 1399->1407 (+8 = handling granular de reasoning_effort para Claude no Copilot do #4443). Ambos dados/wiring coesos nos chokepoints existentes; nao extraiveis. Cobertos por tests/unit (claude-code pricing / base-executor-sanitize-effort + github-claude-reasoning-effort-granular).",
|
||||
"_rebaseline_2026_06_22_4647_opencode_go_deepseek": "PR #4647 (DevEstacion/opencode-go DeepSeek V4 Pro effort variants) review feedback: open-sse/executors/base.ts 1407->1414 (+7 = supportsMaxEffortForProvider now opt-ins opencode-go+deepseek so the literal 'max' effort survives the post-transformReasoningEffortForProvider pass — without this, max was silently rewritten to xhigh (OmniRoute's internal top tier) and the opencode-go upstream rejected it. The check is scoped to opencode-go deliberately to preserve the OpenRouter-DeepSeek inverse invariant (pi#4055, asserted by base-executor-sanitize-effort test:OpenRouter DeepSeek normalizes max -> xhigh). The +5 explanatory comment is required: a naive maintainer could otherwise broaden the check to all deepseek models and break the OpenRouter contract. Cohesive at the existing supportsMaxEffortForProvider chokepoint, next to the Claude/CC-compatible check; not extractable. Covered by tests/unit/base-executor-sanitize-effort.test.ts (3 new opencode-go deepseek cases).",
|
||||
"_rebaseline_2026_06_30_v3842_release_basetsl_5480": "v3.8.42 cycle-close file-size reconciliation: open-sse/executors/base.ts 1497->1500 (+3 net = #5480 'gate claude adaptive thinking defaults' — the adaptive-thinking injection is now gated behind the operator's thinking-budget config at the existing transform chokepoint, so default/passthrough no longer force-injects). Cohesive at the existing reasoning/thinking transform site; not extractable. The fast-path release gate (PR->release/**) does not run check:file-size, so this surfaced only on the release PR (PR->main). Covered by tests/unit/base-thinking-budget-config-5312.test.ts + the #5480 gate test.",
|
||||
"_rebaseline_2026_06_30_v3842_release_chatgptweb_compression": "v3.8.42 cycle-close file-size reconciliation (DRIFT measured OK on each PR's base, stacked above frozen at the merge tip; fast-path PR->release/** does not run check:file-size). (1) open-sse/executors/chatgpt-web.ts 2870->3206 (+336 = #5531 portable SHA3-512 sentinel-PoW wiring with the native-vs-fallback digest path + #5536 GPT-5.5 Pro handoff branch; the pure Keccak-f[1600] fallback itself already lives in the separate leaf open-sse/utils/sha3-512.ts — the executor growth is the cohesive call-site/handoff logic, not extractable without hiding the sentinel chokepoint). (2) tests/unit/chatgpt-web.test.ts 2855->3159 (+304 = #5536 GPT-5.5 Pro handoff coverage; pair-file with its executor). (3) open-sse/services/compression/strategySelector.ts 997->1022 (+25 = #5527 T02 honest default-on pipeline inflation guard wiring at the existing finalizeStackedResult choke). All cohesive at existing chokepoints; covered by tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts, chatgpt-web.test.ts (GPT-5.5 Pro), compression-pipeline-inflation-guard.test.ts.",
|
||||
"_rebaseline_2026_06_20_4023_web_cookie_noauth_validation": "PR #4023 (oyi77) own growth: src/lib/providers/validation.ts 4450->4518 (+68 = a new validateWebCookieProvider that probes the provider's /models endpoint — 401/403 => AUTH_007 SESSION_EXPIRED, any other status => valid session, empty cookie => invalid, provider-not-in-registry => unsupported — plus a local STANDARD_USER_AGENT const for the probe). Cohesive validator at the validateProviderApiKey dispatch; not extractable. Covered by tests/unit/provider-validation-web-cookie-auth007.test.ts. Heavily curated on merge — the PR's branch was badly stale-based (squash-base-stale), so its tree was DESTRUCTIVE: providers/index.ts deleted live providers openadapter/dit/tokenrouter (added by #4313) and the executor/base.ts edits reverted release fixes (#4037 duckduckgo host, theoldllm gpt5 models, base.ts fetch-start-timeout). Only the purely-additive validation feature was kept (validation.ts validateWebCookieProvider + errorCodes AUTH_007 + the test). Dropped: 5 malformed new registry entries (used non-RegistryEntry fields defaultModel/auth + referenced non-existent executors -> tsc TS2353), the destructive providers/index.ts + executor reverts, the unrelated pr-*.sh automation scripts, and evals/types.ts (belongs to the deferred evals modularization #4422). Also removed the PR's fragile 'Phase 2' executor probe (ran a live upstream chat during validation + classified any 'auth'-containing error as SESSION_EXPIRED) and rewrote the test to install its fetch mock before module load (the original mocked too late and silently hit live chatgpt.com).",
|
||||
"_rebaseline_2026_06_20_1308_model_lockout_honors_reset": "port from 9router#1308 own growth: open-sse/services/accountFallback.ts 1731->1752 (+21 = the new exported pure helper selectLockoutCooldownMs + its doc comment — picks the parsed upstream reset as the model-lockout exactCooldownMs when it exceeds the base cooldown, e.g. Antigravity \"Resets in 160h\", else preserves the existing 0/base behavior) and open-sse/executors/antigravity.ts 1680->1686 (this PR +1 = parseRetryFromErrorMessage regex `reset` -> `resets?` so plural \"Resets in 160h27m24s\" matches, plus a comment line; frozen set to the SUM 1686 with the concurrent #1944 which adds +5 at the disjoint passthroughFields region of the same file, so either merge order passes — pair-file rule). The combo lockout call sites in combo.ts now pass selectLockoutCooldownMs(cooldownMs, mlSettings) instead of always base/exponential, so an exhausted model honors the real upstream reset instead of being retried within minutes. Both edits are cohesive at the existing lockout/parse chokepoints; the helper is its own pure function (not extractable further). Covered by tests/unit/combo-model-lockout-honors-reset-1308.test.ts.",
|
||||
"_rebaseline_2026_06_20_1944_antigravity_strip_output_config": "port from 9router#1944: open-sse/executors/antigravity.ts frozen set to the measured cumulative 1687 of two concurrent PRs that touch disjoint regions of this file, so either merge order passes (pair-file rule). #1944 adds +6 at the envelope passthroughFields destructuring (~line 759: drop output_config/output_format — Anthropic/Claude-Code-only fields that Google's Cloud Code envelope rejects with `400 Unknown name \"output_config\"`, which broke every Claude model on Antigravity); #1308 adds +1 at parseRetryFromErrorMessage (~line 889: regex reset->resets?). Base 1680 + 6 + 1 = 1687 (re-measured on the real merge tip — the earlier 1686 estimate was off by one). Both edits are cohesive at their chokepoints; not extractable. Covered by tests/unit/antigravity-strip-output-config-1944.test.ts.",
|
||||
@@ -201,7 +200,6 @@
|
||||
"tests/unit/batch_api.test.ts": 1721,
|
||||
"tests/unit/cc-compatible-provider.test.ts": 1582,
|
||||
"tests/unit/chatcore-translation-paths.test.ts": 3739,
|
||||
"tests/unit/chatgpt-web.test.ts": 4092,
|
||||
"tests/unit/combo-routing-engine.test.ts": 4494,
|
||||
"tests/unit/db-migration-runner.test.ts": 1949,
|
||||
"tests/unit/deepseek-web.test.ts": 1420,
|
||||
@@ -275,7 +273,7 @@
|
||||
"_rebaseline_2026_07_02_5899_airforce_v1_discovery": "PR #5904 own growth: provider-models-route.test.ts 1628->1752 (+124 = test-only Rule #18 regression guards for the Api Airforce /v1/v1/models discovery bug (#5899): (a) a baseUrl ending in /v1/chat/completions must probe .../v1/models not the doubled .../v1/v1/models, and the host-guard case http://v1; (b) a REDIRECT_BLOCKED on one candidate must continue to the next endpoint instead of aborting the probe loop. Both guards fail on the pre-fix code. Test-only additions cohesive with the existing provider-models discovery suite (shared seedConnection/callRoute harness); not separately extractable without duplicating the harness.",
|
||||
"_rebaseline_2026_06_19_4293_codex_spark_scope": "PR #4293 (isolate Codex Spark quota scope) own growth, MEASURED on the actual merged tree (release/v3.8.30 + #4293). Production: auth.ts 2219->2279 (+60) threads requestedModel into Codex quota-policy/headroom/preflight/P2C scoring so normal Codex and GPT-5.3-Codex-Spark windows are evaluated independently; chatCore.ts 5116->5125 (+9) passes the failing model scope into Codex 429 failover (markCodexScopeRateLimited) instead of a connection-wide rateLimitedUntil write; accountFallback.ts 1727->1731 (+4) scopes Codex model-lock keys to codex vs spark. Heavy parsing/display logic lives in new leaf helpers under the cap (codexQuotaScopes.ts, codexUsageQuotas.ts, codexFailover.ts). Tests: account-fallback-service 1544->1569, executor-codex 1336->1339, sse-auth 1527->1553, usage-service-hardening 1612->1633 (added Spark-scope regression coverage). Cohesive wiring at existing selection/failover lockout boundaries; not extractable.",
|
||||
"_rebaseline_2026_06_20_4447_openai_gpt41mini_o_mini_pricing": "PR #4447 own growth: pricing.ts 1592->1620 (+28 = pure-data pricing rows closing the null/$0 gap for registry-exposed OpenAI ids gpt-4.1-mini, gpt-4.1-nano, o3-mini, o4-mini that tripped the catalog pricing gate; getPricingForModel does an exact lookup, so a missing key resolves to null. Official OpenAI per-1M prices + the table's derived-field convention (reasoning=output*1.5, cache_creation=input, cached=official). Restore-green for a pre-existing release/v3.8.32 red surfaced by #4432's __RUN_ALL__ run. Cohesive data; not extractable.",
|
||||
"_rebaseline_2026_06_20_web_cookie_validator_shadow_fix": "validation.ts 4518->4522 (+4 = move the generic web-cookie validateWebCookieProvider dispatch from the TOP of validateProviderApiKey to a FALLBACK after SPECIALTY_VALIDATORS, plus a comment, so #4023's generic AUTH_007 ping no longer shadows the rich per-provider validators (grok-web #3474 IP-reputation/Cloudflare, chatgpt-web cf-mitigated, claude/gemini/copilot/qwen/t3-web). Restores provider-validation-specialty.test.ts (112/112) while keeping web-cookie-auth007 (5/5). Behavior fix at an existing dispatch boundary; not extractable.",
|
||||
"_rebaseline_2026_06_20_web_cookie_validator_shadow_fix": "validation.ts 4518->4522 (+4 = move the generic web-cookie validateWebCookieProvider dispatch from the TOP of validateProviderApiKey to a FALLBACK after SPECIALTY_VALIDATORS, plus a comment, so #4023's generic AUTH_007 ping no longer shadows the rich per-provider validators (grok-web #3474 IP-reputation/Cloudflare and the active claude/gemini/copilot/qwen/t3-web validators). Restores provider-validation-specialty.test.ts (112/112) while keeping web-cookie-auth007 (5/5). Behavior fix at an existing dispatch boundary; not extractable.",
|
||||
"_rebaseline_2026_06_22_phase4b_slm_tier_ultra": "Compression Phase 4 (B) SLM tier own growth: open-sse/services/compression/strategySelector.ts 783->818 (+35 at the existing applyUltraAsync chokepoint). The no-modelPath ultra branch (previously a one-line passthrough to the sync applyCompression) now runs the two-tier resolver: it adapts the body, builds the ultraConfig (threading config.ultraEngine + preserveSystemPrompt), awaits the now-async ultraCompress (SLM Tier-B when ultraEngine===slm and the worker backend is available, else fail-open to the Tier-A heuristic), and threads result.stats.ultraTier into the returned CompressionStats so the resolved tier reaches the D0 telemetry persister. The sync applyCompression ultra branch is also re-pointed to the new pure ultraCompressHeuristic. The two-tier resolver + the pure heuristic live in open-sse/services/compression/ultra.ts and the thin SLM entry in engines/llmlingua/ultraEntry.ts (both <cap, fully unit-tested); strategySelector.ts is cohesive dispatch wiring at the existing ultra chokepoint, not extractable without hiding the dispatch boundary. Covered by tests/unit/compression/ultra-slm-tier.test.ts (applyCompressionAsync ultraTier:slm) + ultra.test.ts (39 regression) + the integration suites. Structural shrink of this file tracked in #3501.",
|
||||
"_rebaseline_2026_06_22_phase4c_adaptive_context_budget": "Compression Phase 4 (C) adaptive context-budget wiring own growth: open-sse/services/compression/strategySelector.ts 818->848 (+30 at the existing selectCompressionPlan dispatch chokepoint). selectCompressionPlan gains an 8th optional `adaptiveOptions` param (modelContextLimit/requestMaxTokens/onAdaptive sink) and, after resolveBasePlan and before the caching-aware pass, runs the PURE resolveAdaptivePlan when config.contextBudget.mode is floor|replace-autotrigger; the new adaptiveEnabled(config) helper also gates the legacy shouldAutoTrigger branch inside resolveBasePlan off when adaptive owns automatic-by-size escalation (D-C4). The escalation ladder, target computation, and the resolver itself live in open-sse/services/compression/adaptiveCompression/{computeTarget,ladder,resolveAdaptivePlan,types}.ts (all <cap, fully unit-tested by Tasks 1-4). strategySelector.ts is cohesive dispatch wiring at the existing compression chokepoint, not extractable without hiding the dispatch boundary, mirroring the prior compression rebaselines (#4217/#4210/phase4b). Covered by tests/unit/compression/adaptive-select-plan-wiring.test.ts (4 tests) + adaptive-chatcore-source-guard.test.ts (2). chatCore.ts also grows ~30 lines at the same call site (threads getTokenLimit(provider,effectiveModel) + request max_tokens into adaptiveOptions and records the adaptive telemetry block onto compression.completed) but stays under its frozen cap. Structural shrink of this file tracked in #3501.",
|
||||
"_rebaseline_2026_06_25_rc17_pr_batch": "rc17 PR batch own growth (cohesive, not extractable): responseSanitizer.ts 1103->1122 (+19 = SanitizeOpenAIResponseOptions interface + stripReasoning option, #4678); tokenRefresh.ts 2070->2090 (+20 = codex 401 defense-in-depth unrecoverable-refresh guard, #4686); token-refresh-service.test.ts 1322->1353 (+31 = 401-unfamiliar-payload regression case, #4686); translator-openai-responses-req.test.ts 1047->1050 (+3 = reasoning_effort non-Copilot assertion update, #4688). All are the merged PRs own surgical additions at existing chokepoints.",
|
||||
@@ -372,7 +370,6 @@
|
||||
"_rebaseline_pr4592_exclude_exhausted_auto": "Reconcile #4592 already-merged growth: combo.ts 2991->3036 (+45, terminal-status quota-cutoff exclusion in buildAutoCandidates + opt-in gate). Fast-gate PR->release does not run check:file-size.",
|
||||
"open-sse/executors/antigravity.ts": 1986,
|
||||
"open-sse/executors/base.ts": 2132,
|
||||
"open-sse/executors/chatgpt-web.ts": 4213,
|
||||
"open-sse/executors/codex.ts": 2031,
|
||||
"open-sse/executors/cursor.ts": 2032,
|
||||
"open-sse/executors/deepseek-web.ts": 1492,
|
||||
@@ -449,7 +446,7 @@
|
||||
"open-sse/utils/sseHeartbeat.ts": 194,
|
||||
"open-sse/utils/proxyFetch.ts": 1244,
|
||||
"_rebaseline_2026_08_23_11177_dns_retry_classification": "PR #11177 (rqzbeh) own growth: proxyFetch.ts 1239->1244 (+5, EAI_AGAIN/ENOTFOUND/ETIMEDOUT join the retryable dispatcher classification alongside ECONNREFUSED — bounded socket retries for transient DNS failures, part of the #10443 Hermes→Antigravity stream-drop fixes). Covered by tests/unit/proxy-fetch-dns-retry-10443.test.ts. Owner pre-authorized baseline bumps 2026-08-22.",
|
||||
"_rebaseline_2026_08_11_v3850_merge_storm_provider_registry: DRIFT do merge-storm 2026-08-11 (99 PRs mergeados no release/v3.8.50). AddApiKeyModal.tsx (PR #8949 ChatGPT Web provider) e useProviderConnections.ts/ModelSelectModal.tsx (PRs #9011 combo test-all, #9499 image combos) = UI nova legítima acima do cap; gateways.ts = god-file de catálogo de providers que cresceu com os PRs #9009/#9421/#9468/#9594 (qualquer split arriscaria corromper o merge de novo — o próprio PR #9421 foi o que quebrou o arquivo; sem split até o release, congelado no tamanho atual). Owner autorizou rebaseline com anotação (2026-08-11).": {
|
||||
"_rebaseline_2026_08_11_v3850_merge_storm_provider_registry: DRIFT do merge-storm 2026-08-11 (99 PRs mergeados no release/v3.8.50). AddApiKeyModal.tsx (PR #8949 ChatGPT Web (Codex) provider) e useProviderConnections.ts/ModelSelectModal.tsx (PRs #9011 combo test-all, #9499 image combos) = UI nova legítima acima do cap; gateways.ts = god-file de catálogo de providers que cresceu com os PRs #9009/#9421/#9468/#9594 (qualquer split arriscaria corromper o merge de novo — o próprio PR #9421 foi o que quebrou o arquivo; sem split até o release, congelado no tamanho atual). Owner autorizou rebaseline com anotação (2026-08-11).": {
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 1062,
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts": 1051,
|
||||
"src/shared/components/ModelSelectModal.tsx": 1138,
|
||||
@@ -462,7 +459,7 @@
|
||||
"src/shared/components/ModelSelectModal.tsx": 1138,
|
||||
"src/shared/constants/providers/apikey/gateways.ts": 1330,
|
||||
"open-sse/vendor/codex-chatgpt-web/bridge.ts": 1387,
|
||||
"_rebaseline_2026_08_11_v3850_merge_storm_provider_registry": "DRIFT do merge-storm 2026-08-11 (99 PRs mergeados no release/v3.8.50). AddApiKeyModal.tsx (PR #8949 ChatGPT Web provider) e useProviderConnections.ts/ModelSelectModal.tsx (PRs #9011 combo test-all, #9499 image combos) = UI nova legitima acima do cap; gateways.ts = god-file de catalogo de providers que cresceu com PRs #9009/#9421/#9468/#9594 (qualquer split arriscaria corromper o merge de novo — o proprio PR #9421 quebrou o arquivo); bridge.ts (PR #8949) = ponte Chromium vendored; proxyFetch.ts 1207->1220 = drift herdado de merges. Owner autorizou rebaseline com anotacao (2026-08-11).",
|
||||
"_rebaseline_2026_08_11_v3850_merge_storm_provider_registry": "DRIFT do merge-storm 2026-08-11 (99 PRs mergeados no release/v3.8.50). AddApiKeyModal.tsx (PR #8949 ChatGPT Web (Codex) provider) e useProviderConnections.ts/ModelSelectModal.tsx (PRs #9011 combo test-all, #9499 image combos) = UI nova legitima acima do cap; gateways.ts = god-file de catalogo de providers que cresceu com PRs #9009/#9421/#9468/#9594 (qualquer split arriscaria corromper o merge de novo — o proprio PR #9421 quebrou o arquivo); bridge.ts (PR #8949) = ponte Chromium vendored; proxyFetch.ts 1207->1220 = drift herdado de merges. Owner autorizou rebaseline com anotacao (2026-08-11).",
|
||||
"src/lib/modelCapabilities.ts": 1072,
|
||||
"_rebaseline_2026_08_21_11034_effort_variants": "DRIFT do tip (base-red #9985): modelCapabilities.ts 1016->1072 (+56) acumulado por PRs ja mergeadas no release/v3.8.50 — principalmente #11034 (resolve effort-variant capabilities a partir do modelo base), alem de #10963/#11040/#10987 growth dos catalogos. Tip puro ficou vermelho neste gate; rebaseline no tip por push direto (owner pre-autorizou crescimento legitimo). Nao tocou no arquivo da #11038.",
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 1014,
|
||||
@@ -564,7 +561,6 @@
|
||||
"_rebaseline_pr4592_exclude_exhausted_auto": "Reconcile #4592 already-merged growth: combo.ts 2991->3036 (+45, terminal-status quota-cutoff exclusion in buildAutoCandidates + opt-in gate). Fast-gate PR->release does not run check:file-size.",
|
||||
"open-sse/executors/antigravity.ts": "1528",
|
||||
"open-sse/executors/base.ts": "1640",
|
||||
"open-sse/executors/chatgpt-web.ts": "3241",
|
||||
"open-sse/executors/codex.ts": "1562",
|
||||
"open-sse/executors/cursor.ts": "1563",
|
||||
"open-sse/executors/deepseek-web.ts": "1148",
|
||||
|
||||
@@ -8,10 +8,8 @@
|
||||
"tests/unit/compression/ultra.test.ts": "v3.8.29 #4253: the vestigial SLM seam + dead deprecated alias were removed from the ultra compression engine; 6 asserts covering the removed seam were removed accordingly (49→43). Verified legitimate, not masking. Prune after v3.8.29 merges to main.",
|
||||
"tests/unit/db-backup-extended.test.ts": "v3.8.29 #4132: db-backup de-flake — 1 timing-sensitive assertion on fire-and-forget backup completion was removed in favor of awaiting actual completion (44→43). Verified legitimate, not masking. Prune after v3.8.29 merges to main.",
|
||||
"@omniroute/opencode-plugin/tests/combos.test.ts": "v3.8.31 #4384: the plugin now prefixes every catalog key with the `omniroute` provider id and drops the legacy `combo/` namespace; the test asserting raw-deletion + a `combo/<slug>` key (a namespace that no longer exists) was removed and the remaining asserts switched to `omniroute/<slug>` keys (82→81). Asserts updated to the new key contract, not weakened. Verified legitimate. Prune after v3.8.31 merges to main.",
|
||||
"tests/unit/chatgpt-web.test.ts": "v3.8.43 #5549: fix(chatgpt-web) restore dot-form Pro model ids — dois assert.equal separados (base Pro slug + pass-through slug) consolidados num único assert.equal(body.model, expectedSlugById[omniId], ...) orientado por tabela de mapeamento (281→280). Asserts consolidados, não enfraquecidos. Verificado legítimo. Prune após v3.8.43 mergear para main.",
|
||||
"tests/unit/chatcore-sanitization.test.ts": "v3.8.43 #5805: fix(translator) strip orphaned tool results — orphaned tool_result blocks (no matching tool_use) are now removed by stripOrphanedToolResults BEFORE content normalization, so the 3 positive `[Tool Result: …]`-text asserts were replaced by removal asserts (no tool_result block, no text), net 65→64. Behavior aligned to the merged #5805 contract; the sibling 'preserves Claude passthrough tool_result' assert (matching tool_use) is untouched. Verified legitimate, not masking. Prune after v3.8.43 merges to main.",
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/__tests__/useModelVisibilityHandlers.test.tsx": "v3.8.43 #5856: fix(dashboard) unify CSRF origin fallback — the model-visibility handler no longer issues a separate `/api/auth/csrf` fetch, so the two asserts pinning the 2nd fetch + its CSRF header were removed and the fetch-count assert updated 2→1 (7→5). Asserts follow the reduced fetch behavior, not weakened. Verified legitimate. Prune after v3.8.43 merges to main.",
|
||||
"tests/unit/provider-validation-specialty.test.ts": "v3.8.43 #5855: fix(qwen-web) unblock validator (retired endpoint) — the old `chat.qwen.ai/api/v2/user` probe asserts (exact URL / Authorization / source / Cookie / WAF-error) no longer apply after the endpoint migration and were replaced by new chathub-path behavior asserts (valid/error/warning), net 406→400. Asserts migrated to the new API surface (#5855/#5432), not weakened. Verified legitimate. Prune after v3.8.43 merges to main.",
|
||||
"tests/unit/chatcore-translation-paths.test.ts": "v3.8.44 #5958: fix(cc-compatible) send SSE accept for streamed requests — o assert do header Accept mudou de application/json para text/event-stream e dois asserts (stream + context_management) foram consolidados num único assert.deepEqual de tupla (297→296). Asserts consolidados/atualizados ao novo contrato, não enfraquecidos. Verificado legítimo. Prune após v3.8.44 mergear para main.",
|
||||
"tests/unit/combo-context-window-filter.test.ts": "v3.8.44 #6088: fix(combo) prefer known context capacity over unknown — a semântica do filtro mudou (unknown-context só sobrevive como fallback), o arquivo foi reescrito: 18 asserts pequenos (length/didFallback/ok) viraram 4 assert.deepEqual na LISTA ORDENADA completa de sobreviventes por caso (mais forte por caso). Asserts migrados ao novo contrato, não enfraquecidos. Verificado legítimo. Prune após v3.8.44 mergear para main.",
|
||||
"tests/unit/qoder-executor.test.ts": "v3.8.44 #5816: feat(qoder) drive PAT auth via qodercli — o executor migrou de chamadas HTTP diretas (api.qoder.com/api1.qoder.sh, headers Cosy-*) para o contrato stdio qodercli://; os asserts que pinavam URLs/headers da superfície aposentada foram substituídos por asserts do novo contrato (73→65). Asserts migrados à nova superfície, não enfraquecidos. Verificado legítimo. Prune após v3.8.44 mergear para main.",
|
||||
@@ -23,8 +21,67 @@
|
||||
"tests/unit/provider-page-helpers-3501.test.ts": "v3.8.47 #6862: feat GPT-5.6 family — 2 assert.ok(values.includes(...)) substituídos por 1 assert.deepEqual(values, [lista completa ordenada de effort tiers]) — asserção estritamente MAIS FORTE, net 69→68. Verificado legítimo. Prune após v3.8.47 mergear para main.",
|
||||
"tests/unit/vscode-token-routes.test.ts": "v3.8.47 #6862: feat GPT-5.6 family — a matriz de modelos do VS Code token route migrou de GPT-5.4/5.5 para a família 5.6 (tiers consolidados, context 200k→500k, novos slugs sol); os asserts da matriz aposentada foram substituídos pelos da nova (net 247→200). Asserts migrados ao novo catálogo, não enfraquecidos. Verificado legítimo. Prune após v3.8.47 mergear para main.",
|
||||
"tests/unit/providers-page-utils.test.ts": "v3.8.47 #6675: remoção dos providers obsoletos glhf/kluster/cablyai/inclusionai — os 2 asserts que citavam providers removidos do catálogo foram removidos junto (net 276→274). Superfície aposentada, não mascaramento. Verificado legítimo. Prune após v3.8.47 mergear para main.",
|
||||
"tests/unit/errorClassifier-noauth-403-6315.test.ts": "v3.8.50 #PENDING: the two terminal SENTINEL_BLOCKED assertions covered only the retired common ChatGPT Web executor and its removed sentinel path (4→2). The remaining no-auth/apikey 403 controls are unchanged; retirement behavior is now covered by deterministic 410/no-network regression tests. Provider surface removed, not weakened.",
|
||||
"tests/unit/live-model-catalog-reconciliation-8926.test.ts": "v3.8.50 #PENDING: six static assertions for curated common ChatGPT Web variants were removed with that provider's catalog (41→35). The remaining live-catalog reconciliation matrix is unchanged; dedicated retirement tests now assert the common ids are absent and fail closed while Codex Web controls remain. Provider surface removed, not weakened.",
|
||||
"tests/unit/provider-validation-specialty.test.ts": "v3.8.50 #PENDING: seventeen assertions exercised the removed common ChatGPT Web TLS/session validator (425→408). The validator and exclusive TLS implementation no longer ship; management retirement regressions now prove both ids return sanitized 410 without network, while other specialty validators remain covered. Provider surface removed, not weakened.",
|
||||
"_deletedWithReplacement": {
|
||||
"_comment": "Deleções de arquivo de teste com SUBSTITUTO verificado (o gate exige que o replacement exista no HEAD e seja arquivo de teste). Uso restrito ao caso 'reescrito em outro path sem rename detectável pelo -M do git'. Cada entrada precisa de reason com PR ref e passa por revisão humana no release PR. Prune após o release mergear para main.",
|
||||
"open-sse/services/__tests__/chatgptTlsClient.test.ts": {
|
||||
"replacement": "tests/unit/chatgpt-web-source-retirement.test.ts",
|
||||
"reason": "v3.8.50 #PENDING: the common ChatGPT Web provider and its exclusive TLS wrapper were retired because provenance could not be cleared. The replacement pins source absence while the independent Codex Web transport remains covered by its dedicated tests."
|
||||
},
|
||||
"tests/unit/chatgpt-image-cache.test.ts": {
|
||||
"replacement": "tests/unit/chatgpt-web-source-retirement.test.ts",
|
||||
"reason": "v3.8.50 #PENDING: the cache existed only for the retired common ChatGPT Web image implementation. The replacement proves that exclusive source stays absent; Codex Web is unaffected."
|
||||
},
|
||||
"tests/unit/chatgpt-web-async-image-ws-shapes-7357.test.ts": {
|
||||
"replacement": "tests/unit/chatgpt-web-image-handler-retirement.test.ts",
|
||||
"reason": "v3.8.50 #PENDING: the common provider's WebSocket image protocol was removed with the non-clearable implementation. The replacement proves deterministic fail-closed image handling without upstream network access."
|
||||
},
|
||||
"tests/unit/chatgpt-web-citations-escape.test.ts": {
|
||||
"replacement": "tests/unit/chatgpt-web-source-retirement.test.ts",
|
||||
"reason": "v3.8.50 #PENDING: citation parsing was exclusive to the retired common provider. The replacement pins removal of that derived source and its registry/executor surface."
|
||||
},
|
||||
"tests/unit/chatgpt-web-citations.test.ts": {
|
||||
"replacement": "tests/unit/chatgpt-web-source-retirement.test.ts",
|
||||
"reason": "v3.8.50 #PENDING: citation parsing was exclusive to the retired common provider. The replacement pins removal of that derived source and its registry/executor surface."
|
||||
},
|
||||
"tests/unit/chatgpt-web-handoff-resume.test.ts": {
|
||||
"replacement": "tests/unit/chatgpt-web-source-retirement.test.ts",
|
||||
"reason": "v3.8.50 #PENDING: handoff/resume was exclusive to the retired common provider. The replacement pins removal of the implementation instead of preserving a functional path to it."
|
||||
},
|
||||
"tests/unit/chatgpt-web-image-silentdrop.test.ts": {
|
||||
"replacement": "tests/unit/chatgpt-web-image-handler-retirement.test.ts",
|
||||
"reason": "v3.8.50 #PENDING: the old image success/error contract no longer exists. The replacement asserts a sanitized 410 PROVIDER_RETIRED response and zero upstream dispatch."
|
||||
},
|
||||
"tests/unit/chatgpt-web-max-thinking-effort.test.ts": {
|
||||
"replacement": "tests/unit/chatgpt-web-runtime-block.test.ts",
|
||||
"reason": "v3.8.50 #PENDING: model-specific thinking behavior belonged to the retired common executor. The replacement proves every explicit retired model path is blocked before dispatch."
|
||||
},
|
||||
"tests/unit/chatgpt-web-models-split.test.ts": {
|
||||
"replacement": "tests/unit/chatgpt-web-retirement.test.ts",
|
||||
"reason": "v3.8.50 #PENDING: the common provider model catalog was removed. The replacement guards catalog/registry/alias retirement and preserves positive Codex Web controls."
|
||||
},
|
||||
"tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts": {
|
||||
"replacement": "tests/unit/chatgpt-web-source-retirement.test.ts",
|
||||
"reason": "v3.8.50 #PENDING: the SHA3 helper existed only for the removed common provider's sentinel proof-of-work path. The replacement pins absence of both source surfaces."
|
||||
},
|
||||
"tests/unit/chatgpt-web-tools-5240.test.ts": {
|
||||
"replacement": "tests/unit/chatgpt-web-source-retirement.test.ts",
|
||||
"reason": "v3.8.50 #PENDING: the functional tool path depended on the retired common executor. The replacement prevents reintroduction of that executor; provider-neutral web-tools coverage remains separate."
|
||||
},
|
||||
"tests/unit/chatgpt-web-tools-7679.test.ts": {
|
||||
"replacement": "tests/unit/web-tools-contract-7679.test.ts",
|
||||
"reason": "v3.8.50 #PENDING: shared web-tools contract coverage moved to a provider-neutral filename while the common ChatGPT Web executor was retired. This is a coverage-preserving relocation."
|
||||
},
|
||||
"tests/unit/chatgpt-web.test.ts": {
|
||||
"replacement": "tests/unit/chatgpt-web-retirement.test.ts",
|
||||
"reason": "v3.8.50 #PENDING: the functional suite targeted a common executor that no longer ships due non-clearable provenance. The replacement guards deterministic retirement across registry, dispatch and Codex Web controls."
|
||||
},
|
||||
"tests/unit/unprefixed-scan-web-cookie-10848.test.ts": {
|
||||
"replacement": "tests/unit/image-registry-gpt56.test.ts",
|
||||
"reason": "v3.8.50 #PENDING: the common provider no longer participates in image catalog scans. The replacement proves its catalog entry is absent, its slashful and bare image models remain unresolved, and the Codex OAuth image catalog remains intact."
|
||||
},
|
||||
"open-sse/services/combo/__tests__/targetExhaustion.test.ts": {
|
||||
"replacement": "tests/unit/combo/combo-target-exhaustion.test.ts",
|
||||
"reason": "v3.8.44 #5976: os testes de exaustão eram flake-prone (delays Math.random, timeouts 30s, >3min no CI) e foram REESCRITOS como unit determinístico com MAIS cobertura (21 casos/52 asserts vs 13 casos/37 asserts). Documentado no commit 5fe225850. Revisão humana: apresentado ao operador no STOP #1 do release v3.8.44."
|
||||
@@ -111,7 +168,6 @@
|
||||
"tests/unit/translator-openai-to-kiro.test.ts": "v3.8.49 #8565: auto-kiro passou a ser REJEITADO com erro (KIRO_REMOVED_AUTO_ALIAS_MESSAGE, \"not a real Kiro upstream model\") em vez de mapeado silenciosamente para \"auto\"; o teste do comportamento antigo foi removido porque hoje ele asseriria o comportamento errado. A rejeição está coberta em kiro-model-aliases.test.ts (assert.throws /not a real Kiro/) e kiro-available-models.test.ts. Net 119→118. Verificado legítimo. Prune após v3.8.49 mergear para main.",
|
||||
"tests/unit/usage-providers.test.ts": "v3.8.49 #7866: o case \"qwen\" saiu de getUsageForProvider (não há mais case \"qwen\" no switch de open-sse/services/usage.ts); o teste cobria esse ramo extinto (net 20→19). Verificado legítimo. Prune após v3.8.49 mergear para main.",
|
||||
"tests/unit/usage-service-hardening.test.ts": "v3.8.49 #7866/#8565/#8013: qwen removido (−3 asserts); o Kimi/Kiro builder-id (uso profileless) passou a ter SUCESSO real em vez de erro de ARN — supportsProfilelessKiroUsage(\"builder-id\") retorna true —, trocando 1 assert de regex de erro por 3 asserts de valor; e os ids de bucket de quota do Antigravity foram atualizados para o catálogo atual. Rodado no HEAD: 23/23 passam. Net 210→209. Verificado legítimo. Prune após v3.8.49 mergear para main.",
|
||||
"tests/unit/virtual-auto-combo.test.ts": "v3.8.49 #7928/#8183: o pooling de contas passou a agrupar conexões web-session do mesmo provider numa entrada lógica com allowedConnectionIds (campo confirmado em open-sse/services/autoCombo/virtualFactory.ts), e o pool no-auth virou uma allowlist fixa (AUTO_COMBO_NOAUTH_ALLOWLIST = opencode, felo-web) — os testes antigos esperavam duplicatas e a inclusão de duckduckgo-web/theoldllm/chipotle, que hoje são corretamente excluídos. Guard dedicado em noauth-autocombo-allowlist.test.ts. Rodado no HEAD: 10/10 passam. Net 39→31. Verificado legítimo. Prune após v3.8.49 mergear para main.",
|
||||
"open-sse/services/__tests__/tierResolver.test.ts": "v3.8.49 #7866: refactor(qwen) remove o provider OAuth legado — o teste \"classifies Qwen as free\" e a entrada de qwen na lista do batch saíram junto com o provider, e os índices do batch desceram de 10 para 9 elementos (net 61→59). Superfície extinta, não enfraquecimento. Verificado legítimo. Prune após v3.8.49 mergear para main.",
|
||||
"tests/unit/plugins-welcome-banner-e2e.test.ts": "v3.8.50 #9126 (commit 8fac6bcd48): o teste único 'BUILTIN_EVENTS has all 14 events' (13 asserts .ok/.equal) foi reestruturado em 3 testes mais específicos — 'contains only emitted/public events' (assert.deepEqual da lista completa), 'does not advertise dead events' (7 asserts .equal(false) para eventos sem emissor real: onModelSelect/onComboResolve/onRateLimit/onQuotaExhaust/onProviderError/onStreamStart/onStreamEnd) e 'lifecycle events remain represented' (4 asserts .ok). Contrato mais forte (agora também nega presença dos eventos mortos), não mais fraco — a contagem líquida cai (73→61) porque o assert.deepEqual único substitui múltiplos assert.ok redundantes com a mesma cobertura. Asserts restruturados, não removidos sem substituição. Verificado legítimo.",
|
||||
"tests/unit/web-tools-translation-2820.test.ts": "v3.8.50 #9343 (commit d969555417): fix(security) exige envelope <tool> explicito — JSON puro NAO deve mais ser promovido a tool_calls. Os 5 testes foram REESCRITOS para o contrato oposto (antes: 'promove e valida name/arguments'; agora: 'toolCalls === null e content preservado'), o que naturalmente usa menos asserts: verificar a NAO-promocao custa 2 asserts, verificar o objeto promovido custava 4. Contrato mais restritivo, nao mais fraco (39->35). Verificado legitimo — a inversao esta explicita nos proprios nomes dos testes ('does NOT promote ... (#9343)').",
|
||||
|
||||
@@ -163,7 +163,7 @@ Prompt compression engines, rules, and language packs.
|
||||
Provider-specific integration guides.
|
||||
|
||||
- [CLAUDE_WEB.md](providers/CLAUDE_WEB.md) — Claude Web (cookie-auth) provider.
|
||||
- [CHATGPT_WEB.md](providers/CHATGPT_WEB.md) — ChatGPT Web (Plus/Pro + Codex) providers.
|
||||
- [CHATGPT_WEB.md](providers/CHATGPT_WEB.md) — ChatGPT Web (Codex) provider and common-provider retirement note.
|
||||
- [ALIBABA-QWEN-PROVIDER-FAMILIES.md](providers/ALIBABA-QWEN-PROVIDER-FAMILIES.md) — Alibaba and Qwen provider families.
|
||||
- [AGENTROUTER.md](providers/AGENTROUTER.md) — AgentRouter setup.
|
||||
- [ZED-DOCKER.md](providers/ZED-DOCKER.md) — Zed IDE integration under Docker.
|
||||
|
||||
@@ -17,7 +17,7 @@ It provides a single OpenAI-compatible endpoint (`/v1/*`) and routes traffic acr
|
||||
|
||||
Core capabilities:
|
||||
|
||||
- OpenAI-compatible API surface for CLI/tools (349 providers, 107 executors)
|
||||
- OpenAI-compatible API surface for CLI/tools (351 providers, 107 executors)
|
||||
- Request/response translation across provider formats
|
||||
- Model combo fallback (multi-model sequence)
|
||||
- Structured combo steps (`provider + model + connection`) with runtime ordering by `compositeTiers`
|
||||
@@ -488,9 +488,6 @@ the global circuit breaker / connection cooldown / model lockout layers:
|
||||
`claudeCodeToolRemapper.ts`
|
||||
- Claude Code fingerprint shaping: `open-sse/services/claudeCodeFingerprint.ts`
|
||||
- Claude Code obfuscation: `open-sse/services/claudeCodeObfuscation.ts`
|
||||
- ChatGPT TLS client: `open-sse/services/chatgptTlsClient.ts` (curl-impersonate
|
||||
style for ChatGPT-Web sessions)
|
||||
- ChatGPT image cache: `open-sse/services/chatgptImageCache.ts`
|
||||
|
||||
For the full stealth playbook and operational guidance, see
|
||||
[`docs/security/STEALTH_GUIDE.md`](../security/STEALTH_GUIDE.md).
|
||||
@@ -898,103 +895,103 @@ flowchart LR
|
||||
|
||||
Each provider has a specialized executor extending `BaseExecutor` (in `open-sse/executors/base.ts`), which provides URL building, header construction, retry with exponential backoff, credential refresh hooks, and the `execute()` orchestration method.
|
||||
|
||||
| Executor | Provider(s) | Special Handling |
|
||||
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| `DefaultExecutor` | OpenAI, Claude, Gemini, Qwen, OpenRouter, GLM, Kimi, MiniMax, DeepSeek, Groq, xAI, Mistral, Perplexity, Together, Fireworks, Cerebras, Cohere, NVIDIA, etc. | Dynamic URL/header config per provider |
|
||||
| `AntigravityExecutor` | Google Antigravity | Custom project/session IDs, Retry-After parsing, 429 obfuscation |
|
||||
| `AzureOpenAIExecutor` | Azure OpenAI | Deployment-based routing, api-version query enforcement |
|
||||
| `BlackboxWebExecutor` | Blackbox AI (web-mode) | Web-session reverse with TLS fingerprint emulation |
|
||||
| `ChatGPTWebExecutor` | ChatGPT web | TLS client + session cookie management (`chatgptTlsClient.ts`) |
|
||||
| `ClaudeIdentityExecutor` | Claude.ai (CCH path) | Constraint + tool-remap pipelines, fingerprint shaping |
|
||||
| `CliProxyApiExecutor` | CLIProxyAPI-compatible providers | Custom auth and protocol handling |
|
||||
| `CloudflareAiExecutor` | Cloudflare Workers AI | Account ID injection, Neurons-based usage tracking |
|
||||
| `CodexExecutor` | OpenAI Codex | Injects system instructions, forces reasoning effort |
|
||||
| `CommandCodeExecutor` | Command Code | OAuth + per-session header rotation |
|
||||
| `CursorExecutor` | Cursor IDE | ConnectRPC protocol, Protobuf encoding, request signing via checksum |
|
||||
| `DevinCliExecutor` | Devin CLI | Devin task lifecycle bridging via cloud agent module |
|
||||
| `GithubExecutor` | GitHub Copilot | Copilot token refresh, VSCode-mimicking headers |
|
||||
| `GitlabExecutor` | GitLab Duo | GitLab OAuth + project-scoped routing |
|
||||
| `GlmExecutor` | Z.AI GLM (incl. `glmt` preset) | Thinking-budget aware, GLMT preset constants |
|
||||
| `GrokWebExecutor` | xAI Grok web | Web-session reverse, mode selection (think/standard) |
|
||||
| `KieExecutor` | KIE | Custom token issuance with rotating session anchors |
|
||||
| `KiroExecutor` | AWS CodeWhisperer/Kiro | AWS EventStream binary format → SSE conversion |
|
||||
| `MuseSparkWebExecutor` | Muse Spark (web) | Web-session reverse with image-message bridging |
|
||||
| `NlpCloudExecutor` | NLP Cloud | Provider-specific request body shape |
|
||||
| `OpenCodeExecutor` | OpenCode | AI SDK compatible provider setup |
|
||||
| `PerplexityWebExecutor` | Perplexity web | Web-session reverse for chat continuation |
|
||||
| `PetalsExecutor` | Petals distributed inference | Decentralized swarm routing |
|
||||
| `PollinationsExecutor` | Pollinations AI | No API key required, rate-limited requests |
|
||||
| `QoderExecutor` | Qoder AI | PAT and OAuth support, multi-model free tier |
|
||||
| `VertexExecutor` | Google Vertex AI | Service account auth, region-based endpoints |
|
||||
| `DevinDesktopExecutor` | Devin Desktop | Imported API key + Connect-protobuf chat streaming |
|
||||
| Executor | Provider(s) | Special Handling |
|
||||
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| `DefaultExecutor` | OpenAI, Claude, Gemini, Qwen, OpenRouter, GLM, Kimi, MiniMax, DeepSeek, Groq, xAI, Mistral, Perplexity, Together, Fireworks, Cerebras, Cohere, NVIDIA, etc. | Dynamic URL/header config per provider |
|
||||
| `AntigravityExecutor` | Google Antigravity | Custom project/session IDs, Retry-After parsing, 429 obfuscation |
|
||||
| `AzureOpenAIExecutor` | Azure OpenAI | Deployment-based routing, api-version query enforcement |
|
||||
| `BlackboxWebExecutor` | Blackbox AI (web-mode) | Web-session reverse with TLS fingerprint emulation |
|
||||
| `ClaudeIdentityExecutor` | Claude.ai (CCH path) | Constraint + tool-remap pipelines, fingerprint shaping |
|
||||
| `CliProxyApiExecutor` | CLIProxyAPI-compatible providers | Custom auth and protocol handling |
|
||||
| `CloudflareAiExecutor` | Cloudflare Workers AI | Account ID injection, Neurons-based usage tracking |
|
||||
| `CodexExecutor` | OpenAI Codex | Injects system instructions, forces reasoning effort |
|
||||
| `ChatGptWebCodexExecutor` | ChatGPT Web (Codex) | Browser-session Responses API bridge with thread/turn pinning |
|
||||
| `CommandCodeExecutor` | Command Code | OAuth + per-session header rotation |
|
||||
| `CursorExecutor` | Cursor IDE | ConnectRPC protocol, Protobuf encoding, request signing via checksum |
|
||||
| `DevinCliExecutor` | Devin CLI | Devin task lifecycle bridging via cloud agent module |
|
||||
| `GithubExecutor` | GitHub Copilot | Copilot token refresh, VSCode-mimicking headers |
|
||||
| `GitlabExecutor` | GitLab Duo | GitLab OAuth + project-scoped routing |
|
||||
| `GlmExecutor` | Z.AI GLM (incl. `glmt` preset) | Thinking-budget aware, GLMT preset constants |
|
||||
| `GrokWebExecutor` | xAI Grok web | Web-session reverse, mode selection (think/standard) |
|
||||
| `KieExecutor` | KIE | Custom token issuance with rotating session anchors |
|
||||
| `KiroExecutor` | AWS CodeWhisperer/Kiro | AWS EventStream binary format → SSE conversion |
|
||||
| `MuseSparkWebExecutor` | Muse Spark (web) | Web-session reverse with image-message bridging |
|
||||
| `NlpCloudExecutor` | NLP Cloud | Provider-specific request body shape |
|
||||
| `OpenCodeExecutor` | OpenCode | AI SDK compatible provider setup |
|
||||
| `PerplexityWebExecutor` | Perplexity web | Web-session reverse for chat continuation |
|
||||
| `PetalsExecutor` | Petals distributed inference | Decentralized swarm routing |
|
||||
| `PollinationsExecutor` | Pollinations AI | No API key required, rate-limited requests |
|
||||
| `QoderExecutor` | Qoder AI | PAT and OAuth support, multi-model free tier |
|
||||
| `VertexExecutor` | Google Vertex AI | Service account auth, region-based endpoints |
|
||||
| `DevinDesktopExecutor` | Devin Desktop | Imported API key + Connect-protobuf chat streaming |
|
||||
|
||||
All other providers (including custom compatible nodes) use the `DefaultExecutor`.
|
||||
|
||||
## Provider Compatibility Matrix
|
||||
|
||||
> **Note:** The matrix below is a representative sample of the 237 registered providers in
|
||||
> **Note:** The matrix below is a representative sample of the 351 registered providers in
|
||||
> OmniRoute v3.8.0. For the canonical and continuously-updated list, refer to
|
||||
> [`docs/reference/PROVIDER_REFERENCE.md`](../reference/PROVIDER_REFERENCE.md) (auto-generated) or the source of
|
||||
> truth at `src/shared/constants/providers.ts` (Zod-validated at load).
|
||||
|
||||
| Provider | Format | Auth | Stream | Non-Stream | Token Refresh | Usage API |
|
||||
| ----------------- | ---------------- | --------------------- | ---------------- | ---------- | ------------- | ------------------ |
|
||||
| Claude | claude | API Key / OAuth | ✅ | ✅ | ✅ | ⚠️ Admin only |
|
||||
| Gemini | gemini | API Key / OAuth | ✅ | ✅ | ✅ | ⚠️ Cloud Console |
|
||||
| Antigravity | antigravity | OAuth | ✅ | ✅ | ✅ | ✅ Full quota API |
|
||||
| OpenAI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Codex | openai-responses | OAuth | ✅ forced | ❌ | ✅ | ✅ Rate limits |
|
||||
| GitHub Copilot | openai | OAuth + Copilot Token | ✅ | ✅ | ✅ | ✅ Quota snapshots |
|
||||
| Cursor | cursor | Custom checksum | ✅ | ✅ | ❌ | ❌ |
|
||||
| Kiro | kiro | AWS SSO OIDC | ✅ (EventStream) | ❌ | ✅ | ✅ Usage limits |
|
||||
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ Per request |
|
||||
| Kilo Code | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| Cline | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| Kimi Coding | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| OpenRouter | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| GLM/Kimi/MiniMax | claude | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| DeepSeek | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Groq | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| xAI (Grok) | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Mistral | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Perplexity | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Together AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Fireworks AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cerebras | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cohere | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| NVIDIA NIM | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cloudflare AI | openai | API Token + Acct ID | ✅ | ✅ | ❌ | ❌ |
|
||||
| Pollinations | openai | None (no key) | ✅ | ✅ | ❌ | ❌ |
|
||||
| Scaleway AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| LongCat | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Ollama Cloud | openai | API Key (optional) | ✅ | ✅ | ❌ | ❌ |
|
||||
| HuggingFace | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Nebius | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| SiliconFlow | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Hyperbolic | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Vertex AI | gemini | Service Account | ✅ | ✅ | ✅ | ⚠️ Cloud Console |
|
||||
| Command Code | openai | OAuth | ✅ | ✅ | ✅ | ⚠️ Per request |
|
||||
| Z.AI / GLM | openai | API Key / OAuth | ✅ | ✅ | ❌ | ❌ |
|
||||
| GLMT (preset) | claude | API Key | ✅ | ✅ | ❌ | ⚠️ Per request |
|
||||
| Kimi Coding | openai | OAuth / API Key | ✅ | ✅ | ✅ | ❌ |
|
||||
| KIE | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Devin Desktop | openai | Imported API key | ✅ (Connect→SSE) | ✅ | ❌ | ⚠️ Per request |
|
||||
| GitLab Duo | openai | OAuth (GitLab) | ✅ | ✅ | ✅ | ❌ |
|
||||
| Devin CLI | openai | Local CLI login | ✅ | ✅ | ❌ | ✅ Task API |
|
||||
| Codex Cloud | openai-responses | OAuth | ✅ | ❌ | ✅ | ✅ Rate limits |
|
||||
| Jules | openai | OAuth | ✅ | ✅ | ✅ | ✅ Task API |
|
||||
| AgentRouter | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| ChatGPT-Web | openai | Session cookie + TLS | ✅ | ✅ | ❌ | ❌ |
|
||||
| Grok-Web | openai | Session cookie | ✅ | ✅ | ❌ | ❌ |
|
||||
| Perplexity-Web | openai | Session cookie | ✅ | ✅ | ❌ | ❌ |
|
||||
| BlackBox-Web | openai | Session cookie + TLS | ✅ | ✅ | ❌ | ❌ |
|
||||
| Muse-Spark-Web | openai | Session cookie | ✅ | ✅ | ❌ | ❌ |
|
||||
| ModelScope | openai | API Key | ✅ | ✅ | ❌ | ⚠️ Quota policy |
|
||||
| BazaarLink | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Petals | openai | None | ✅ | ✅ | ❌ | ❌ |
|
||||
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ Per request |
|
||||
| OpenCode (Go/Zen) | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| CLIProxyAPI | openai | Custom | ✅ | ✅ | ❌ | ❌ |
|
||||
| Provider | Format | Auth | Stream | Non-Stream | Token Refresh | Usage API |
|
||||
| ------------------- | ---------------- | --------------------- | ---------------- | ---------- | ------------- | ------------------ |
|
||||
| Claude | claude | API Key / OAuth | ✅ | ✅ | ✅ | ⚠️ Admin only |
|
||||
| Gemini | gemini | API Key / OAuth | ✅ | ✅ | ✅ | ⚠️ Cloud Console |
|
||||
| Antigravity | antigravity | OAuth | ✅ | ✅ | ✅ | ✅ Full quota API |
|
||||
| OpenAI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Codex | openai-responses | OAuth | ✅ forced | ❌ | ✅ | ✅ Rate limits |
|
||||
| ChatGPT Web (Codex) | openai-responses | Browser session | ✅ forced | ❌ | ❌ | ❌ |
|
||||
| GitHub Copilot | openai | OAuth + Copilot Token | ✅ | ✅ | ✅ | ✅ Quota snapshots |
|
||||
| Cursor | cursor | Custom checksum | ✅ | ✅ | ❌ | ❌ |
|
||||
| Kiro | kiro | AWS SSO OIDC | ✅ (EventStream) | ❌ | ✅ | ✅ Usage limits |
|
||||
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ Per request |
|
||||
| Kilo Code | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| Cline | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| Kimi Coding | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| OpenRouter | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| GLM/Kimi/MiniMax | claude | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| DeepSeek | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Groq | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| xAI (Grok) | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Mistral | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Perplexity | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Together AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Fireworks AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cerebras | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cohere | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| NVIDIA NIM | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cloudflare AI | openai | API Token + Acct ID | ✅ | ✅ | ❌ | ❌ |
|
||||
| Pollinations | openai | None (no key) | ✅ | ✅ | ❌ | ❌ |
|
||||
| Scaleway AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| LongCat | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Ollama Cloud | openai | API Key (optional) | ✅ | ✅ | ❌ | ❌ |
|
||||
| HuggingFace | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Nebius | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| SiliconFlow | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Hyperbolic | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Vertex AI | gemini | Service Account | ✅ | ✅ | ✅ | ⚠️ Cloud Console |
|
||||
| Command Code | openai | OAuth | ✅ | ✅ | ✅ | ⚠️ Per request |
|
||||
| Z.AI / GLM | openai | API Key / OAuth | ✅ | ✅ | ❌ | ❌ |
|
||||
| GLMT (preset) | claude | API Key | ✅ | ✅ | ❌ | ⚠️ Per request |
|
||||
| Kimi Coding | openai | OAuth / API Key | ✅ | ✅ | ✅ | ❌ |
|
||||
| KIE | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Devin Desktop | openai | Imported API key | ✅ (Connect→SSE) | ✅ | ❌ | ⚠️ Per request |
|
||||
| GitLab Duo | openai | OAuth (GitLab) | ✅ | ✅ | ✅ | ❌ |
|
||||
| Devin CLI | openai | Local CLI login | ✅ | ✅ | ❌ | ✅ Task API |
|
||||
| Codex Cloud | openai-responses | OAuth | ✅ | ❌ | ✅ | ✅ Rate limits |
|
||||
| Jules | openai | OAuth | ✅ | ✅ | ✅ | ✅ Task API |
|
||||
| AgentRouter | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Grok-Web | openai | Session cookie | ✅ | ✅ | ❌ | ❌ |
|
||||
| Perplexity-Web | openai | Session cookie | ✅ | ✅ | ❌ | ❌ |
|
||||
| BlackBox-Web | openai | Session cookie + TLS | ✅ | ✅ | ❌ | ❌ |
|
||||
| Muse-Spark-Web | openai | Session cookie | ✅ | ✅ | ❌ | ❌ |
|
||||
| ModelScope | openai | API Key | ✅ | ✅ | ❌ | ⚠️ Quota policy |
|
||||
| BazaarLink | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Petals | openai | None | ✅ | ✅ | ❌ | ❌ |
|
||||
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ Per request |
|
||||
| OpenCode (Go/Zen) | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| CLIProxyAPI | openai | Custom | ✅ | ✅ | ❌ | ❌ |
|
||||
|
||||
## Format Translation Coverage
|
||||
|
||||
|
||||
@@ -233,7 +233,6 @@ v1/
|
||||
├── audio/{speech, transcriptions}/ TTS + STT
|
||||
├── batches/[id]/{cancel}, batches/ OpenAI Batches API
|
||||
├── chat/completions/ Chat Completions (the main endpoint)
|
||||
├── chatgpt-web/ ChatGPT-Web compat
|
||||
├── completions/ Legacy text completions
|
||||
├── embeddings/ Embeddings
|
||||
├── files/[id]/, files/ Files API
|
||||
@@ -292,7 +291,7 @@ table groups the actual directories and notable top-level files.
|
||||
| `jobs/` | Background jobs (`autoUpdate.ts`, …) |
|
||||
| `memory/` | Persistent memory: `store.ts`, `cache.ts`, `retrieval.ts`, `summarization.ts`, `extraction.ts`, `injection.ts`, `qdrant.ts`, `settings.ts`, `verify.ts`, `schemas.ts`, `types.ts` |
|
||||
| `monitoring/` | `observability.ts` |
|
||||
| `oauth/` | OAuth/import provider modules (22): `agy`, `antigravity`, `claude`, `cline`, `codebuddy-cn`, `codex`, `cursor`, `devin-desktop`, `ghe-copilot`, `github`, `gitlab-duo`, `grok-cli-oauth`, `grok-cli`, `kilocode`, `kimi-coding`, `kiro`, `qoder`, `raycast`, `trae`, `xai-oauth`, `zed-hosted`, `zed`, plus `services/`, `utils/`, and `constants/oauth.ts` |
|
||||
| `oauth/` | OAuth/import provider modules (22): `agy`, `antigravity`, `claude`, `cline`, `codebuddy-cn`, `codex`, `cursor`, `devin-desktop`, `ghe-copilot`, `github`, `gitlab-duo`, `grok-cli-oauth`, `grok-cli`, `kilocode`, `kimi-coding`, `kiro`, `qoder`, `raycast`, `trae`, `xai-oauth`, `zed-hosted`, `zed`, plus `services/`, `utils/`, and `constants/oauth.ts` |
|
||||
| `plugins/` | Plugin loader (`index.ts`) |
|
||||
| `promptCache/` | `prefixAnalyzer.ts`, `index.ts` |
|
||||
| `providerModels/` | Managed model lifecycle: `modelDiscovery.ts`, `managedModelImport.ts`, `managedAvailableModels.ts`, `cursorAgent.ts` |
|
||||
@@ -481,16 +480,16 @@ open-sse/
|
||||
|
||||
### 4.2 `open-sse/executors/`
|
||||
|
||||
101 provider executors, each extending `BaseExecutor` (`base.ts`):
|
||||
107 provider executors, each extending `BaseExecutor` (`base.ts`):
|
||||
|
||||
`antigravity`, `azure-openai`, `blackbox-web`, `chatgpt-web`, `cliproxyapi`,
|
||||
`cloudflare-ai`, `codex`, `commandCode`, `cursor`, `default`, `devin-cli`,
|
||||
`antigravity`, `azure-openai`, `blackbox-web`, `cliproxyapi`,
|
||||
`chatgpt-web-codex`, `cloudflare-ai`, `codex`, `commandCode`, `cursor`, `default`, `devin-cli`,
|
||||
`muse-spark-web`, `nlpcloud`, `opencode`, `perplexity-web`, `petals`,
|
||||
`pollinations`, `qoder`, `vertex`, `devin-desktop`, plus `claudeIdentity.ts`
|
||||
(shared identity helper) and `index.ts` (registry).
|
||||
|
||||
> Note: providers not listed here are served by `default.ts` using the generic
|
||||
> OpenAI-compatible executor. The full provider catalog (338 providers) lives in
|
||||
> OpenAI-compatible executor. The full provider catalog (351 providers) lives in
|
||||
> `src/shared/constants/providers.ts`.
|
||||
|
||||
### 4.3 `open-sse/translator/`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<svg viewBox="0 0 1200 842" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="OmniRoute free-tier budget: about 1.51 billion free tokens per month steady, up to about 2.13 billion in the first month with signup credits. The catalog contains 455 rows, 448 active and 7 discontinued, grouped into 40 recurring pool keys; 20 pools have a published positive monthly token budget and 20 have a zero, uncapped, or keyless budget. Honest pool-deduped math counts each shared free pool once; 15 providers carry a terms-of-service avoid flag. The 20 quantified pools are Mistral 1 billion, LLM7 150 million, Nara 150 million, Gemini 60 million, Cerebras 30 million, Cloudflare AI 30 million, API Airforce 24 million, Ollama Cloud 20 million, Groq 15 million, Bluesminds 7.2 million, SambaNova 6 million, Arcee 4.8 million, Navy 4.5 million, BazaarLink 3.6 million, OpenRouter 1.2 million, Cohere 800 thousand, HuggingChat 500 thousand, Morph 400 thousand, Hugging Face 200 thousand, and Kiro 25 thousand. One-time signup credits add about 626 million. Uncapped providers and the OpenRouter top-up boost are shown separately so they do not inflate the headline. Live usage remains available at /dashboard/free-tiers.">
|
||||
<svg viewBox="0 0 1200 842" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="OmniRoute free-tier budget: about 1.51 billion free tokens per month steady, up to about 2.13 billion in the first month with signup credits. The catalog contains 454 rows, 447 active and 7 discontinued, grouped into 40 recurring pool keys; 20 pools have a published positive monthly token budget and 20 have a zero, uncapped, or keyless budget. Honest pool-deduped math counts each shared free pool once; 15 providers carry a terms-of-service avoid flag. The 20 quantified pools are Mistral 1 billion, LLM7 150 million, Nara 150 million, Gemini 60 million, Cerebras 30 million, Cloudflare AI 30 million, API Airforce 24 million, Ollama Cloud 20 million, Groq 15 million, Bluesminds 7.2 million, SambaNova 6 million, Arcee 4.8 million, Navy 4.5 million, BazaarLink 3.6 million, OpenRouter 1.2 million, Cohere 800 thousand, HuggingChat 500 thousand, Morph 400 thousand, Hugging Face 200 thousand, and Kiro 25 thousand. One-time signup credits add about 626 million. Uncapped providers and the OpenRouter top-up boost are shown separately so they do not inflate the headline. Live usage remains available at /dashboard/free-tiers.">
|
||||
<desc>Pool-deduplicated chart of the 20 recurring free-token pools with positive published budgets, plus signup credits and uncapped providers shown separately.</desc>
|
||||
<defs>
|
||||
<pattern id="gridPaperF" width="32" height="32" patternUnits="userSpaceOnUse">
|
||||
@@ -64,7 +64,7 @@
|
||||
<text x="60" y="228" font-family="Consolas, 'Courier New', monospace" font-size="104" font-weight="800" fill="url(#gradBrandF)">~1.51B</text>
|
||||
<text x="62" y="266" font-family="Consolas, 'Courier New', monospace" font-size="15" letter-spacing="3" font-weight="700" fill="#a1a1aa">FREE TOKENS / MONTH · <tspan fill="#22c55e">STEADY</tspan></text>
|
||||
<text x="62" y="298" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="16" fill="#F7F6FC">up to <tspan font-weight="800" fill="#22c55e">~2.13B</tspan> in your first month — signup credits</text>
|
||||
<text x="62" y="326" font-family="Consolas, 'Courier New', monospace" font-size="12" fill="#71717a">documented free tiers · <tspan fill="#8b5cf6">40 recurring pools</tspan> · <tspan fill="#8b5cf6">455 catalog entries</tspan> · one endpoint</text>
|
||||
<text x="62" y="326" font-family="Consolas, 'Courier New', monospace" font-size="12" fill="#71717a">documented free tiers · <tspan fill="#8b5cf6">40 recurring pools</tspan> · <tspan fill="#8b5cf6">454 catalog entries</tspan> · one endpoint</text>
|
||||
|
||||
<!-- ═══ Panel · The honest math ═══ -->
|
||||
<rect x="680" y="84" width="460" height="216" rx="14" fill="#161b22" stroke="#ffffff" stroke-opacity="0.08" stroke-width="1"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
@@ -73,7 +73,7 @@
|
||||
<circle cx="6.6" cy="6.6" r="1.4" fill="#fdcb6e" stroke="none"/>
|
||||
</g>
|
||||
<text x="862" y="170" font-size="18" font-weight="800" fill="#fdcb6e">$0 to start</text>
|
||||
<text x="826" y="204" font-size="13.5" fill="#a1a1aa">90+ providers with a free tier, 56 free</text>
|
||||
<text x="826" y="204" font-size="13.5" fill="#a1a1aa">90+ providers with a free tier, 55 free</text>
|
||||
<text x="826" y="226" font-size="13.5" fill="#a1a1aa">forever — Qoder, Pollinations, Cloudflare,</text>
|
||||
<text x="826" y="248" font-size="13.5" fill="#a1a1aa">SiliconFlow… No card needed.</text>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
@@ -1,6 +1,6 @@
|
||||
# Free Tiers Guide: Understand and Combine Free AI Access
|
||||
|
||||
> **TL;DR**: OmniRoute registers 350 provider IDs, with **154 provider-catalog entries marked `hasFree`**. The stricter audited free-model catalog covers **40 recurring pool keys / 455 entries** (448 active + 7 discontinued). Connect several suitable providers for broader fallback capacity; every quota, approval rule, privacy policy, and paid-overage condition still applies.
|
||||
> **TL;DR**: OmniRoute registers 351 provider IDs, with **154 provider-catalog entries marked `hasFree`**. The stricter audited free-model catalog covers **40 recurring pool keys / 454 entries** (447 active + 7 discontinued). Connect several suitable providers for broader fallback capacity; every quota, approval rule, privacy policy, and paid-overage condition still applies.
|
||||
|
||||
---
|
||||
|
||||
@@ -163,8 +163,8 @@ The live, pool-deduplicated catalog currently reports:
|
||||
| ---------------------------------------------------- | -----------------------------------------------: | ----------------------------------------------------------------------------------------- |
|
||||
| Recurring quantified grant | **~1.51B tokens/month** | Shared pools counted once; excludes uncapped providers from the sum |
|
||||
| First month with signup grants | **~2.13B tokens** | Recurring total plus one-time and recurring credits |
|
||||
| Audited free-model inventory | **40 recurring pool keys / 455 catalog entries** | 448 active + 7 discontinued; distinct from the 350-provider catalog |
|
||||
| Recurring/keyless free-forever providers represented | **56** | Unique providers across recurring daily/monthly/credit/uncapped and keyless catalog types |
|
||||
| Audited free-model inventory | **40 recurring pool keys / 454 catalog entries** | 447 active + 7 discontinued; distinct from the 351-provider catalog |
|
||||
| Recurring/keyless free-forever providers represented | **55** | Unique providers across recurring daily/monthly/credit/uncapped and keyless catalog types |
|
||||
| Provider catalog entries marked `hasFree` | **154 / 350** | Broader provider metadata; not all have a quantifiable recurring quota |
|
||||
|
||||
These values are computed from `open-sse/config/freeModelCatalog.ts`; see the
|
||||
|
||||
@@ -21,7 +21,7 @@ Think of a provider like a **phone carrier**. Just as you need a phone carrier t
|
||||
| **Free** | No payment required | Kiro, OpenCode Free, Pollinations | $0 |
|
||||
| **API Key** | You need an API key | OpenAI, Anthropic, Google | Pay per use |
|
||||
| **OAuth** | Login with your account | Claude Code, GitHub Copilot | Subscription |
|
||||
| **Web Cookie** | Uses your browser session | ChatGPT Web, Gemini Web | $0 (uses your account) |
|
||||
| **Web Cookie** | Uses your browser session | ChatGPT Web (Codex), Gemini Web | $0 (uses your account) |
|
||||
|
||||
### Web Cookie Providers
|
||||
|
||||
|
||||
@@ -80,14 +80,14 @@ The exact credentials required depend on the provider.
|
||||
|
||||
Different websites store authentication differently. Some require only cookies, while others may require additional headers or tokens.
|
||||
|
||||
| Provider | Credential Format | Provider Guide |
|
||||
| ----------- | -------------------------------------------------------------- | ------------------------------- |
|
||||
| Claude Web | Full Cookie request header | `docs/providers/CLAUDE_WEB.md` |
|
||||
| ChatGPT Web | Full Cookie header or `__Secure-next-auth.session-token` value | `docs/providers/CHATGPT_WEB.md` |
|
||||
| Gemini Web | _(verify)_ | |
|
||||
| Copilot Web | _(verify)_ | |
|
||||
| Grok Web | _(verify)_ | |
|
||||
| ... | ... | ... |
|
||||
| Provider | Credential Format | Provider Guide |
|
||||
| ------------------- | -------------------------- | ------------------------------- |
|
||||
| Claude Web | Full Cookie request header | `docs/providers/CLAUDE_WEB.md` |
|
||||
| ChatGPT Web (Codex) | Full Cookie header | `docs/providers/CHATGPT_WEB.md` |
|
||||
| Gemini Web | _(verify)_ | |
|
||||
| Copilot Web | _(verify)_ | |
|
||||
| Grok Web | _(verify)_ | |
|
||||
| ... | ... | ... |
|
||||
|
||||
> Update this table as new Web Cookie providers are added or existing providers change their authentication requirements.
|
||||
|
||||
|
||||
@@ -488,9 +488,6 @@ globalnych warstwach circuit breaker / connection cooldown / model lockout:
|
||||
`claudeCodeToolRemapper.ts`
|
||||
- Kształtowanie fingerprint Claude Code: `open-sse/services/claudeCodeFingerprint.ts`
|
||||
- Obfuskacja Claude Code: `open-sse/services/claudeCodeObfuscation.ts`
|
||||
- Klient TLS ChatGPT: `open-sse/services/chatgptTlsClient.ts` (styl curl-impersonate
|
||||
dla sesji ChatGPT-Web)
|
||||
- Cache obrazów ChatGPT: `open-sse/services/chatgptImageCache.ts`
|
||||
|
||||
Pełny playbook stealth i wskazówki operacyjne: zob.
|
||||
[`docs/security/STEALTH_GUIDE.md`](../security/STEALTH_GUIDE.md).
|
||||
@@ -898,103 +895,103 @@ flowchart LR
|
||||
|
||||
Każdy dostawca ma wyspecjalizowany executor rozszerzający `BaseExecutor` (w `open-sse/executors/base.ts`), który zapewnia budowanie URL, konstrukcję nagłówków, retry z exponential backoff, hooki odświeżania poświadczeń oraz metodę orkiestracji `execute()`.
|
||||
|
||||
| Executor | Provider(s) | Specjalna obsługa |
|
||||
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `DefaultExecutor` | OpenAI, Claude, Gemini, Qwen, OpenRouter, GLM, Kimi, MiniMax, DeepSeek, Groq, xAI, Mistral, Perplexity, Together, Fireworks, Cerebras, Cohere, NVIDIA, etc. | Dynamiczna konfiguracja URL/nagłówków per dostawca |
|
||||
| `AntigravityExecutor` | Google Antigravity | Niestandardowe ID project/session, parsowanie Retry-After, obfuskacja 429 |
|
||||
| `AzureOpenAIExecutor` | Azure OpenAI | Routing oparty na deployment, egzekwowanie query api-version |
|
||||
| `BlackboxWebExecutor` | Blackbox AI (web-mode) | Reverse sesji web z emulacją fingerprint TLS |
|
||||
| `ChatGPTWebExecutor` | ChatGPT web | Klient TLS + zarządzanie cookie sesji (`chatgptTlsClient.ts`) |
|
||||
| `ClaudeIdentityExecutor` | Claude.ai (CCH path) | Potoki constraint + tool-remap, kształtowanie fingerprint |
|
||||
| `CliProxyApiExecutor` | CLIProxyAPI-compatible providers | Niestandardowa obsługa auth i protokołu |
|
||||
| `CloudflareAiExecutor` | Cloudflare Workers AI | Injekcja Account ID, śledzenie użycia oparte na Neurons |
|
||||
| `CodexExecutor` | OpenAI Codex | Wstrzykuje instrukcje systemowe, wymusza reasoning effort |
|
||||
| `CommandCodeExecutor` | Command Code | OAuth + rotacja nagłówków per sesja |
|
||||
| `CursorExecutor` | Cursor IDE | Protokół ConnectRPC, kodowanie Protobuf, podpisywanie żądań przez checksum |
|
||||
| `DevinCliExecutor` | Devin CLI | Mostkowanie cyklu życia zadań Devin przez moduł cloud agent |
|
||||
| `GithubExecutor` | GitHub Copilot | Odświeżanie tokenu Copilot, nagłówki imitujące VSCode |
|
||||
| `GitlabExecutor` | GitLab Duo | OAuth GitLab + routing w zakresie projektu |
|
||||
| `GlmExecutor` | Z.AI GLM (incl. `glmt` preset) | Świadomy thinking-budget, stałe presetu GLMT |
|
||||
| `GrokWebExecutor` | xAI Grok web | Reverse sesji web, wybór trybu (think/standard) |
|
||||
| `KieExecutor` | KIE | Niestandardowe wydawanie tokenów z rotującymi kotwicami sesji |
|
||||
| `KiroExecutor` | AWS CodeWhisperer/Kiro | Konwersja binarnego formatu AWS EventStream → SSE |
|
||||
| `MuseSparkWebExecutor` | Muse Spark (web) | Reverse sesji web z mostkowaniem image-message |
|
||||
| `NlpCloudExecutor` | NLP Cloud | Kształt body żądania specyficzny dla dostawcy |
|
||||
| `OpenCodeExecutor` | OpenCode | Konfiguracja dostawcy zgodna z AI SDK |
|
||||
| `PerplexityWebExecutor` | Perplexity web | Reverse sesji web dla kontynuacji czatu |
|
||||
| `PetalsExecutor` | Petals distributed inference | Zdecentralizowany routing swarm |
|
||||
| `PollinationsExecutor` | Pollinations AI | Klucz API niewymagany, żądania z rate limitem |
|
||||
| `QoderExecutor` | Qoder AI | Wsparcie PAT i OAuth, darmowy tier multi-model |
|
||||
| `VertexExecutor` | Google Vertex AI | Auth service account, endpointy oparte na regionie |
|
||||
| `DevinDesktopExecutor` | Devin Desktop | Importowany klucz API + strumieniowanie czatu Connect-protobuf |
|
||||
| Executor | Provider(s) | Specjalna obsługa |
|
||||
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `DefaultExecutor` | OpenAI, Claude, Gemini, Qwen, OpenRouter, GLM, Kimi, MiniMax, DeepSeek, Groq, xAI, Mistral, Perplexity, Together, Fireworks, Cerebras, Cohere, NVIDIA, etc. | Dynamiczna konfiguracja URL/nagłówków per dostawca |
|
||||
| `AntigravityExecutor` | Google Antigravity | Niestandardowe ID project/session, parsowanie Retry-After, obfuskacja 429 |
|
||||
| `AzureOpenAIExecutor` | Azure OpenAI | Routing oparty na deployment, egzekwowanie query api-version |
|
||||
| `BlackboxWebExecutor` | Blackbox AI (web-mode) | Reverse sesji web z emulacją fingerprint TLS |
|
||||
| `ClaudeIdentityExecutor` | Claude.ai (CCH path) | Potoki constraint + tool-remap, kształtowanie fingerprint |
|
||||
| `CliProxyApiExecutor` | CLIProxyAPI-compatible providers | Niestandardowa obsługa auth i protokołu |
|
||||
| `CloudflareAiExecutor` | Cloudflare Workers AI | Injekcja Account ID, śledzenie użycia oparte na Neurons |
|
||||
| `CodexExecutor` | OpenAI Codex | Wstrzykuje instrukcje systemowe, wymusza reasoning effort |
|
||||
| `ChatGptWebCodexExecutor` | ChatGPT Web (Codex) | Most Responses API przez sesję przeglądarki z przypięciem wątku/turn |
|
||||
| `CommandCodeExecutor` | Command Code | OAuth + rotacja nagłówków per sesja |
|
||||
| `CursorExecutor` | Cursor IDE | Protokół ConnectRPC, kodowanie Protobuf, podpisywanie żądań przez checksum |
|
||||
| `DevinCliExecutor` | Devin CLI | Mostkowanie cyklu życia zadań Devin przez moduł cloud agent |
|
||||
| `GithubExecutor` | GitHub Copilot | Odświeżanie tokenu Copilot, nagłówki imitujące VSCode |
|
||||
| `GitlabExecutor` | GitLab Duo | OAuth GitLab + routing w zakresie projektu |
|
||||
| `GlmExecutor` | Z.AI GLM (incl. `glmt` preset) | Świadomy thinking-budget, stałe presetu GLMT |
|
||||
| `GrokWebExecutor` | xAI Grok web | Reverse sesji web, wybór trybu (think/standard) |
|
||||
| `KieExecutor` | KIE | Niestandardowe wydawanie tokenów z rotującymi kotwicami sesji |
|
||||
| `KiroExecutor` | AWS CodeWhisperer/Kiro | Konwersja binarnego formatu AWS EventStream → SSE |
|
||||
| `MuseSparkWebExecutor` | Muse Spark (web) | Reverse sesji web z mostkowaniem image-message |
|
||||
| `NlpCloudExecutor` | NLP Cloud | Kształt body żądania specyficzny dla dostawcy |
|
||||
| `OpenCodeExecutor` | OpenCode | Konfiguracja dostawcy zgodna z AI SDK |
|
||||
| `PerplexityWebExecutor` | Perplexity web | Reverse sesji web dla kontynuacji czatu |
|
||||
| `PetalsExecutor` | Petals distributed inference | Zdecentralizowany routing swarm |
|
||||
| `PollinationsExecutor` | Pollinations AI | Klucz API niewymagany, żądania z rate limitem |
|
||||
| `QoderExecutor` | Qoder AI | Wsparcie PAT i OAuth, darmowy tier multi-model |
|
||||
| `VertexExecutor` | Google Vertex AI | Auth service account, endpointy oparte na regionie |
|
||||
| `DevinDesktopExecutor` | Devin Desktop | Importowany klucz API + strumieniowanie czatu Connect-protobuf |
|
||||
|
||||
Wszystkie pozostałe dostawcy (w tym niestandardowe węzły kompatybilne) używają `DefaultExecutor`.
|
||||
|
||||
## Macierz kompatybilności dostawców
|
||||
|
||||
> **Uwaga:** Poniższa macierz to reprezentatywna próbka spośród 237 zarejestrowanych dostawców w
|
||||
> **Uwaga:** Poniższa macierz to reprezentatywna próbka spośród 351 zarejestrowanych dostawców w
|
||||
> OmniRoute v3.8.0. Kanoniczna i stale aktualizowana lista: zob.
|
||||
> [`docs/reference/PROVIDER_REFERENCE.md`](../reference/PROVIDER_REFERENCE.md) (auto-generowana) lub źródło
|
||||
> prawdy w `src/shared/constants/providers.ts` (walidowane Zod przy ładowaniu).
|
||||
|
||||
| Dostawca | Format | Auth | Stream | Non-Stream | Token Refresh | Usage API |
|
||||
| ----------------- | ---------------- | ----------------------- | ---------------- | ---------- | ------------- | ------------------ |
|
||||
| Claude | claude | API Key / OAuth | ✅ | ✅ | ✅ | ⚠️ Tylko Admin |
|
||||
| Gemini | gemini | API Key / OAuth | ✅ | ✅ | ✅ | ⚠️ Cloud Console |
|
||||
| Antigravity | antigravity | OAuth | ✅ | ✅ | ✅ | ✅ Pełne API quota |
|
||||
| OpenAI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Codex | openai-responses | OAuth | ✅ wymuszony | ❌ | ✅ | ✅ Rate limity |
|
||||
| GitHub Copilot | openai | OAuth + Copilot Token | ✅ | ✅ | ✅ | ✅ Snapshoty quota |
|
||||
| Cursor | cursor | Niestandardowy checksum | ✅ | ✅ | ❌ | ❌ |
|
||||
| Kiro | kiro | AWS SSO OIDC | ✅ (EventStream) | ❌ | ✅ | ✅ Limity użycia |
|
||||
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ Per żądanie |
|
||||
| Kilo Code | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| Cline | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| Kimi Coding | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| OpenRouter | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| GLM/Kimi/MiniMax | claude | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| DeepSeek | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Groq | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| xAI (Grok) | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Mistral | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Perplexity | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Together AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Fireworks AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cerebras | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cohere | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| NVIDIA NIM | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cloudflare AI | openai | API Token + Acct ID | ✅ | ✅ | ❌ | ❌ |
|
||||
| Pollinations | openai | Brak (bez klucza) | ✅ | ✅ | ❌ | ❌ |
|
||||
| Scaleway AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| LongCat | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Ollama Cloud | openai | API Key (opcjonalny) | ✅ | ✅ | ❌ | ❌ |
|
||||
| HuggingFace | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Nebius | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| SiliconFlow | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Hyperbolic | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Vertex AI | gemini | Service Account | ✅ | ✅ | ✅ | ⚠️ Cloud Console |
|
||||
| Command Code | openai | OAuth | ✅ | ✅ | ✅ | ⚠️ Per żądanie |
|
||||
| Z.AI / GLM | openai | API Key / OAuth | ✅ | ✅ | ❌ | ❌ |
|
||||
| GLMT (preset) | claude | API Key | ✅ | ✅ | ❌ | ⚠️ Per żądanie |
|
||||
| Kimi Coding | openai | OAuth / API Key | ✅ | ✅ | ✅ | ❌ |
|
||||
| KIE | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Devin Desktop | openai | Importowany klucz API | ✅ (Connect→SSE) | ✅ | ❌ | ⚠️ Na żądanie |
|
||||
| GitLab Duo | openai | OAuth (GitLab) | ✅ | ✅ | ✅ | ❌ |
|
||||
| Devin CLI | openai | Lokalne logowanie CLI | ✅ | ✅ | ❌ | ✅ Task API |
|
||||
| Codex Cloud | openai-responses | OAuth | ✅ | ❌ | ✅ | ✅ Rate limity |
|
||||
| Jules | openai | OAuth | ✅ | ✅ | ✅ | ✅ Task API |
|
||||
| AgentRouter | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| ChatGPT-Web | openai | Cookie sesji + TLS | ✅ | ✅ | ❌ | ❌ |
|
||||
| Grok-Web | openai | Cookie sesji | ✅ | ✅ | ❌ | ❌ |
|
||||
| Perplexity-Web | openai | Cookie sesji | ✅ | ✅ | ❌ | ❌ |
|
||||
| BlackBox-Web | openai | Cookie sesji + TLS | ✅ | ✅ | ❌ | ❌ |
|
||||
| Muse-Spark-Web | openai | Cookie sesji | ✅ | ✅ | ❌ | ❌ |
|
||||
| ModelScope | openai | API Key | ✅ | ✅ | ❌ | ⚠️ Polityka quota |
|
||||
| BazaarLink | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Petals | openai | Brak | ✅ | ✅ | ❌ | ❌ |
|
||||
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ Per żądanie |
|
||||
| OpenCode (Go/Zen) | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| CLIProxyAPI | openai | Custom | ✅ | ✅ | ❌ | ❌ |
|
||||
| Dostawca | Format | Auth | Stream | Non-Stream | Token Refresh | Usage API |
|
||||
| ------------------- | ---------------- | ----------------------- | ---------------- | ---------- | ------------- | ------------------ |
|
||||
| Claude | claude | API Key / OAuth | ✅ | ✅ | ✅ | ⚠️ Tylko Admin |
|
||||
| Gemini | gemini | API Key / OAuth | ✅ | ✅ | ✅ | ⚠️ Cloud Console |
|
||||
| Antigravity | antigravity | OAuth | ✅ | ✅ | ✅ | ✅ Pełne API quota |
|
||||
| OpenAI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Codex | openai-responses | OAuth | ✅ wymuszony | ❌ | ✅ | ✅ Rate limity |
|
||||
| ChatGPT Web (Codex) | openai-responses | Sesja przeglądarki | ✅ wymuszony | ❌ | ❌ | ❌ |
|
||||
| GitHub Copilot | openai | OAuth + Copilot Token | ✅ | ✅ | ✅ | ✅ Snapshoty quota |
|
||||
| Cursor | cursor | Niestandardowy checksum | ✅ | ✅ | ❌ | ❌ |
|
||||
| Kiro | kiro | AWS SSO OIDC | ✅ (EventStream) | ❌ | ✅ | ✅ Limity użycia |
|
||||
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ Per żądanie |
|
||||
| Kilo Code | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| Cline | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| Kimi Coding | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| OpenRouter | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| GLM/Kimi/MiniMax | claude | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| DeepSeek | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Groq | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| xAI (Grok) | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Mistral | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Perplexity | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Together AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Fireworks AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cerebras | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cohere | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| NVIDIA NIM | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cloudflare AI | openai | API Token + Acct ID | ✅ | ✅ | ❌ | ❌ |
|
||||
| Pollinations | openai | Brak (bez klucza) | ✅ | ✅ | ❌ | ❌ |
|
||||
| Scaleway AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| LongCat | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Ollama Cloud | openai | API Key (opcjonalny) | ✅ | ✅ | ❌ | ❌ |
|
||||
| HuggingFace | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Nebius | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| SiliconFlow | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Hyperbolic | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Vertex AI | gemini | Service Account | ✅ | ✅ | ✅ | ⚠️ Cloud Console |
|
||||
| Command Code | openai | OAuth | ✅ | ✅ | ✅ | ⚠️ Per żądanie |
|
||||
| Z.AI / GLM | openai | API Key / OAuth | ✅ | ✅ | ❌ | ❌ |
|
||||
| GLMT (preset) | claude | API Key | ✅ | ✅ | ❌ | ⚠️ Per żądanie |
|
||||
| Kimi Coding | openai | OAuth / API Key | ✅ | ✅ | ✅ | ❌ |
|
||||
| KIE | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Devin Desktop | openai | Importowany klucz API | ✅ (Connect→SSE) | ✅ | ❌ | ⚠️ Na żądanie |
|
||||
| GitLab Duo | openai | OAuth (GitLab) | ✅ | ✅ | ✅ | ❌ |
|
||||
| Devin CLI | openai | Lokalne logowanie CLI | ✅ | ✅ | ❌ | ✅ Task API |
|
||||
| Codex Cloud | openai-responses | OAuth | ✅ | ❌ | ✅ | ✅ Rate limity |
|
||||
| Jules | openai | OAuth | ✅ | ✅ | ✅ | ✅ Task API |
|
||||
| AgentRouter | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Grok-Web | openai | Cookie sesji | ✅ | ✅ | ❌ | ❌ |
|
||||
| Perplexity-Web | openai | Cookie sesji | ✅ | ✅ | ❌ | ❌ |
|
||||
| BlackBox-Web | openai | Cookie sesji + TLS | ✅ | ✅ | ❌ | ❌ |
|
||||
| Muse-Spark-Web | openai | Cookie sesji | ✅ | ✅ | ❌ | ❌ |
|
||||
| ModelScope | openai | API Key | ✅ | ✅ | ❌ | ⚠️ Polityka quota |
|
||||
| BazaarLink | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Petals | openai | Brak | ✅ | ✅ | ❌ | ❌ |
|
||||
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ Per żądanie |
|
||||
| OpenCode (Go/Zen) | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| CLIProxyAPI | openai | Custom | ✅ | ✅ | ❌ | ❌ |
|
||||
|
||||
## Pokrycie tłumaczenia formatów
|
||||
|
||||
|
||||
@@ -234,7 +234,6 @@ v1/
|
||||
├── audio/{speech, transcriptions}/ TTS + STT
|
||||
├── batches/[id]/{cancel}, batches/ OpenAI Batches API
|
||||
├── chat/completions/ Chat Completions (główny endpoint)
|
||||
├── chatgpt-web/ compat ChatGPT-Web
|
||||
├── completions/ Legacy text completions
|
||||
├── embeddings/ Embeddings
|
||||
├── files/[id]/, files/ Pliki API
|
||||
@@ -482,16 +481,16 @@ open-sse/
|
||||
|
||||
### 4.2 `open-sse/executors/`
|
||||
|
||||
84 executory providerów, każdy rozszerza `BaseExecutor` (`base.ts`):
|
||||
107 executorów providerów, każdy rozszerza `BaseExecutor` (`base.ts`):
|
||||
|
||||
`antigravity`, `azure-openai`, `blackbox-web`, `chatgpt-web`, `cliproxyapi`,
|
||||
`cloudflare-ai`, `codex`, `commiCode`, `cursor`, `default`, `devin-cli`,
|
||||
`antigravity`, `azure-openai`, `blackbox-web`, `cliproxyapi`,
|
||||
`chatgpt-web-codex`, `cloudflare-ai`, `codex`, `commiCode`, `cursor`, `default`, `devin-cli`,
|
||||
`muse-spark-web`, `nlpcloud`, `opencode`, `perplexity-web`, `petals`,
|
||||
`pollinations`, `qoder`, `vertex`, `windsurf`, plus `claudeIdentity.ts`
|
||||
(współdzielony helper identity) i `index.ts` (rejestr).
|
||||
|
||||
> Uwaga: providery niewymienione tutaj są obsługiwane przez `default.ts` z generycznym
|
||||
> executorem zgodnym z OpenAI. Pełny katalog providerów (329 wpisów) jest w
|
||||
> executorem zgodnym z OpenAI. Pełny katalog providerów (351 wpisów) jest w
|
||||
> `src/shared/constants/providers.ts`.
|
||||
|
||||
### 4.3 `open-sse/translator/`
|
||||
|
||||
@@ -15,7 +15,7 @@ Traktuj providera jak **operatora komórkowego**. Tak jak potrzebujesz operatora
|
||||
| **Free** | Bez płatności | Kiro, OpenCode Free, Pollinations | $0 |
|
||||
| **API Key** | Wymagany klucz API | OpenAI, Anthropic, Google | Płatność za użycie |
|
||||
| **OAuth** | Logowanie kontem | Claude Code, GitHub Copilot | Subskrypcja |
|
||||
| **Web Cookie** | Używa sesji przeglądarki | ChatGPT Web, Gemini Web | $0 (korzysta z Twojego konta) |
|
||||
| **Web Cookie** | Używa sesji przeglądarki | ChatGPT Web (Codex), Gemini Web | $0 (korzysta z Twojego konta) |
|
||||
|
||||
### Providery Web Cookie
|
||||
|
||||
@@ -67,17 +67,17 @@ Zobacz **[WEB-COOKIE-GUIDE.md](./WEB-COOKIE-GUIDE.md)** — ogólna konfiguracja
|
||||
|
||||
Te providery oferują **darmowy dostęp** bez karty kredytowej:
|
||||
|
||||
| Provider | Darmowy limit | Modele | Jak połączyć |
|
||||
| ----------------- | ----------------- | ---------------------------------------- | ------------------ |
|
||||
| **Kiro AI** | 50 credits/mies. | Claude Sonnet 4.5, Haiku 4.5, Opus 4.6 | Bez auth |
|
||||
| **OpenCode Free** | Brak opublikowanego capu; limity obowiązują | GPT-4o, Claude, Gemini | Bez auth |
|
||||
| **Pollinations** | Bez klucza | GPT-5, Claude, Gemini, DeepSeek, Llama 4 | Bez auth |
|
||||
| **LongCat** | 10M jednorazowo | LongCat-2.0 | Klucz API + KYC |
|
||||
| **Cloudflare AI** | 10K neurons/dzień | 50+ modeli | Bez auth |
|
||||
| **NVIDIA NIM** | ~40 RPM | 129 modeli | Wymagany klucz API |
|
||||
| **Cerebras** | 1M tokenów/dzień | Qwen3 235B, GPT-OSS 120B | Wymagany klucz API |
|
||||
| **Qwen** | Brak opublikowanego capu; limity obowiązują | Qwen3-coder-plus/flash/next | Bez auth |
|
||||
| **Qoder** | Brak opublikowanego capu; limity obowiązują | Kimi-K2, DeepSeek-R1, Qwen3-coder | Bez auth |
|
||||
| Provider | Darmowy limit | Modele | Jak połączyć |
|
||||
| ----------------- | ------------------------------------------- | ---------------------------------------- | ------------------ |
|
||||
| **Kiro AI** | 50 credits/mies. | Claude Sonnet 4.5, Haiku 4.5, Opus 4.6 | Bez auth |
|
||||
| **OpenCode Free** | Brak opublikowanego capu; limity obowiązują | GPT-4o, Claude, Gemini | Bez auth |
|
||||
| **Pollinations** | Bez klucza | GPT-5, Claude, Gemini, DeepSeek, Llama 4 | Bez auth |
|
||||
| **LongCat** | 10M jednorazowo | LongCat-2.0 | Klucz API + KYC |
|
||||
| **Cloudflare AI** | 10K neurons/dzień | 50+ modeli | Bez auth |
|
||||
| **NVIDIA NIM** | ~40 RPM | 129 modeli | Wymagany klucz API |
|
||||
| **Cerebras** | 1M tokenów/dzień | Qwen3 235B, GPT-OSS 120B | Wymagany klucz API |
|
||||
| **Qwen** | Brak opublikowanego capu; limity obowiązują | Qwen3-coder-plus/flash/next | Bez auth |
|
||||
| **Qoder** | Brak opublikowanego capu; limity obowiązują | Kimi-K2, DeepSeek-R1, Qwen3-coder | Bez auth |
|
||||
|
||||
**Wskazówka**: Podłącz kilka darmowych providerów, aby zwiększyć pokrycie fallbacku.
|
||||
Brak opublikowanego limitu tokenów nie oznacza nieograniczonej dostępności.
|
||||
|
||||
@@ -80,14 +80,14 @@ Dokładne wymagane poświadczenia zależą od dostawcy.
|
||||
|
||||
Różne witryny przechowują uwierzytelnianie na różne sposoby. Niektóre wymagają tylko ciasteczek, inne mogą wymagać dodatkowych nagłówków lub tokenów.
|
||||
|
||||
| Dostawca | Format poświadczeń | Przewodnik dostawcy |
|
||||
| ----------- | ----------------------------- | ------------------------------ |
|
||||
| Claude Web | Pełny nagłówek żądania Cookie | `docs/providers/CLAUDE_WEB.md` |
|
||||
| ChatGPT Web | _(verify)_ | |
|
||||
| Gemini Web | _(verify)_ | |
|
||||
| Copilot Web | _(verify)_ | |
|
||||
| Grok Web | _(verify)_ | |
|
||||
| ... | ... | ... |
|
||||
| Dostawca | Format poświadczeń | Przewodnik dostawcy |
|
||||
| ------------------- | ----------------------------- | ------------------------------- |
|
||||
| Claude Web | Pełny nagłówek żądania Cookie | `docs/providers/CLAUDE_WEB.md` |
|
||||
| ChatGPT Web (Codex) | Pełny nagłówek Cookie | `docs/providers/CHATGPT_WEB.md` |
|
||||
| Gemini Web | _(verify)_ | |
|
||||
| Copilot Web | _(verify)_ | |
|
||||
| Grok Web | _(verify)_ | |
|
||||
| ... | ... | ... |
|
||||
|
||||
> Aktualizuj tę tabelę, gdy dodawani są nowi dostawcy Web Cookie lub gdy istniejący dostawcy zmieniają wymagania uwierzytelniania.
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ do scraperów OAuth/subskrypcyjnych. Wynikowy stan terminalny dotyczy **połącz
|
||||
nie providera.
|
||||
|
||||
Przy tym wbudowane _łańcuchy_ są zorientowane na providery subskrypcyjne/OAuth
|
||||
z realnym ryzykiem bana (ChatGPT Web, Claude Web, Codex, Muse Spark, Antigravity).
|
||||
z realnym ryzykiem bana (ChatGPT Web Codex, Claude Web, Codex, Muse Spark, Antigravity).
|
||||
Provider z kluczem API odpali detektor tylko wtedy, gdy ciało błędu dosłownie
|
||||
zawiera jeden z podciągów.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ lastUpdated: 2026-06-28
|
||||
|
||||
# Przewodnik po stealth
|
||||
|
||||
> **Source of truth:** `open-sse/utils/tlsClient.ts`, `open-sse/services/{chatgptTlsClient,claudeCodeCCH,claudeCodeFingerprint,claudeCodeObfuscation,claudeCodeCompatible}.ts`, `open-sse/config/cliFingerprints.ts`, `src/mitm/`
|
||||
> **Source of truth:** `open-sse/utils/tlsClient.ts`, `open-sse/services/{claudeCodeCCH,claudeCodeFingerprint,claudeCodeObfuscation,claudeCodeCompatible}.ts`, `open-sse/config/cliFingerprints.ts`, `src/mitm/`
|
||||
> **Last updated:** 2026-06-28 — v3.8.40
|
||||
> **Audience:** Engineers maintaining provider-specific stealth integrations.
|
||||
|
||||
@@ -14,7 +14,7 @@ OmniRoute integruje się z providerami, których edge aktywnie fingerprintuje ni
|
||||
|
||||
## Uwaga prawna i etyczna
|
||||
|
||||
Funkcje stealth istnieją po to, by OmniRoute mógł działać jako warstwa kompatybilności między oficjalnymi kontami użytkownika (Claude Code CLI, ChatGPT Desktop/Web, Antigravity, Cursor itd.) a ujednoliconym API OmniRoute. **Nie** służą do omijania fraud detection, współdzielenia poświadczeń ani naruszania Terms of Service providera. Maintainerzy oczekują, że operatorzy będą przestrzegać upstream ToS, które zaakceptowali przy tworzeniu kont.
|
||||
Funkcje stealth istnieją po to, by OmniRoute mógł działać jako warstwa kompatybilności między oficjalnymi kontami użytkownika (Claude Code CLI, Codex, Antigravity, Cursor itd.) a ujednoliconym API OmniRoute. **Nie** służą do omijania fraud detection, współdzielenia poświadczeń ani naruszania Terms of Service providera. Maintainerzy oczekują, że operatorzy będą przestrzegać upstream ToS, które zaakceptowali przy tworzeniu kont.
|
||||
|
||||
---
|
||||
|
||||
@@ -29,20 +29,6 @@ Lazy-loaded sesja `wreq-js`, która impersonuje **Chrome 124 na macOS**. Używan
|
||||
- Timeout: `TLS_CLIENT_TIMEOUT_MS` (dziedziczy z `FETCH_TIMEOUT_MS`, domyślnie 600000)
|
||||
- Response z `wreq-js` jest zgodny z fetch (`headers`, `text()`, `json()`, `clone()`, `body`).
|
||||
|
||||
### `open-sse/services/chatgptTlsClient.ts` — tls-client-node (Firefox 148)
|
||||
|
||||
Dedykowany impersonator TLS dla `chatgpt.com`. Konfiguracja Cloudflare ChatGPT pinuje `cf_clearance` do JA3/JA4 + kolejności ramek HTTP/2 SETTINGS — handshake undici dostaje `cf-mitigated: challenge` nawet przy poprawnych cookies.
|
||||
|
||||
- Profile: `firefox_148` (musi pasować do wysyłanego `User-Agent` Firefox 148)
|
||||
- Mode: `runtimeMode: "native"` (shared library ładowana przez koffi; unika managed sidecar HTTP)
|
||||
- `withRandomTLSExtensionOrder: true`
|
||||
- `tlsFetchChatGpt(url, options)` obsługuje streaming (zapisuje body do pliku tymczasowego, tailed jako `ReadableStream`)
|
||||
- Hang detection: `raceWithTimeout` + `TlsClientHangError` wywołuje `resetClientCache()`, więc kolejne wywołanie respawnuje binding
|
||||
- Proxy resolution (priority): per-call `proxyUrl` → `OMNIROUTE_TLS_PROXY_URL` → `HTTPS_PROXY`/`HTTP_PROXY`/`ALL_PROXY` (natywny binding **nie** czyta tych env sam; trzeba je przekazać)
|
||||
- Errors: `TlsClientUnavailableError` (brak binary), `TlsClientHangError` (binding w deadlocku)
|
||||
|
||||
---
|
||||
|
||||
## Pakiet stealth Claude Code
|
||||
|
||||
Gdy `cliCompatMode` jest włączony, OmniRoute przekształca wychodzące żądania Claude tak, by były nieodróżnialne od ruchu `claude-cli`. Współpracują trzy moduły:
|
||||
@@ -253,17 +239,17 @@ OmniRoute czyści inbound nagłówki klienta przed forwardem, by żądanie przyc
|
||||
2. Wyodrębnij JA3/JA4 i literową kolejność nagłówków
|
||||
3. Zaktualizuj odpowiedni wpis `CLI_FINGERPRINTS[...]`
|
||||
4. Podbij pasujący domyślny `*_USER_AGENT` w `.env.example`
|
||||
5. Jeśli zmienił się sam TLS handshake: zaktualizuj `chatgptTlsClient.ts::CHATGPT_PROFILE` lub opcję wreq-js `browser:`
|
||||
6. Odpal `chatgptTlsClient.test.ts` i ręcznego canary przeciwko żywemu providerowi
|
||||
5. Jeśli zmienił się sam TLS handshake, zaktualizuj odpowiedni wrapper providera lub opcję wreq-js `browser:`
|
||||
6. Odpal testy TLS danego providera i ręcznego canary przeciwko żywemu providerowi
|
||||
7. Wypuść w patch release; udokumentuj w `CHANGELOG.md`
|
||||
|
||||
---
|
||||
|
||||
## Testy
|
||||
|
||||
- `open-sse/services/__tests__/chatgptTlsClient.test.ts` — priorytet resolution proxy, obsługa abort, hang recovery
|
||||
- `open-sse/services/__tests__/claudeTlsClient.test.ts` — zachowanie współdzielonego wrappera TLS
|
||||
- `tests/unit/anthropic-cache-fingerprint.test.ts` — determinizm fingerprintu
|
||||
- `tests/unit/chatgpt-web.test.ts` — end-to-end ścieżka stealth dla ChatGPT
|
||||
- `tests/unit/chatgpt-web-source-retirement.test.ts` — wspólne źródło ChatGPT Web pozostaje nieobecne, a Codex Web pozostaje zachowany
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -469,9 +469,6 @@ camadas globais de disjuntor / cooldown de conexão / bloqueio de modelo:
|
||||
`claudeCodeToolRemapper.ts`
|
||||
- Modelagem de impressão digital do Claude Code: `open-sse/services/claudeCodeFingerprint.ts`
|
||||
- Ofuscação do Claude Code: `open-sse/services/claudeCodeObfuscation.ts`
|
||||
- Cliente TLS do ChatGPT: `open-sse/services/chatgptTlsClient.ts` (estilo de
|
||||
impersonação curl para sessões do ChatGPT-Web)
|
||||
- Cache de imagem do ChatGPT: `open-sse/services/chatgptImageCache.ts`
|
||||
|
||||
Para o guia completo de furtividade e orientações operacionais, veja
|
||||
[`docs/security/STEALTH_GUIDE.md`](../security/STEALTH_GUIDE.md).
|
||||
@@ -879,35 +876,35 @@ flowchart LR
|
||||
|
||||
Cada provedor tem um executor especializado que estende `BaseExecutor` (em `open-sse/executors/base.ts`), que fornece construção de URL, construção de cabeçalhos, tentativas com retrocesso exponencial, ganchos de atualização de credenciais e o método de orquestração `execute()`.
|
||||
|
||||
| Executor | Provedor(es) | Tratamento Especial |
|
||||
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| `DefaultExecutor` | OpenAI, Claude, Gemini, Qwen, OpenRouter, GLM, Kimi, MiniMax, DeepSeek, Groq, xAI, Mistral, Perplexity, Together, Fireworks, Cerebras, Cohere, NVIDIA, etc. | Configuração dinâmica de URL/cabeçalho por provedor |
|
||||
| `AntigravityExecutor` | Google Antigravity | IDs de projeto/sessão personalizados, análise de Retry-After, ofuscação de 429 |
|
||||
| `AzureOpenAIExecutor` | Azure OpenAI | Roteamento baseado em implantação, aplicação de consulta de api-version |
|
||||
| `BlackboxWebExecutor` | Blackbox AI (modo web) | Reversão de sessão web com emulação de impressão digital TLS |
|
||||
| `ChatGPTWebExecutor` | ChatGPT web | Gerenciamento de cliente TLS + cookie de sessão (`chatgptTlsClient.ts`) |
|
||||
| `ClaudeIdentityExecutor` | Claude.ai (caminho CCH) | Pipelines de restrição + remapeamento de ferramentas, modelagem de impressão digital |
|
||||
| `CliProxyApiExecutor` | Provedores compatíveis com CLIProxyAPI | Manipulação personalizada de autenticação e protocolo |
|
||||
| `CloudflareAiExecutor` | Cloudflare Workers AI | Injeção de ID de conta, rastreamento de uso baseado em Neurons |
|
||||
| `CodexExecutor` | OpenAI Codex | Injeções de instruções do sistema, força de esforço de raciocínio |
|
||||
| `CommandCodeExecutor` | Código de Comando | Rotação de cabeçalho por sessão + OAuth |
|
||||
| `CursorExecutor` | Cursor IDE | Protocolo ConnectRPC, codificação Protobuf, assinatura de requisições via checksum |
|
||||
| `DevinCliExecutor` | Devin CLI | Conexão do ciclo de vida da tarefa Devin via módulo de agente em nuvem |
|
||||
| `GithubExecutor` | GitHub Copilot | Atualização de token do Copilot, cabeçalhos imitando VSCode |
|
||||
| `GitlabExecutor` | GitLab Duo | Roteamento baseado em projeto + OAuth do GitLab |
|
||||
| `GlmExecutor` | Z.AI GLM (incl. preset `glmt`) | Consciente do orçamento de pensamento, constantes do preset GLMT |
|
||||
| `GrokWebExecutor` | xAI Grok web | Reversão de sessão web, seleção de modo (pensar/padrão) |
|
||||
| `KieExecutor` | KIE | Emissão de token personalizada com âncoras de sessão rotativas |
|
||||
| `KiroExecutor` | AWS CodeWhisperer/Kiro | Formato binário do AWS EventStream → conversão para SSE |
|
||||
| `MuseSparkWebExecutor` | Muse Spark (web) | Reversão de sessão web com integração de imagem-mensagem |
|
||||
| `NlpCloudExecutor` | NLP Cloud | Formato de corpo de requisição específico do provedor |
|
||||
| `OpenCodeExecutor` | OpenCode | Configuração de provedor compatível com AI SDK |
|
||||
| `PerplexityWebExecutor` | Perplexity web | Reversão de sessão web para continuidade de chat |
|
||||
| `PetalsExecutor` | Inferência distribuída Petals | Roteamento de enxame descentralizado |
|
||||
| `PollinationsExecutor` | Pollinations AI | Nenhuma chave de API necessária, requisições limitadas por taxa |
|
||||
| `QoderExecutor` | Qoder AI | Suporte a PAT e OAuth, nível gratuito multi-modelo |
|
||||
| `VertexExecutor` | Google Vertex AI | Autenticação de conta de serviço, endpoints baseados em região |
|
||||
| `DevinDesktopExecutor` | Devin Desktop | Chave de API importada + streaming de chat Connect-protobuf |
|
||||
| Executor | Provedor(es) | Tratamento Especial |
|
||||
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| `DefaultExecutor` | OpenAI, Claude, Gemini, Qwen, OpenRouter, GLM, Kimi, MiniMax, DeepSeek, Groq, xAI, Mistral, Perplexity, Together, Fireworks, Cerebras, Cohere, NVIDIA, etc. | Configuração dinâmica de URL/cabeçalho por provedor |
|
||||
| `AntigravityExecutor` | Google Antigravity | IDs de projeto/sessão personalizados, análise de Retry-After, ofuscação de 429 |
|
||||
| `AzureOpenAIExecutor` | Azure OpenAI | Roteamento baseado em implantação, aplicação de consulta de api-version |
|
||||
| `BlackboxWebExecutor` | Blackbox AI (modo web) | Reversão de sessão web com emulação de impressão digital TLS |
|
||||
| `ClaudeIdentityExecutor` | Claude.ai (caminho CCH) | Pipelines de restrição + remapeamento de ferramentas, modelagem de impressão digital |
|
||||
| `CliProxyApiExecutor` | Provedores compatíveis com CLIProxyAPI | Manipulação personalizada de autenticação e protocolo |
|
||||
| `CloudflareAiExecutor` | Cloudflare Workers AI | Injeção de ID de conta, rastreamento de uso baseado em Neurons |
|
||||
| `CodexExecutor` | OpenAI Codex | Injeções de instruções do sistema, força de esforço de raciocínio |
|
||||
| `ChatGptWebCodexExecutor` | ChatGPT Web (Codex) | Ponte Responses API por sessão de navegador com fixação de thread/turn |
|
||||
| `CommandCodeExecutor` | Código de Comando | Rotação de cabeçalho por sessão + OAuth |
|
||||
| `CursorExecutor` | Cursor IDE | Protocolo ConnectRPC, codificação Protobuf, assinatura de requisições via checksum |
|
||||
| `DevinCliExecutor` | Devin CLI | Conexão do ciclo de vida da tarefa Devin via módulo de agente em nuvem |
|
||||
| `GithubExecutor` | GitHub Copilot | Atualização de token do Copilot, cabeçalhos imitando VSCode |
|
||||
| `GitlabExecutor` | GitLab Duo | Roteamento baseado em projeto + OAuth do GitLab |
|
||||
| `GlmExecutor` | Z.AI GLM (incl. preset `glmt`) | Consciente do orçamento de pensamento, constantes do preset GLMT |
|
||||
| `GrokWebExecutor` | xAI Grok web | Reversão de sessão web, seleção de modo (pensar/padrão) |
|
||||
| `KieExecutor` | KIE | Emissão de token personalizada com âncoras de sessão rotativas |
|
||||
| `KiroExecutor` | AWS CodeWhisperer/Kiro | Formato binário do AWS EventStream → conversão para SSE |
|
||||
| `MuseSparkWebExecutor` | Muse Spark (web) | Reversão de sessão web com integração de imagem-mensagem |
|
||||
| `NlpCloudExecutor` | NLP Cloud | Formato de corpo de requisição específico do provedor |
|
||||
| `OpenCodeExecutor` | OpenCode | Configuração de provedor compatível com AI SDK |
|
||||
| `PerplexityWebExecutor` | Perplexity web | Reversão de sessão web para continuidade de chat |
|
||||
| `PetalsExecutor` | Inferência distribuída Petals | Roteamento de enxame descentralizado |
|
||||
| `PollinationsExecutor` | Pollinations AI | Nenhuma chave de API necessária, requisições limitadas por taxa |
|
||||
| `QoderExecutor` | Qoder AI | Suporte a PAT e OAuth, nível gratuito multi-modelo |
|
||||
| `VertexExecutor` | Google Vertex AI | Autenticação de conta de serviço, endpoints baseados em região |
|
||||
| `DevinDesktopExecutor` | Devin Desktop | Chave de API importada + streaming de chat Connect-protobuf |
|
||||
|
||||
Todos os outros provedores (incluindo nós compatíveis personalizados) usam o `DefaultExecutor`.
|
||||
|
||||
@@ -918,65 +915,65 @@ Todos os outros provedores (incluindo nós compatíveis personalizados) usam o `
|
||||
> [`docs/reference/PROVIDER_REFERENCE.md`](../reference/PROVIDER_REFERENCE.md) (gerada automaticamente) ou a fonte
|
||||
> de verdade em `src/shared/constants/providers.ts` (validada pelo Zod na carga).
|
||||
|
||||
| Provedor | Formato | Autenticação | Stream | Não-Stream | Atualização de Token | API de Uso |
|
||||
| ----------------- | ---------------- | -------------------------- | ---------------- | ---------- | -------------------- | -------------------- |
|
||||
| Claude | claude | Chave de API / OAuth | ✅ | ✅ | ✅ | ⚠️ Somente Admin |
|
||||
| Gemini | gemini | Chave de API / OAuth | ✅ | ✅ | ✅ | ⚠️ Console da Nuvem |
|
||||
| Antigravity | antigravity | OAuth | ✅ | ✅ | ✅ | ✅ API de cota total |
|
||||
| OpenAI | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Codex | openai-responses | OAuth | ✅ forçado | ❌ | ✅ | ✅ Limites de taxa |
|
||||
| GitHub Copilot | openai | OAuth + Token Copilot | ✅ | ✅ | ✅ | ✅ Capturas de cota |
|
||||
| Cursor | cursor | Checksum personalizado | ✅ | ✅ | ❌ | ❌ |
|
||||
| Kiro | kiro | AWS SSO OIDC | ✅ (EventStream) | ❌ | ✅ | ✅ Limites de uso |
|
||||
| Qwen | openai | OAuth | ✅ | ✅ | ✅ | ⚠️ Por solicitação |
|
||||
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ Por solicitação |
|
||||
| Kilo Code | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| Cline | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| Kimi Coding | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| OpenRouter | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| GLM/Kimi/MiniMax | claude | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| DeepSeek | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Groq | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| xAI (Grok) | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Mistral | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Perplexity | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Together AI | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Fireworks AI | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cerebras | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cohere | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| NVIDIA NIM | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cloudflare AI | openai | Token de API + ID da conta | ✅ | ✅ | ❌ | ❌ |
|
||||
| Pollinations | openai | Nenhum (sem chave) | ✅ | ✅ | ❌ | ❌ |
|
||||
| Scaleway AI | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| LongCat | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Ollama Cloud | openai | Chave de API (opcional) | ✅ | ✅ | ❌ | ❌ |
|
||||
| HuggingFace | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Nebius | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| SiliconFlow | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Hyperbolic | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Vertex AI | gemini | Conta de Serviço | ✅ | ✅ | ✅ | ⚠️ Console da Nuvem |
|
||||
| Command Code | openai | OAuth | ✅ | ✅ | ✅ | ⚠️ Por solicitação |
|
||||
| Z.AI / GLM | openai | Chave de API / OAuth | ✅ | ✅ | ❌ | ❌ |
|
||||
| GLMT (preset) | claude | Chave de API | ✅ | ✅ | ❌ | ⚠️ Por solicitação |
|
||||
| Kimi Coding | openai | OAuth / Chave de API | ✅ | ✅ | ✅ | ❌ |
|
||||
| KIE | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Devin Desktop | openai | Chave de API importada | ✅ (Connect→SSE) | ✅ | ❌ | ⚠️ Por solicitação |
|
||||
| GitLab Duo | openai | OAuth (GitLab) | ✅ | ✅ | ✅ | ❌ |
|
||||
| Devin CLI | openai | Login local da CLI | ✅ | ✅ | ❌ | ✅ API de Tarefas |
|
||||
| Codex Cloud | openai-responses | OAuth | ✅ | ❌ | ✅ | ✅ Limites de taxa |
|
||||
| Jules | openai | OAuth | ✅ | ✅ | ✅ | ✅ API de Tarefas |
|
||||
| AgentRouter | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| ChatGPT-Web | openai | Cookie de sessão + TLS | ✅ | ✅ | ❌ | ❌ |
|
||||
| Grok-Web | openai | Cookie de sessão | ✅ | ✅ | ❌ | ❌ |
|
||||
| Perplexity-Web | openai | Cookie de sessão | ✅ | ✅ | ❌ | ❌ |
|
||||
| BlackBox-Web | openai | Cookie de sessão + TLS | ✅ | ✅ | ❌ | ❌ |
|
||||
| Muse-Spark-Web | openai | Cookie de sessão | ✅ | ✅ | ❌ | ❌ |
|
||||
| ModelScope | openai | Chave de API | ✅ | ✅ | ❌ | ⚠️ Política de cota |
|
||||
| BazaarLink | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Petals | openai | Nenhum | ✅ | ✅ | ❌ | ❌ |
|
||||
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ Por solicitação |
|
||||
| OpenCode (Go/Zen) | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| CLIProxyAPI | openai | Personalizado | ✅ | ✅ | ❌ | ❌ |
|
||||
| Provedor | Formato | Autenticação | Stream | Não-Stream | Atualização de Token | API de Uso |
|
||||
| ------------------- | ---------------- | -------------------------- | ---------------- | ---------- | -------------------- | -------------------- |
|
||||
| Claude | claude | Chave de API / OAuth | ✅ | ✅ | ✅ | ⚠️ Somente Admin |
|
||||
| Gemini | gemini | Chave de API / OAuth | ✅ | ✅ | ✅ | ⚠️ Console da Nuvem |
|
||||
| Antigravity | antigravity | OAuth | ✅ | ✅ | ✅ | ✅ API de cota total |
|
||||
| OpenAI | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Codex | openai-responses | OAuth | ✅ forçado | ❌ | ✅ | ✅ Limites de taxa |
|
||||
| ChatGPT Web (Codex) | openai-responses | Sessão de navegador | ✅ forçado | ❌ | ❌ | ❌ |
|
||||
| GitHub Copilot | openai | OAuth + Token Copilot | ✅ | ✅ | ✅ | ✅ Capturas de cota |
|
||||
| Cursor | cursor | Checksum personalizado | ✅ | ✅ | ❌ | ❌ |
|
||||
| Kiro | kiro | AWS SSO OIDC | ✅ (EventStream) | ❌ | ✅ | ✅ Limites de uso |
|
||||
| Qwen | openai | OAuth | ✅ | ✅ | ✅ | ⚠️ Por solicitação |
|
||||
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ Por solicitação |
|
||||
| Kilo Code | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| Cline | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| Kimi Coding | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| OpenRouter | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| GLM/Kimi/MiniMax | claude | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| DeepSeek | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Groq | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| xAI (Grok) | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Mistral | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Perplexity | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Together AI | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Fireworks AI | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cerebras | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cohere | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| NVIDIA NIM | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cloudflare AI | openai | Token de API + ID da conta | ✅ | ✅ | ❌ | ❌ |
|
||||
| Pollinations | openai | Nenhum (sem chave) | ✅ | ✅ | ❌ | ❌ |
|
||||
| Scaleway AI | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| LongCat | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Ollama Cloud | openai | Chave de API (opcional) | ✅ | ✅ | ❌ | ❌ |
|
||||
| HuggingFace | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Nebius | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| SiliconFlow | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Hyperbolic | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Vertex AI | gemini | Conta de Serviço | ✅ | ✅ | ✅ | ⚠️ Console da Nuvem |
|
||||
| Command Code | openai | OAuth | ✅ | ✅ | ✅ | ⚠️ Por solicitação |
|
||||
| Z.AI / GLM | openai | Chave de API / OAuth | ✅ | ✅ | ❌ | ❌ |
|
||||
| GLMT (preset) | claude | Chave de API | ✅ | ✅ | ❌ | ⚠️ Por solicitação |
|
||||
| Kimi Coding | openai | OAuth / Chave de API | ✅ | ✅ | ✅ | ❌ |
|
||||
| KIE | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Devin Desktop | openai | Chave de API importada | ✅ (Connect→SSE) | ✅ | ❌ | ⚠️ Por solicitação |
|
||||
| GitLab Duo | openai | OAuth (GitLab) | ✅ | ✅ | ✅ | ❌ |
|
||||
| Devin CLI | openai | Login local da CLI | ✅ | ✅ | ❌ | ✅ API de Tarefas |
|
||||
| Codex Cloud | openai-responses | OAuth | ✅ | ❌ | ✅ | ✅ Limites de taxa |
|
||||
| Jules | openai | OAuth | ✅ | ✅ | ✅ | ✅ API de Tarefas |
|
||||
| AgentRouter | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Grok-Web | openai | Cookie de sessão | ✅ | ✅ | ❌ | ❌ |
|
||||
| Perplexity-Web | openai | Cookie de sessão | ✅ | ✅ | ❌ | ❌ |
|
||||
| BlackBox-Web | openai | Cookie de sessão + TLS | ✅ | ✅ | ❌ | ❌ |
|
||||
| Muse-Spark-Web | openai | Cookie de sessão | ✅ | ✅ | ❌ | ❌ |
|
||||
| ModelScope | openai | Chave de API | ✅ | ✅ | ❌ | ⚠️ Política de cota |
|
||||
| BazaarLink | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
|
||||
| Petals | openai | Nenhum | ✅ | ✅ | ❌ | ❌ |
|
||||
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ Por solicitação |
|
||||
| OpenCode (Go/Zen) | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| CLIProxyAPI | openai | Personalizado | ✅ | ✅ | ❌ | ❌ |
|
||||
|
||||
## Cobertura de Tradução de Formato
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ OmniRoute 是基于 Next.js 构建的本地 AI 路由网关和控制台。
|
||||
|
||||
核心能力:
|
||||
|
||||
- OpenAI 兼容的 API 接口,供 CLI/工具使用(237 个服务商、68 个执行器)
|
||||
- OpenAI 兼容的 API 接口,供 CLI/工具使用(351 个服务商、107 个执行器)
|
||||
- 跨服务商格式的请求/响应转换
|
||||
- 模型 Combo 容灾(多模型序列)
|
||||
- 结构化 Combo 步骤(`服务商 + 模型 + 连接`),通过 `compositeTiers` 在运行时排序
|
||||
@@ -487,9 +487,6 @@ FSM 状态转换反馈到 Auto Combo 的评分中,使后台/自动化任务偏
|
||||
`claudeCodeToolRemapper.ts`
|
||||
- Claude Code 指纹塑造:`open-sse/services/claudeCodeFingerprint.ts`
|
||||
- Claude Code 混淆:`open-sse/services/claudeCodeObfuscation.ts`
|
||||
- ChatGPT TLS 客户端:`open-sse/services/chatgptTlsClient.ts`(为 ChatGPT-Web 会话
|
||||
提供 curl-impersonate 风格的 TLS 指纹)
|
||||
- ChatGPT 图片缓存:`open-sse/services/chatgptImageCache.ts`
|
||||
|
||||
完整隐身策略和操作指南参见
|
||||
[`docs/security/STEALTH_GUIDE.md`](../security/STEALTH_GUIDE.md)。
|
||||
@@ -896,104 +893,104 @@ flowchart LR
|
||||
|
||||
每个服务商都有一个继承 `BaseExecutor`(在 `open-sse/executors/base.ts` 中)的专用执行器,该基类提供了 URL 构建、Header 构造、带指数退避的重试、凭据刷新钩子以及 `execute()` 编排方法。
|
||||
|
||||
| 执行器 | 服务商 | 特殊处理 |
|
||||
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
|
||||
| `DefaultExecutor` | OpenAI, Claude, Gemini, Qwen, OpenRouter, GLM, Kimi, MiniMax, DeepSeek, Groq, xAI, Mistral, Perplexity, Together, Fireworks, Cerebras, Cohere, NVIDIA 等 | 每服务商动态 URL/Header 配置 |
|
||||
| `AntigravityExecutor` | Google Antigravity | 自定义项目/会话 ID、Retry-After 解析、429 混淆 |
|
||||
| `AzureOpenAIExecutor` | Azure OpenAI | 基于部署的路由、api-version 查询参数强制执行 |
|
||||
| `BlackboxWebExecutor` | Blackbox AI (web-mode) | Web 会话反向 + TLS 指纹模拟 |
|
||||
| `ChatGPTWebExecutor` | ChatGPT web | TLS 客户端 + 会话 Cookie 管理(`chatgptTlsClient.ts`)|
|
||||
| `ClaudeIdentityExecutor` | Claude.ai (CCH 通道) | 约束 + Tool 重映射管线、指纹塑造 |
|
||||
| `CliProxyApiExecutor` | CLIProxyAPI 兼容服务商 | 自定义认证和协议处理 |
|
||||
| `CloudflareAiExecutor` | Cloudflare Workers AI | 帐户 ID 注入、基于 Neurons 的用量追踪 |
|
||||
| `CodexExecutor` | OpenAI Codex | 注入系统指令、强制推理力度 |
|
||||
| `CommandCodeExecutor` | Command Code | OAuth + 每会话 Header 轮换 |
|
||||
| `CursorExecutor` | Cursor IDE | ConnectRPC 协议、Protobuf 编码、基于校验和的请求签名|
|
||||
| `DevinCliExecutor` | Devin CLI | Devin 任务生命周期桥接(通过云代理模块) |
|
||||
| `GithubExecutor` | GitHub Copilot | Copilot Token 刷新、VSCode 模仿 Header |
|
||||
| `GitlabExecutor` | GitLab Duo | GitLab OAuth + 项目级路由 |
|
||||
| `GlmExecutor` | Z.AI GLM(含 `glmt` 预设) | Thinking Budget 感知、GLMT 预设常量 |
|
||||
| `GrokWebExecutor` | xAI Grok web | Web 会话反向、模式选择(think/standard) |
|
||||
| `KieExecutor` | KIE | 自定义 Token 签发 + 轮换会话锚点 |
|
||||
| `KiroExecutor` | AWS CodeWhisperer/Kiro | AWS EventStream 二进制格式 → SSE 转换 |
|
||||
| `MuseSparkWebExecutor` | Muse Spark (web) | Web 会话反向 + 图片消息桥接 |
|
||||
| `NlpCloudExecutor` | NLP Cloud | 服务商专属请求体形状 |
|
||||
| `OpenCodeExecutor` | OpenCode | AI SDK 兼容服务商初始化 |
|
||||
| `PerplexityWebExecutor` | Perplexity web | Web 会话反向,用于聊延续 |
|
||||
| `PetalsExecutor` | Petals distributed inference | 去中心化集群路由 |
|
||||
| `PollinationsExecutor` | Pollinations AI | 无需 API Key、带速率限制的请求 |
|
||||
| `QoderExecutor` | Qoder AI | PAT 和 OAuth 支持、多模型免费层 |
|
||||
| `VertexExecutor` | Google Vertex AI | 服务帐户认证、基于区域的端点 |
|
||||
| `DevinDesktopExecutor` | Devin Desktop | 导入的 API 密钥 + Connect-protobuf 聊天流 |
|
||||
| 执行器 | 服务商 | 特殊处理 |
|
||||
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
|
||||
| `DefaultExecutor` | OpenAI, Claude, Gemini, Qwen, OpenRouter, GLM, Kimi, MiniMax, DeepSeek, Groq, xAI, Mistral, Perplexity, Together, Fireworks, Cerebras, Cohere, NVIDIA 等 | 每服务商动态 URL/Header 配置 |
|
||||
| `AntigravityExecutor` | Google Antigravity | 自定义项目/会话 ID、Retry-After 解析、429 混淆 |
|
||||
| `AzureOpenAIExecutor` | Azure OpenAI | 基于部署的路由、api-version 查询参数强制执行 |
|
||||
| `BlackboxWebExecutor` | Blackbox AI (web-mode) | Web 会话反向 + TLS 指纹模拟 |
|
||||
| `ClaudeIdentityExecutor` | Claude.ai (CCH 通道) | 约束 + Tool 重映射管线、指纹塑造 |
|
||||
| `CliProxyApiExecutor` | CLIProxyAPI 兼容服务商 | 自定义认证和协议处理 |
|
||||
| `CloudflareAiExecutor` | Cloudflare Workers AI | 帐户 ID 注入、基于 Neurons 的用量追踪 |
|
||||
| `CodexExecutor` | OpenAI Codex | 注入系统指令、强制推理力度 |
|
||||
| `ChatGptWebCodexExecutor` | ChatGPT Web (Codex) | 浏览器会话 Responses API 桥接与线程/轮次固定 |
|
||||
| `CommandCodeExecutor` | Command Code | OAuth + 每会话 Header 轮换 |
|
||||
| `CursorExecutor` | Cursor IDE | ConnectRPC 协议、Protobuf 编码、基于校验和的请求签名 |
|
||||
| `DevinCliExecutor` | Devin CLI | Devin 任务生命周期桥接(通过云代理模块) |
|
||||
| `GithubExecutor` | GitHub Copilot | Copilot Token 刷新、VSCode 模仿 Header |
|
||||
| `GitlabExecutor` | GitLab Duo | GitLab OAuth + 项目级路由 |
|
||||
| `GlmExecutor` | Z.AI GLM(含 `glmt` 预设) | Thinking Budget 感知、GLMT 预设常量 |
|
||||
| `GrokWebExecutor` | xAI Grok web | Web 会话反向、模式选择(think/standard) |
|
||||
| `KieExecutor` | KIE | 自定义 Token 签发 + 轮换会话锚点 |
|
||||
| `KiroExecutor` | AWS CodeWhisperer/Kiro | AWS EventStream 二进制格式 → SSE 转换 |
|
||||
| `MuseSparkWebExecutor` | Muse Spark (web) | Web 会话反向 + 图片消息桥接 |
|
||||
| `NlpCloudExecutor` | NLP Cloud | 服务商专属请求体形状 |
|
||||
| `OpenCodeExecutor` | OpenCode | AI SDK 兼容服务商初始化 |
|
||||
| `PerplexityWebExecutor` | Perplexity web | Web 会话反向,用于聊延续 |
|
||||
| `PetalsExecutor` | Petals distributed inference | 去中心化集群路由 |
|
||||
| `PollinationsExecutor` | Pollinations AI | 无需 API Key、带速率限制的请求 |
|
||||
| `QoderExecutor` | Qoder AI | PAT 和 OAuth 支持、多模型免费层 |
|
||||
| `VertexExecutor` | Google Vertex AI | 服务帐户认证、基于区域的端点 |
|
||||
| `DevinDesktopExecutor` | Devin Desktop | 导入的 API 密钥 + Connect-protobuf 聊天流 |
|
||||
|
||||
其余所有服务商(含自定义兼容节点)使用 `DefaultExecutor`。
|
||||
|
||||
## 服务商兼容性矩阵
|
||||
|
||||
> **注意:** 下表是 OmniRoute v3.8.0 中 237 个已注册服务商的代表性样本。
|
||||
> **注意:** 下表是 OmniRoute v3.8.0 中 351 个已注册服务商的代表性样本。
|
||||
> 完整且持续更新的列表请参阅
|
||||
> [`docs/reference/PROVIDER_REFERENCE.md`](../reference/PROVIDER_REFERENCE.md)(自动生成)或数据源头
|
||||
> `src/shared/constants/providers.ts`(加载时通过 Zod 校验)。
|
||||
|
||||
| 服务商 | 格式 | 认证 | 流式 | 非流式 | Token 刷新 | 用量 API |
|
||||
| ----------------- | ---------------- | --------------------- | -------------- | ---------- | ----------- | ----------------- |
|
||||
| Claude | claude | API Key / OAuth | ✅ | ✅ | ✅ | ⚠️ 仅管理员 |
|
||||
| Gemini | gemini | API Key / OAuth | ✅ | ✅ | ✅ | ⚠️ Cloud Console |
|
||||
| Antigravity | antigravity | OAuth | ✅ | ✅ | ✅ | ✅ 完整配额 API |
|
||||
| OpenAI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Codex | openai-responses | OAuth | ✅ 强制 | ❌ | ✅ | ✅ 速率限制 |
|
||||
| GitHub Copilot | openai | OAuth + Copilot Token | ✅ | ✅ | ✅ | ✅ 配额快照 |
|
||||
| Cursor | cursor | 自定义校验和 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Kiro | kiro | AWS SSO OIDC | ✅ (EventStream)| ❌ | ✅ | ✅ 用量限制 |
|
||||
| Qwen | openai | OAuth | ✅ | ✅ | ✅ | ⚠️ 每请求 |
|
||||
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ 每请求 |
|
||||
| Kilo Code | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| Cline | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| Kimi Coding | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| OpenRouter | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| GLM/Kimi/MiniMax | claude | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| DeepSeek | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Groq | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| xAI (Grok) | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Mistral | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Perplexity | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Together AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Fireworks AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cerebras | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cohere | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| NVIDIA NIM | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cloudflare AI | openai | API Token + 帐户 ID | ✅ | ✅ | ❌ | ❌ |
|
||||
| Pollinations | openai | 无需密钥 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Scaleway AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| LongCat | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Ollama Cloud | openai | API Key(可选) | ✅ | ✅ | ❌ | ❌ |
|
||||
| HuggingFace | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Nebius | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| SiliconFlow | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Hyperbolic | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Vertex AI | gemini | 服务帐户 | ✅ | ✅ | ✅ | ⚠️ Cloud Console |
|
||||
| Command Code | openai | OAuth | ✅ | ✅ | ✅ | ⚠️ 每请求 |
|
||||
| Z.AI / GLM | openai | API Key / OAuth | ✅ | ✅ | ❌ | ❌ |
|
||||
| GLMT (preset) | claude | API Key | ✅ | ✅ | ❌ | ⚠️ 每请求 |
|
||||
| Kimi Coding | openai | OAuth / API Key | ✅ | ✅ | ✅ | ❌ |
|
||||
| KIE | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Devin Desktop | openai | 导入的 API 密钥 | ✅ (Connect→SSE) | ✅ | ❌ | ⚠️ 每请求 |
|
||||
| GitLab Duo | openai | OAuth (GitLab) | ✅ | ✅ | ✅ | ❌ |
|
||||
| Devin CLI | openai | 本地 CLI 登录 | ✅ | ✅ | ❌ | ✅ 任务 API |
|
||||
| Codex Cloud | openai-responses | OAuth | ✅ | ❌ | ✅ | ✅ 速率限制 |
|
||||
| Jules | openai | OAuth | ✅ | ✅ | ✅ | ✅ 任务 API |
|
||||
| AgentRouter | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| ChatGPT-Web | openai | 会话 Cookie + TLS | ✅ | ✅ | ❌ | ❌ |
|
||||
| Grok-Web | openai | 会话 Cookie | ✅ | ✅ | ❌ | ❌ |
|
||||
| Perplexity-Web | openai | 会话 Cookie | ✅ | ✅ | ❌ | ❌ |
|
||||
| BlackBox-Web | openai | 会话 Cookie + TLS | ✅ | ✅ | ❌ | ❌ |
|
||||
| Muse-Spark-Web | openai | 会话 Cookie | ✅ | ✅ | ❌ | ❌ |
|
||||
| ModelScope | openai | API Key | ✅ | ✅ | ❌ | ⚠️ 配额策略 |
|
||||
| BazaarLink | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Petals | openai | 无需密钥 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ 每请求 |
|
||||
| OpenCode (Go/Zen) | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| CLIProxyAPI | openai | 自定义 | ✅ | ✅ | ❌ | ❌ |
|
||||
| 服务商 | 格式 | 认证 | 流式 | 非流式 | Token 刷新 | 用量 API |
|
||||
| ------------------- | ---------------- | --------------------- | ---------------- | ------ | ---------- | ---------------- |
|
||||
| Claude | claude | API Key / OAuth | ✅ | ✅ | ✅ | ⚠️ 仅管理员 |
|
||||
| Gemini | gemini | API Key / OAuth | ✅ | ✅ | ✅ | ⚠️ Cloud Console |
|
||||
| Antigravity | antigravity | OAuth | ✅ | ✅ | ✅ | ✅ 完整配额 API |
|
||||
| OpenAI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Codex | openai-responses | OAuth | ✅ 强制 | ❌ | ✅ | ✅ 速率限制 |
|
||||
| ChatGPT Web (Codex) | openai-responses | 浏览器会话 | ✅ 强制 | ❌ | ❌ | ❌ |
|
||||
| GitHub Copilot | openai | OAuth + Copilot Token | ✅ | ✅ | ✅ | ✅ 配额快照 |
|
||||
| Cursor | cursor | 自定义校验和 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Kiro | kiro | AWS SSO OIDC | ✅ (EventStream) | ❌ | ✅ | ✅ 用量限制 |
|
||||
| Qwen | openai | OAuth | ✅ | ✅ | ✅ | ⚠️ 每请求 |
|
||||
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ 每请求 |
|
||||
| Kilo Code | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| Cline | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| Kimi Coding | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| OpenRouter | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| GLM/Kimi/MiniMax | claude | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| DeepSeek | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Groq | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| xAI (Grok) | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Mistral | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Perplexity | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Together AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Fireworks AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cerebras | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cohere | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| NVIDIA NIM | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cloudflare AI | openai | API Token + 帐户 ID | ✅ | ✅ | ❌ | ❌ |
|
||||
| Pollinations | openai | 无需密钥 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Scaleway AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| LongCat | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Ollama Cloud | openai | API Key(可选) | ✅ | ✅ | ❌ | ❌ |
|
||||
| HuggingFace | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Nebius | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| SiliconFlow | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Hyperbolic | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Vertex AI | gemini | 服务帐户 | ✅ | ✅ | ✅ | ⚠️ Cloud Console |
|
||||
| Command Code | openai | OAuth | ✅ | ✅ | ✅ | ⚠️ 每请求 |
|
||||
| Z.AI / GLM | openai | API Key / OAuth | ✅ | ✅ | ❌ | ❌ |
|
||||
| GLMT (preset) | claude | API Key | ✅ | ✅ | ❌ | ⚠️ 每请求 |
|
||||
| Kimi Coding | openai | OAuth / API Key | ✅ | ✅ | ✅ | ❌ |
|
||||
| KIE | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Devin Desktop | openai | 导入的 API 密钥 | ✅ (Connect→SSE) | ✅ | ❌ | ⚠️ 每请求 |
|
||||
| GitLab Duo | openai | OAuth (GitLab) | ✅ | ✅ | ✅ | ❌ |
|
||||
| Devin CLI | openai | 本地 CLI 登录 | ✅ | ✅ | ❌ | ✅ 任务 API |
|
||||
| Codex Cloud | openai-responses | OAuth | ✅ | ❌ | ✅ | ✅ 速率限制 |
|
||||
| Jules | openai | OAuth | ✅ | ✅ | ✅ | ✅ 任务 API |
|
||||
| AgentRouter | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Grok-Web | openai | 会话 Cookie | ✅ | ✅ | ❌ | ❌ |
|
||||
| Perplexity-Web | openai | 会话 Cookie | ✅ | ✅ | ❌ | ❌ |
|
||||
| BlackBox-Web | openai | 会话 Cookie + TLS | ✅ | ✅ | ❌ | ❌ |
|
||||
| Muse-Spark-Web | openai | 会话 Cookie | ✅ | ✅ | ❌ | ❌ |
|
||||
| ModelScope | openai | API Key | ✅ | ✅ | ❌ | ⚠️ 配额策略 |
|
||||
| BazaarLink | openai | API Key | ✅ | ✅ | ❌ | ❌ |
|
||||
| Petals | openai | 无需密钥 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ 每请求 |
|
||||
| OpenCode (Go/Zen) | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| CLIProxyAPI | openai | 自定义 | ✅ | ✅ | ❌ | ❌ |
|
||||
|
||||
## 格式转换覆盖
|
||||
|
||||
@@ -1029,25 +1026,25 @@ flowchart LR
|
||||
|
||||
## 支持的 API 端点
|
||||
|
||||
| 端点 | 格式 | 处理器 |
|
||||
| --------------------------------------------------- | ------------------ | ----------------------------------------------------------------- |
|
||||
| `POST /v1/chat/completions` | OpenAI Chat | `src/sse/handlers/chat.ts` |
|
||||
| `POST /v1/messages` | Claude Messages | 同一处理器(自动检测) |
|
||||
| `POST /v1/responses` | OpenAI Responses | `open-sse/handlers/responsesHandler.ts` |
|
||||
| `POST /v1/embeddings` | OpenAI Embeddings | `open-sse/handlers/embeddings.ts` |
|
||||
| `GET /v1/embeddings` | 模型列表 | API 路由 |
|
||||
| `POST /v1/images/generations` | OpenAI Images | `open-sse/handlers/imageGeneration.ts` |
|
||||
| `GET /v1/images/generations` | 模型列表 | API 路由 |
|
||||
| `POST /v1/providers/{provider}/chat/completions` | OpenAI Chat | 带模型校验的专用每服务商路由 |
|
||||
| `POST /v1/providers/{provider}/embeddings` | OpenAI Embeddings | 带模型校验的专用每服务商路由 |
|
||||
| `POST /v1/providers/{provider}/images/generations` | OpenAI Images | 带模型校验的专用每服务商路由 |
|
||||
| `POST /v1/messages/count_tokens` | Claude Token Count | API 路由 |
|
||||
| `GET /v1/models` | OpenAI Models 列表 | API 路由(聊 + 向量嵌入 + 图片 + 自定义模型) |
|
||||
| `GET /api/models/catalog` | 目录 | 按服务商 + 类型分组的所有模型 |
|
||||
| `POST /v1beta/models/*:streamGenerateContent` | Gemini native | API 路由 |
|
||||
| `GET/PUT/DELETE /api/settings/proxy` | 代理配置 | 网络代理配置 |
|
||||
| `POST /api/settings/proxy/test` | 代理连通性 | 代理健康/连通性测试端点 |
|
||||
| `GET/POST/DELETE /api/provider-models` | 服务商模型 | 服务商模型元数据,支撑自定义和管理可用模型 |
|
||||
| 端点 | 格式 | 处理器 |
|
||||
| -------------------------------------------------- | ------------------ | --------------------------------------------- |
|
||||
| `POST /v1/chat/completions` | OpenAI Chat | `src/sse/handlers/chat.ts` |
|
||||
| `POST /v1/messages` | Claude Messages | 同一处理器(自动检测) |
|
||||
| `POST /v1/responses` | OpenAI Responses | `open-sse/handlers/responsesHandler.ts` |
|
||||
| `POST /v1/embeddings` | OpenAI Embeddings | `open-sse/handlers/embeddings.ts` |
|
||||
| `GET /v1/embeddings` | 模型列表 | API 路由 |
|
||||
| `POST /v1/images/generations` | OpenAI Images | `open-sse/handlers/imageGeneration.ts` |
|
||||
| `GET /v1/images/generations` | 模型列表 | API 路由 |
|
||||
| `POST /v1/providers/{provider}/chat/completions` | OpenAI Chat | 带模型校验的专用每服务商路由 |
|
||||
| `POST /v1/providers/{provider}/embeddings` | OpenAI Embeddings | 带模型校验的专用每服务商路由 |
|
||||
| `POST /v1/providers/{provider}/images/generations` | OpenAI Images | 带模型校验的专用每服务商路由 |
|
||||
| `POST /v1/messages/count_tokens` | Claude Token Count | API 路由 |
|
||||
| `GET /v1/models` | OpenAI Models 列表 | API 路由(聊 + 向量嵌入 + 图片 + 自定义模型) |
|
||||
| `GET /api/models/catalog` | 目录 | 按服务商 + 类型分组的所有模型 |
|
||||
| `POST /v1beta/models/*:streamGenerateContent` | Gemini native | API 路由 |
|
||||
| `GET/PUT/DELETE /api/settings/proxy` | 代理配置 | 网络代理配置 |
|
||||
| `POST /api/settings/proxy/test` | 代理连通性 | 代理健康/连通性测试端点 |
|
||||
| `GET/POST/DELETE /api/provider-models` | 服务商模型 | 服务商模型元数据,支撑自定义和管理可用模型 |
|
||||
|
||||
## 旁路处理器
|
||||
|
||||
|
||||
@@ -10,18 +10,18 @@
|
||||
|
||||
## 1. 技术栈
|
||||
|
||||
| 关注领域 | 技术选型 |
|
||||
| ---------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| Web 框架 | **Next.js 16**(App Router,独立输出,无全局中间件) |
|
||||
| 语言 | **TypeScript 6.0+** — 目标 `ES2022`,`module: esnext`,`moduleResolution: bundler`,`strict: false` |
|
||||
| 运行时 | **Node.js** `>=22.22.2 <23` 或 `>=24.0.0 <27`(通过 `engines` + `SUPPORTED_NODE_RANGE` 强制) |
|
||||
| 数据库 | **SQLite**,基于 `better-sqlite3`(单例,WAL 日志模式) |
|
||||
| 桌面端 | **Electron 41** + `electron-builder` 26.10(独立工作空间 `electron/`) |
|
||||
| 测试 | **Node 原生测试运行器**(单元/集成)、**Vitest**(MCP、autoCombo、缓存)、**Playwright**(端到端 + 协议端到端) |
|
||||
| 构建 | Next.js 独立模式,通过 `scripts/build/build-next-isolated.mjs` |
|
||||
| 代码检查 | ESLint flat 配置 + Prettier(Husky pre-commit 触发 `lint-staged`) |
|
||||
| 模块系统 | 全局 ESM(`"type": "module"`) |
|
||||
| 工作空间 | npm workspace — `open-sse` 是唯一的子工作空间 |
|
||||
| 关注领域 | 技术选型 |
|
||||
| -------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| Web 框架 | **Next.js 16**(App Router,独立输出,无全局中间件) |
|
||||
| 语言 | **TypeScript 6.0+** — 目标 `ES2022`,`module: esnext`,`moduleResolution: bundler`,`strict: false` |
|
||||
| 运行时 | **Node.js** `>=22.22.2 <23` 或 `>=24.0.0 <27`(通过 `engines` + `SUPPORTED_NODE_RANGE` 强制) |
|
||||
| 数据库 | **SQLite**,基于 `better-sqlite3`(单例,WAL 日志模式) |
|
||||
| 桌面端 | **Electron 41** + `electron-builder` 26.10(独立工作空间 `electron/`) |
|
||||
| 测试 | **Node 原生测试运行器**(单元/集成)、**Vitest**(MCP、autoCombo、缓存)、**Playwright**(端到端 + 协议端到端) |
|
||||
| 构建 | Next.js 独立模式,通过 `scripts/build/build-next-isolated.mjs` |
|
||||
| 代码检查 | ESLint flat 配置 + Prettier(Husky pre-commit 触发 `lint-staged`) |
|
||||
| 模块系统 | 全局 ESM(`"type": "module"`) |
|
||||
| 工作空间 | npm workspace — `open-sse` 是唯一的子工作空间 |
|
||||
|
||||
路径别名(`tsconfig.json`):
|
||||
|
||||
@@ -85,20 +85,20 @@ App Router 同时暴露仪表盘 UI 和公开/管理 HTTP API。**不存在全
|
||||
|
||||
`src/app/` 下的顶层段:
|
||||
|
||||
| 路径 | 用途 |
|
||||
| ------------------------------------------------------------------------------ | --------------------------------- |
|
||||
| `api/` | 所有 HTTP API 路由(详见下文分解)|
|
||||
| `a2a/` | A2A JSON-RPC 2.0 端点(`POST /a2a`)|
|
||||
| `.well-known/agent.json/` | A2A Agent Card 发现文档 |
|
||||
| `(dashboard)/` | 仪表盘 UI(路由组,无 URL 前缀) |
|
||||
| `auth/`、`login/`、`forgot-password/`、`callback/` | 认证流程 |
|
||||
| `landing/` | 市场/落地页 |
|
||||
| `docs/` | 嵌入式 API 文档查看器 |
|
||||
| `status/`、`maintenance/`、`offline/` | 运维页面 |
|
||||
| `privacy/`、`terms/` | 法律页面 |
|
||||
| `400/`、`401/`、`403/`、`408/`、`429/`、`500/`、`502/`、`503/` | 静态错误页面 |
|
||||
| `error.tsx`、`global-error.tsx`、`not-found.tsx`、`forbidden/`、`loading.tsx` | 框架错误/加载边界 |
|
||||
| `layout.tsx`、`page.tsx`、`globals.css`、`manifest.ts` | 根外壳 |
|
||||
| 路径 | 用途 |
|
||||
| ----------------------------------------------------------------------------- | ------------------------------------ |
|
||||
| `api/` | 所有 HTTP API 路由(详见下文分解) |
|
||||
| `a2a/` | A2A JSON-RPC 2.0 端点(`POST /a2a`) |
|
||||
| `.well-known/agent.json/` | A2A Agent Card 发现文档 |
|
||||
| `(dashboard)/` | 仪表盘 UI(路由组,无 URL 前缀) |
|
||||
| `auth/`、`login/`、`forgot-password/`、`callback/` | 认证流程 |
|
||||
| `landing/` | 市场/落地页 |
|
||||
| `docs/` | 嵌入式 API 文档查看器 |
|
||||
| `status/`、`maintenance/`、`offline/` | 运维页面 |
|
||||
| `privacy/`、`terms/` | 法律页面 |
|
||||
| `400/`、`401/`、`403/`、`408/`、`429/`、`500/`、`502/`、`503/` | 静态错误页面 |
|
||||
| `error.tsx`、`global-error.tsx`、`not-found.tsx`、`forbidden/`、`loading.tsx` | 框架错误/加载边界 |
|
||||
| `layout.tsx`、`page.tsx`、`globals.css`、`manifest.ts` | 根外壳 |
|
||||
|
||||
#### 3.1.1 `src/app/(dashboard)/dashboard/` — UI 页面
|
||||
|
||||
@@ -218,7 +218,6 @@ v1/
|
||||
├── audio/{speech, transcriptions}/ TTS + STT
|
||||
├── batches/[id]/{cancel}, batches/ OpenAI Batches API
|
||||
├── chat/completions/ 对话补全(主要端点)
|
||||
├── chatgpt-web/ ChatGPT-Web 兼容
|
||||
├── completions/ 旧版文本补全
|
||||
├── embeddings/ 嵌入
|
||||
├── files/[id]/, files/ Files API
|
||||
@@ -254,46 +253,46 @@ v1/
|
||||
|
||||
始终通过这些模块导入数据、同步、OAuth、技能、记忆等。下表按实际目录和重要顶层文件分组。
|
||||
|
||||
| 模块 | 用途 |
|
||||
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `a2a/` | A2A 协议服务器:`taskManager.ts`、`streaming.ts`、`taskExecution.ts`、`routingLogger.ts`、`skills/`(6 个技能:成本分析、健康报告、服务商发现、配额管理、智能路由、列出能力) |
|
||||
| `acp/` | Agent-Control-Protocol:`index.ts`、`manager.ts`、`registry.ts` |
|
||||
| `api/` | 内部 API 辅助工具:`requireManagementAuth.ts`、`requireCliToolsAuth.ts`、`errorResponse.ts` |
|
||||
| `auth/` | `managementPassword.ts`(密码重置/哈希) |
|
||||
| `batches/` | OpenAI Batches API 服务(`service.ts`) |
|
||||
| `catalog/` | OpenRouter 目录同步(`openrouterCatalog.ts`) |
|
||||
| `cloudAgent/` | 云代理注册表:`api.ts`、`baseAgent.ts`、`db.ts`、`index.ts`、`registry.ts`、`types.ts`、`agents/{codex, devin, jules}.ts` |
|
||||
| `combos/` | Combo 解析辅助工具 |
|
||||
| `compliance/` | 审计 + 服务商审计:`index.ts`、`providerAudit.ts` |
|
||||
| `config/` | 运行时配置粘合层 |
|
||||
| `db/` | SQLite 领域模块(见 §3.2.1) |
|
||||
| `display/` | API 响应使用的 UI/展示辅助工具 |
|
||||
| `embeddings/` | 嵌入服务注册表 |
|
||||
| `env/` | 环境变量加载 + 自检 |
|
||||
| `evals/` | 评估框架运行时 |
|
||||
| `guardrails/` | `piiMasker.ts`、`promptInjection.ts`、`visionBridge.ts`、`visionBridgeHelpers.ts`、`registry.ts`、`base.ts` |
|
||||
| `jobs/` | 后台任务(`autoUpdate.ts` 等) |
|
||||
| `memory/` | 持久化记忆:`store.ts`、`cache.ts`、`retrieval.ts`、`summarization.ts`、`extraction.ts`、`injection.ts`、`qdrant.ts`、`settings.ts`、`verify.ts`、`schemas.ts`、`types.ts` |
|
||||
| `monitoring/` | `observability.ts` |
|
||||
| `oauth/` | OAuth 服务商(14 个):`antigravity`、`claude`、`cline`、`codex`、`cursor`、`gemini`、`github`、`gitlab-duo`、`kilocode`、`kimi-coding`、`kiro`、`qoder`、`qwen`、`windsurf`,以及 `services/`、`utils/{pkce, server, banner, codexAuthFile, ui}`、`constants/oauth.ts` |
|
||||
| `plugins/` | 插件加载器(`index.ts`) |
|
||||
| `promptCache/` | `prefixAnalyzer.ts`、`index.ts` |
|
||||
| `providerModels/` | 托管模型生命周期:`modelDiscovery.ts`、`managedModelImport.ts`、`managedAvailableModels.ts`、`cursorAgent.ts` |
|
||||
| `providers/` | 服务商辅助工具:`catalog.ts`、`validation.ts`、`imageValidation.ts`、`claudeExtraUsage.ts`、`codexConnectionDefaults.ts`、`codexFastTier.ts`、`webCookieAuth.ts`、`managedAvailableModels.ts`、`requestDefaults.ts` |
|
||||
| `resilience/` | `settings.ts` — 熔断器、冷却、锁定的设置 |
|
||||
| `runtime/` | 运行时特性检测 |
|
||||
| `search/` | `executeWebSearch.ts` |
|
||||
| `services/` | 嵌入式服务框架:`ServiceSupervisor.ts`(通用子进程管理器,带操作锁、环形缓冲区、健康检查器)、`bootstrap.ts`(进程级注册和自动启动)、`registry.ts`(工具 → 管理器映射)、`apiKey.ts`(AES-256-GCM Key 存储)、`modelSync.ts`(定期模型同步)、`ringBuffer.ts`(5 MB 环形日志缓冲)、`healthCheck.ts`(HTTP 健康探测)、`types.ts`、`embedWsProxy.ts`(WebSocket 代理)、`installers/{ninerouter,cliproxy}.ts`。详见 `docs/frameworks/EMBEDDED-SERVICES.md` |
|
||||
| `agentSkills/` | Agent Skills 目录 + 生成器:`catalog.ts`(getCatalog/getSkillById/filterCatalog/computeCoverage)、`generator.ts`(generateAgentSkills → 写入 `skills/{id}/SKILL.md`)、`openapiParser.ts`(从 OpenAPI 规范提取 REST 端点)、`cliRegistryParser.ts`(从 bin/cli-registry 提取 CLI 子命令)、`schemas.ts`(Zod:AgentSkillSchema、SkillCoverageSchema、ListQuerySchema、GenerateBodySchema)、`types.ts`(AgentSkill、SkillCoverage、SkillMarkdown、GeneratorReport)。被 REST 路由(`/api/agent-skills/*`)、MCP 工具(`omniroute_agent_skills_*`)和 A2A 技能 `list-capabilities` 消费。参见 [AGENT-SKILLS.md](../frameworks/AGENT-SKILLS.md)。 |
|
||||
| `skills/` | 技能框架:`registry.ts`、`executor.ts`、`interception.ts`、`injection.ts`、`sandbox.ts`、`custom.ts`、`hybrid.ts`、`builtins.ts`、`a2a.ts`、`providerSettings.ts`、`schemas.ts`、`skillssh.ts`、`types.ts`,以及 `builtin/browser.ts` |
|
||||
| `spend/` | `batchWriter.ts`(写后缓冲) |
|
||||
| `sync/` | `bundle.ts`、`tokens.ts`(云同步) |
|
||||
| `system/` | 系统级辅助工具 |
|
||||
| `translator/` | 顶层翻译器粘合层(委托到 `open-sse/translator/`) |
|
||||
| `usage/` | 用量核算:`costCalculator.ts`、`tokenAccounting.ts`、`usageHistory.ts`、`aggregateHistory.ts`、`usageStats.ts`、`callLogs.ts`、`callLogArtifacts.ts`、`fetcher.ts`、`providerLimits.ts`、`migrations.ts` |
|
||||
| `versionManager/` | 自动更新 + 版本清单 |
|
||||
| `ws/` | WebSocket 桥接 |
|
||||
| `zed-oauth/` | Zed 编辑器 OAuth 流程 |
|
||||
| 模块 | 用途 |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `a2a/` | A2A 协议服务器:`taskManager.ts`、`streaming.ts`、`taskExecution.ts`、`routingLogger.ts`、`skills/`(6 个技能:成本分析、健康报告、服务商发现、配额管理、智能路由、列出能力) |
|
||||
| `acp/` | Agent-Control-Protocol:`index.ts`、`manager.ts`、`registry.ts` |
|
||||
| `api/` | 内部 API 辅助工具:`requireManagementAuth.ts`、`requireCliToolsAuth.ts`、`errorResponse.ts` |
|
||||
| `auth/` | `managementPassword.ts`(密码重置/哈希) |
|
||||
| `batches/` | OpenAI Batches API 服务(`service.ts`) |
|
||||
| `catalog/` | OpenRouter 目录同步(`openrouterCatalog.ts`) |
|
||||
| `cloudAgent/` | 云代理注册表:`api.ts`、`baseAgent.ts`、`db.ts`、`index.ts`、`registry.ts`、`types.ts`、`agents/{codex, devin, jules}.ts` |
|
||||
| `combos/` | Combo 解析辅助工具 |
|
||||
| `compliance/` | 审计 + 服务商审计:`index.ts`、`providerAudit.ts` |
|
||||
| `config/` | 运行时配置粘合层 |
|
||||
| `db/` | SQLite 领域模块(见 §3.2.1) |
|
||||
| `display/` | API 响应使用的 UI/展示辅助工具 |
|
||||
| `embeddings/` | 嵌入服务注册表 |
|
||||
| `env/` | 环境变量加载 + 自检 |
|
||||
| `evals/` | 评估框架运行时 |
|
||||
| `guardrails/` | `piiMasker.ts`、`promptInjection.ts`、`visionBridge.ts`、`visionBridgeHelpers.ts`、`registry.ts`、`base.ts` |
|
||||
| `jobs/` | 后台任务(`autoUpdate.ts` 等) |
|
||||
| `memory/` | 持久化记忆:`store.ts`、`cache.ts`、`retrieval.ts`、`summarization.ts`、`extraction.ts`、`injection.ts`、`qdrant.ts`、`settings.ts`、`verify.ts`、`schemas.ts`、`types.ts` |
|
||||
| `monitoring/` | `observability.ts` |
|
||||
| `oauth/` | OAuth 服务商(14 个):`antigravity`、`claude`、`cline`、`codex`、`cursor`、`gemini`、`github`、`gitlab-duo`、`kilocode`、`kimi-coding`、`kiro`、`qoder`、`qwen`、`windsurf`,以及 `services/`、`utils/{pkce, server, banner, codexAuthFile, ui}`、`constants/oauth.ts` |
|
||||
| `plugins/` | 插件加载器(`index.ts`) |
|
||||
| `promptCache/` | `prefixAnalyzer.ts`、`index.ts` |
|
||||
| `providerModels/` | 托管模型生命周期:`modelDiscovery.ts`、`managedModelImport.ts`、`managedAvailableModels.ts`、`cursorAgent.ts` |
|
||||
| `providers/` | 服务商辅助工具:`catalog.ts`、`validation.ts`、`imageValidation.ts`、`claudeExtraUsage.ts`、`codexConnectionDefaults.ts`、`codexFastTier.ts`、`webCookieAuth.ts`、`managedAvailableModels.ts`、`requestDefaults.ts` |
|
||||
| `resilience/` | `settings.ts` — 熔断器、冷却、锁定的设置 |
|
||||
| `runtime/` | 运行时特性检测 |
|
||||
| `search/` | `executeWebSearch.ts` |
|
||||
| `services/` | 嵌入式服务框架:`ServiceSupervisor.ts`(通用子进程管理器,带操作锁、环形缓冲区、健康检查器)、`bootstrap.ts`(进程级注册和自动启动)、`registry.ts`(工具 → 管理器映射)、`apiKey.ts`(AES-256-GCM Key 存储)、`modelSync.ts`(定期模型同步)、`ringBuffer.ts`(5 MB 环形日志缓冲)、`healthCheck.ts`(HTTP 健康探测)、`types.ts`、`embedWsProxy.ts`(WebSocket 代理)、`installers/{ninerouter,cliproxy}.ts`。详见 `docs/frameworks/EMBEDDED-SERVICES.md` |
|
||||
| `agentSkills/` | Agent Skills 目录 + 生成器:`catalog.ts`(getCatalog/getSkillById/filterCatalog/computeCoverage)、`generator.ts`(generateAgentSkills → 写入 `skills/{id}/SKILL.md`)、`openapiParser.ts`(从 OpenAPI 规范提取 REST 端点)、`cliRegistryParser.ts`(从 bin/cli-registry 提取 CLI 子命令)、`schemas.ts`(Zod:AgentSkillSchema、SkillCoverageSchema、ListQuerySchema、GenerateBodySchema)、`types.ts`(AgentSkill、SkillCoverage、SkillMarkdown、GeneratorReport)。被 REST 路由(`/api/agent-skills/*`)、MCP 工具(`omniroute_agent_skills_*`)和 A2A 技能 `list-capabilities` 消费。参见 [AGENT-SKILLS.md](../frameworks/AGENT-SKILLS.md)。 |
|
||||
| `skills/` | 技能框架:`registry.ts`、`executor.ts`、`interception.ts`、`injection.ts`、`sandbox.ts`、`custom.ts`、`hybrid.ts`、`builtins.ts`、`a2a.ts`、`providerSettings.ts`、`schemas.ts`、`skillssh.ts`、`types.ts`,以及 `builtin/browser.ts` |
|
||||
| `spend/` | `batchWriter.ts`(写后缓冲) |
|
||||
| `sync/` | `bundle.ts`、`tokens.ts`(云同步) |
|
||||
| `system/` | 系统级辅助工具 |
|
||||
| `translator/` | 顶层翻译器粘合层(委托到 `open-sse/translator/`) |
|
||||
| `usage/` | 用量核算:`costCalculator.ts`、`tokenAccounting.ts`、`usageHistory.ts`、`aggregateHistory.ts`、`usageStats.ts`、`callLogs.ts`、`callLogArtifacts.ts`、`fetcher.ts`、`providerLimits.ts`、`migrations.ts` |
|
||||
| `versionManager/` | 自动更新 + 版本清单 |
|
||||
| `ws/` | WebSocket 桥接 |
|
||||
| `zed-oauth/` | Zed 编辑器 OAuth 流程 |
|
||||
|
||||
`src/lib/` 中的顶层文件:
|
||||
|
||||
@@ -357,23 +356,23 @@ v1/
|
||||
|
||||
纯业务逻辑,无 I/O。被路由和处理器导入。
|
||||
|
||||
| 文件 | 用途 |
|
||||
| -------------------------------------------- | --------------------------------- |
|
||||
| `policyEngine.ts` | 顶层策略解析器 |
|
||||
| `fallbackPolicy.ts` | 容灾决策树 |
|
||||
| `costRules.ts` | 成本计算规则 |
|
||||
| `lockoutPolicy.ts` | 模型锁定决策 |
|
||||
| `tagRouter.ts` | 基于标签的路由 |
|
||||
| `comboResolver.ts` | Combo 解析(请求 → 目标列表) |
|
||||
| `connectionModelRules.ts` | 按连接过滤模型 |
|
||||
| `modelAvailability.ts` | 模型可用性检查 |
|
||||
| `degradation.ts` | 降级模式切换 |
|
||||
| `providerExpiration.ts` | 过期账户/Key 检测 |
|
||||
| `quotaCache.ts` | 缓存配额决策 |
|
||||
| `responses.ts`、`omnirouteResponseMeta.ts` | 响应格式辅助工具 |
|
||||
| `configAudit.ts` | 配置变更审计 |
|
||||
| `assessment/` | 模型评估(按 RFC,部分实现) |
|
||||
| `types.ts` | 共享领域类型 |
|
||||
| 文件 | 用途 |
|
||||
| ------------------------------------------ | ----------------------------- |
|
||||
| `policyEngine.ts` | 顶层策略解析器 |
|
||||
| `fallbackPolicy.ts` | 容灾决策树 |
|
||||
| `costRules.ts` | 成本计算规则 |
|
||||
| `lockoutPolicy.ts` | 模型锁定决策 |
|
||||
| `tagRouter.ts` | 基于标签的路由 |
|
||||
| `comboResolver.ts` | Combo 解析(请求 → 目标列表) |
|
||||
| `connectionModelRules.ts` | 按连接过滤模型 |
|
||||
| `modelAvailability.ts` | 模型可用性检查 |
|
||||
| `degradation.ts` | 降级模式切换 |
|
||||
| `providerExpiration.ts` | 过期账户/Key 检测 |
|
||||
| `quotaCache.ts` | 缓存配额决策 |
|
||||
| `responses.ts`、`omnirouteResponseMeta.ts` | 响应格式辅助工具 |
|
||||
| `configAudit.ts` | 配置变更审计 |
|
||||
| `assessment/` | 模型评估(按 RFC,部分实现) |
|
||||
| `types.ts` | 共享领域类型 |
|
||||
|
||||
### 3.4 `src/server/` — 服务端专用
|
||||
|
||||
@@ -442,35 +441,35 @@ open-sse/
|
||||
|
||||
### 4.1 `open-sse/handlers/`
|
||||
|
||||
| 处理器 | 用途 |
|
||||
| ------------------------- | ------------------------------------------ |
|
||||
| `chatCore.ts` | 主对话管道(缓存、速率限制、Combo 路由、执行器调度)|
|
||||
| `responsesHandler.ts` | OpenAI Responses API 入口 |
|
||||
| `embeddings.ts` | 嵌入 |
|
||||
| `imageGeneration.ts` | 图像生成 |
|
||||
| `audioSpeech.ts` | Text-to-speech |
|
||||
| `audioTranscription.ts` | Speech-to-text |
|
||||
| `videoGeneration.ts` | 视频生成 |
|
||||
| `musicGeneration.ts` | 音乐生成 |
|
||||
| `rerank.ts` | 重排序 |
|
||||
| `moderations.ts` | 内容审核 |
|
||||
| `search.ts` | 网页搜索 |
|
||||
| `sseParser.ts` | SSE 事件解析器 |
|
||||
| `usageExtractor.ts` | 从上游流中提取 Token 计数 |
|
||||
| `responseSanitizer.ts` | 去除服务商专用噪音 |
|
||||
| `responseTranslator.ts` | 服务商响应与翻译器层之间的粘合 |
|
||||
| 处理器 | 用途 |
|
||||
| ----------------------- | ---------------------------------------------------- |
|
||||
| `chatCore.ts` | 主对话管道(缓存、速率限制、Combo 路由、执行器调度) |
|
||||
| `responsesHandler.ts` | OpenAI Responses API 入口 |
|
||||
| `embeddings.ts` | 嵌入 |
|
||||
| `imageGeneration.ts` | 图像生成 |
|
||||
| `audioSpeech.ts` | Text-to-speech |
|
||||
| `audioTranscription.ts` | Speech-to-text |
|
||||
| `videoGeneration.ts` | 视频生成 |
|
||||
| `musicGeneration.ts` | 音乐生成 |
|
||||
| `rerank.ts` | 重排序 |
|
||||
| `moderations.ts` | 内容审核 |
|
||||
| `search.ts` | 网页搜索 |
|
||||
| `sseParser.ts` | SSE 事件解析器 |
|
||||
| `usageExtractor.ts` | 从上游流中提取 Token 计数 |
|
||||
| `responseSanitizer.ts` | 去除服务商专用噪音 |
|
||||
| `responseTranslator.ts` | 服务商响应与翻译器层之间的粘合 |
|
||||
|
||||
### 4.2 `open-sse/executors/`
|
||||
|
||||
68 个服务商执行器,各自扩展 `BaseExecutor`(`base.ts`):
|
||||
107 个服务商执行器,各自扩展 `BaseExecutor`(`base.ts`):
|
||||
|
||||
`antigravity`、`azure-openai`、`blackbox-web`、`chatgpt-web`、`cliproxyapi`、
|
||||
`cloudflare-ai`、`codex`、`commandCode`、`cursor`、`default`、`devin-cli`、
|
||||
`antigravity`、`azure-openai`、`blackbox-web`、`cliproxyapi`、
|
||||
`chatgpt-web-codex`、`cloudflare-ai`、`codex`、`commandCode`、`cursor`、`default`、`devin-cli`、
|
||||
`muse-spark-web`、`nlpcloud`、`opencode`、`perplexity-web`、`petals`、
|
||||
`pollinations`、`qoder`、`vertex`、`windsurf`,以及 `claudeIdentity.ts`
|
||||
(共享身份标识辅助)和 `index.ts`(注册表)。
|
||||
|
||||
> 注意:未在此列出的服务商由 `default.ts` 通过通用 OpenAI 兼容执行器提供服务。完整的服务商目录(237 条目)位于 `src/shared/constants/providers.ts`。
|
||||
> 注意:未在此列出的服务商由 `default.ts` 通过通用 OpenAI 兼容执行器提供服务。完整的服务商目录(351 条目)位于 `src/shared/constants/providers.ts`。
|
||||
|
||||
### 4.3 `open-sse/translator/`
|
||||
|
||||
@@ -500,21 +499,21 @@ open-sse/
|
||||
|
||||
重点关注(完整列表见 `open-sse/services/`):
|
||||
|
||||
| 关注领域 | 文件 |
|
||||
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Combo 路由 | `combo.ts`(17 种策略)、`comboConfig.ts`、`comboMetrics.ts`、`comboManifestMetrics.ts`、`comboAgentMiddleware.ts` |
|
||||
| Auto Combo 引擎 | `autoCombo/` — `engine.ts`、`scoring.ts`、`taskFitness.ts`、`virtualFactory.ts`、`modePacks.ts`、`autoPrefix.ts`、`persistence.ts`、`providerDiversity.ts`、`providerRegistryAccessor.ts`、`routerStrategy.ts`、`selfHealing.ts`、`index.ts` |
|
||||
| 容灾 | `accountFallback.ts`(冷却 + 锁定)、`errorClassifier.ts`、`emergencyFallback.ts`、`rateLimitManager.ts`、`rateLimitSemaphore.ts`、`accountSemaphore.ts`、`accountSelector.ts` |
|
||||
| 配额 | `quotaMonitor.ts`、`quotaPreflight.ts`、`bailianQuotaFetcher.ts`、`codexQuotaFetcher.ts`、`deepseekQuotaFetcher.ts`、`crofUsageFetcher.ts`、`antigravityCredits.ts` |
|
||||
| 缓存 | `reasoningCache.ts`、`searchCache.ts`、`signatureCache.ts`、`requestDedup.ts` |
|
||||
| 路由智能 | `intentClassifier.ts`、`taskAwareRouter.ts`、`backgroundTaskDetector.ts`、`volumeDetector.ts`、`wildcardRouter.ts`、`workflowFSM.ts`、`specificityDetector.ts`、`specificityRules.ts`、`specificityTypes.ts` |
|
||||
| 模型处理 | `modelCapabilities.ts`、`modelDeprecation.ts`、`modelFamilyFallback.ts`、`modelStrip.ts`、`model.ts`、`provider.ts`、`providerRequestDefaults.ts`、`providerCostData.ts`、`payloadRules.ts` |
|
||||
| 压缩 | `compression/` — 完整的压缩引擎接线 |
|
||||
| Token + 会话 | `tokenRefresh.ts`、`sessionManager.ts`、`apiKeyRotator.ts`、`contextManager.ts`、`contextHandoff.ts`、`systemPrompt.ts`、`roleNormalizer.ts`、`responsesInputSanitizer.ts`、`toolSchemaSanitizer.ts`、`toolLimitDetector.ts`、`thinkingBudget.ts` |
|
||||
| 层级 / 清单 | `tierResolver.ts`、`tierConfig.ts`、`tierDefaults.json`、`tierTypes.ts`、`manifestAdapter.ts` |
|
||||
| IP / 网络 | `ipFilter.ts`、`webSearchFallback.ts` |
|
||||
| 批次 | `batchProcessor.ts` |
|
||||
| 用量 | `usage.ts` |
|
||||
| 关注领域 | 文件 |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Combo 路由 | `combo.ts`(17 种策略)、`comboConfig.ts`、`comboMetrics.ts`、`comboManifestMetrics.ts`、`comboAgentMiddleware.ts` |
|
||||
| Auto Combo 引擎 | `autoCombo/` — `engine.ts`、`scoring.ts`、`taskFitness.ts`、`virtualFactory.ts`、`modePacks.ts`、`autoPrefix.ts`、`persistence.ts`、`providerDiversity.ts`、`providerRegistryAccessor.ts`、`routerStrategy.ts`、`selfHealing.ts`、`index.ts` |
|
||||
| 容灾 | `accountFallback.ts`(冷却 + 锁定)、`errorClassifier.ts`、`emergencyFallback.ts`、`rateLimitManager.ts`、`rateLimitSemaphore.ts`、`accountSemaphore.ts`、`accountSelector.ts` |
|
||||
| 配额 | `quotaMonitor.ts`、`quotaPreflight.ts`、`bailianQuotaFetcher.ts`、`codexQuotaFetcher.ts`、`deepseekQuotaFetcher.ts`、`crofUsageFetcher.ts`、`antigravityCredits.ts` |
|
||||
| 缓存 | `reasoningCache.ts`、`searchCache.ts`、`signatureCache.ts`、`requestDedup.ts` |
|
||||
| 路由智能 | `intentClassifier.ts`、`taskAwareRouter.ts`、`backgroundTaskDetector.ts`、`volumeDetector.ts`、`wildcardRouter.ts`、`workflowFSM.ts`、`specificityDetector.ts`、`specificityRules.ts`、`specificityTypes.ts` |
|
||||
| 模型处理 | `modelCapabilities.ts`、`modelDeprecation.ts`、`modelFamilyFallback.ts`、`modelStrip.ts`、`model.ts`、`provider.ts`、`providerRequestDefaults.ts`、`providerCostData.ts`、`payloadRules.ts` |
|
||||
| 压缩 | `compression/` — 完整的压缩引擎接线 |
|
||||
| Token + 会话 | `tokenRefresh.ts`、`sessionManager.ts`、`apiKeyRotator.ts`、`contextManager.ts`、`contextHandoff.ts`、`systemPrompt.ts`、`roleNormalizer.ts`、`responsesInputSanitizer.ts`、`toolSchemaSanitizer.ts`、`toolLimitDetector.ts`、`thinkingBudget.ts` |
|
||||
| 层级 / 清单 | `tierResolver.ts`、`tierConfig.ts`、`tierDefaults.json`、`tierTypes.ts`、`manifestAdapter.ts` |
|
||||
| IP / 网络 | `ipFilter.ts`、`webSearchFallback.ts` |
|
||||
| 批次 | `batchProcessor.ts` |
|
||||
| 用量 | `usage.ts` |
|
||||
|
||||
### 4.6 `open-sse/mcp-server/`
|
||||
|
||||
@@ -624,14 +623,14 @@ bin/
|
||||
|
||||
常用命令:
|
||||
|
||||
| 命令 | 运行内容 |
|
||||
| --------------------------------------------------------- | -------------------------------------------------------------- |
|
||||
| `npm run test:unit` | `tests/unit/*.test.ts` 全部(Node 测试运行器,并发 10) |
|
||||
| `npm run test:vitest` | Vitest 套件(MCP、autoCombo、缓存) |
|
||||
| `npm run test:e2e` | Playwright UI 套件 |
|
||||
| `npm run test:protocols:e2e` | MCP + A2A 协议端到端 |
|
||||
| `npm run test:coverage` | 覆盖率门槛(行/语句/函数/分支 ≥ 60%) |
|
||||
| `node --import tsx/esm --test tests/unit/<file>.test.ts` | 单文件运行 |
|
||||
| 命令 | 运行内容 |
|
||||
| -------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| `npm run test:unit` | `tests/unit/*.test.ts` 全部(Node 测试运行器,并发 10) |
|
||||
| `npm run test:vitest` | Vitest 套件(MCP、autoCombo、缓存) |
|
||||
| `npm run test:e2e` | Playwright UI 套件 |
|
||||
| `npm run test:protocols:e2e` | MCP + A2A 协议端到端 |
|
||||
| `npm run test:coverage` | 覆盖率门槛(行/语句/函数/分支 ≥ 60%) |
|
||||
| `node --import tsx/esm --test tests/unit/<file>.test.ts` | 单文件运行 |
|
||||
|
||||
---
|
||||
|
||||
@@ -696,11 +695,11 @@ bin/
|
||||
|
||||
### 容灾运行时状态(三种机制)
|
||||
|
||||
| 机制 | 范围 | 位置 |
|
||||
| -------------- | ------------------ | -------------------------------------------------------------------------------------------------- |
|
||||
| 服务商熔断器 | 整个服务商 | `src/shared/utils/circuitBreaker.ts`,持久化于 `domain_circuit_breakers` |
|
||||
| 连接冷却 | 单个账户/Key | `src/sse/services/auth.ts` 中的 `markAccountUnavailable()`;由 `accountFallback.checkFallbackError()` 消费 |
|
||||
| 模型锁定 | 服务商 + 连接 + 模型| `open-sse/services/accountFallback.ts`,持久化于 `domain_lockout_state` |
|
||||
| 机制 | 范围 | 位置 |
|
||||
| ------------ | -------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| 服务商熔断器 | 整个服务商 | `src/shared/utils/circuitBreaker.ts`,持久化于 `domain_circuit_breakers` |
|
||||
| 连接冷却 | 单个账户/Key | `src/sse/services/auth.ts` 中的 `markAccountUnavailable()`;由 `accountFallback.checkFallbackError()` 消费 |
|
||||
| 模型锁定 | 服务商 + 连接 + 模型 | `open-sse/services/accountFallback.ts`,持久化于 `domain_lockout_state` |
|
||||
|
||||
参见 [RESILIENCE_GUIDE.md](./RESILIENCE_GUIDE.md) 和 [CLAUDE.md](../../CLAUDE.md) 中的专门章节。
|
||||
|
||||
|
||||
@@ -83,28 +83,28 @@ echo "OMNIROUTE_WS_BRIDGE_SECRET=$(openssl rand -base64 32)"
|
||||
|
||||
OmniRoute 使用 **SQLite**(通过 `better-sqlite3`)进行所有持久化存储。以下变量控制数据位置、加密和生命周期。
|
||||
|
||||
| 变量 | 默认值 | 源文件 | 说明 |
|
||||
| -------------------------------------- | -------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `DATA_DIR` | `~/.omniroute/` | `src/lib/db/core.ts` | SQLite 数据库、备份和数据文件的根目录。在 Docker 卷或自定义路径中可覆盖。 |
|
||||
| `STORAGE_ENCRYPTION_KEY` | _(空 = 禁用)_ | `src/lib/db/encryption.ts` | 用于 SQLite 数据库静态全量加密的 AES 密钥。使用 `openssl rand -hex 32` 生成。 |
|
||||
| `STORAGE_ENCRYPTION_KEY_VERSION` | `v1` | `scripts/build/bootstrap-env.mjs`, `electron/main.js` | 加密密钥的版本标签。进行密钥轮换时递增,以支持解密旧备份。 |
|
||||
| `DISABLE_SQLITE_AUTO_BACKUP` | `false` | `src/lib/db/backup.ts` | 设为 `true` 时,跳过每次启动前迁移时运行的自动数据库备份。 |
|
||||
| `OMNIROUTE_CRYPT_KEY` | _(未设置)_ | `src/lib/db/encryption.ts` | `STORAGE_ENCRYPTION_KEY` 的**旧版别名**。主变量缺失时作为回退被接受。 |
|
||||
| `OMNIROUTE_API_KEY_BASE64` | _(未设置)_ | `src/lib/db/encryption.ts` | **旧版别名**(Base64 编码形式),作为回退被接受。使用前自动解码。 |
|
||||
| `OMNIROUTE_DB_HEALTHCHECK_INTERVAL_MS` | _(未设置)_ | `src/lib/db/core.ts` | 覆盖定期 SQLite 健康检查的间隔(毫秒)。未设置时根据 `NODE_ENV` 推导默认值。 |
|
||||
| `OMNIROUTE_SKIP_DB_HEALTHCHECK` | `0` | `src/lib/db/core.ts`, `src/lib/db/healthCheck.ts` | 设为 `1` 可在启动时完全跳过数据库健康检查。适用于短生命周期任务和集成测试。 |
|
||||
| `OMNIROUTE_FORCE_DB_HEALTHCHECK` | `0` | `src/lib/db/core.ts` | 设为 `1` 可强制开启数据库健康检查循环,即使正常会被跳过(如短生命周期任务)。 |
|
||||
| `OMNIROUTE_SKIP_POSTINSTALL` | `0` | `scripts/postinstall.mjs` | 设为 `1` 可在 `npm install` 期间跳过原生运行时预热。适用于 CI/无头安装,此时 sqlite 已构建好。 |
|
||||
| `OMNIROUTE_MIGRATIONS_DIR` | _(自动检测)_ | `src/lib/db/migrationRunner.ts` | 覆盖迁移运行器扫描的目录。在自定义构建中打包迁移文件时很有用。 |
|
||||
| 变量 | 默认值 | 源文件 | 说明 |
|
||||
| -------------------------------------- | -------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `DATA_DIR` | `~/.omniroute/` | `src/lib/db/core.ts` | SQLite 数据库、备份和数据文件的根目录。在 Docker 卷或自定义路径中可覆盖。 |
|
||||
| `STORAGE_ENCRYPTION_KEY` | _(空 = 禁用)_ | `src/lib/db/encryption.ts` | 用于 SQLite 数据库静态全量加密的 AES 密钥。使用 `openssl rand -hex 32` 生成。 |
|
||||
| `STORAGE_ENCRYPTION_KEY_VERSION` | `v1` | `scripts/build/bootstrap-env.mjs`, `electron/main.js` | 加密密钥的版本标签。进行密钥轮换时递增,以支持解密旧备份。 |
|
||||
| `DISABLE_SQLITE_AUTO_BACKUP` | `false` | `src/lib/db/backup.ts` | 设为 `true` 时,跳过每次启动前迁移时运行的自动数据库备份。 |
|
||||
| `OMNIROUTE_CRYPT_KEY` | _(未设置)_ | `src/lib/db/encryption.ts` | `STORAGE_ENCRYPTION_KEY` 的**旧版别名**。主变量缺失时作为回退被接受。 |
|
||||
| `OMNIROUTE_API_KEY_BASE64` | _(未设置)_ | `src/lib/db/encryption.ts` | **旧版别名**(Base64 编码形式),作为回退被接受。使用前自动解码。 |
|
||||
| `OMNIROUTE_DB_HEALTHCHECK_INTERVAL_MS` | _(未设置)_ | `src/lib/db/core.ts` | 覆盖定期 SQLite 健康检查的间隔(毫秒)。未设置时根据 `NODE_ENV` 推导默认值。 |
|
||||
| `OMNIROUTE_SKIP_DB_HEALTHCHECK` | `0` | `src/lib/db/core.ts`, `src/lib/db/healthCheck.ts` | 设为 `1` 可在启动时完全跳过数据库健康检查。适用于短生命周期任务和集成测试。 |
|
||||
| `OMNIROUTE_FORCE_DB_HEALTHCHECK` | `0` | `src/lib/db/core.ts` | 设为 `1` 可强制开启数据库健康检查循环,即使正常会被跳过(如短生命周期任务)。 |
|
||||
| `OMNIROUTE_SKIP_POSTINSTALL` | `0` | `scripts/postinstall.mjs` | 设为 `1` 可在 `npm install` 期间跳过原生运行时预热。适用于 CI/无头安装,此时 sqlite 已构建好。 |
|
||||
| `OMNIROUTE_MIGRATIONS_DIR` | _(自动检测)_ | `src/lib/db/migrationRunner.ts` | 覆盖迁移运行器扫描的目录。在自定义构建中打包迁移文件时很有用。 |
|
||||
| `OMNIROUTE_EXTRA_MIGRATIONS_DIRS` | _(未设置)_ | `src/lib/db/migrationRunner/extraDirs.ts` | 以 `namespace=dir` 形式追加的迁移目录,条目之间用平台路径分隔符分隔(例如 `ee=/opt/app/enterprise/db/migrations`)。其中的文件会以 `<namespace>-<number>` 记录版本,因此自带迁移的发行版永远不会与上游的数字槽位冲突。条目格式错误、命名空间非法或目录缺失会在启动时抛错,而不是静默跳过该 schema。 |
|
||||
| `OMNIROUTE_MAX_PENDING_MIGRATIONS` | `50` | `src/lib/db/migrationRunner.ts` | 大量待处理迁移的安全阈值(#3416)。如果现有数据库上有超过此数量的待处理迁移,启动将中止(防止跟踪表被清空)。恢复旧备份时提高此值;设为 `0` 可禁用检查。 |
|
||||
| `OMNIROUTE_SPEND_FLUSH_INTERVAL_MS` | _(代码内默认值)_ | `src/lib/spend/batchWriter.ts` | 批量消费/成本写入器的刷新间隔(毫秒)。值越小写合并越少;值越大数据库争用越少。 |
|
||||
| `OMNIROUTE_SPEND_MAX_BUFFER_SIZE` | _(代码内默认值)_ | `src/lib/spend/batchWriter.ts` | 强制刷新前的最大缓存消费条目数。在高 QPS 部署中提高;在内存受限场景下降低。 |
|
||||
| `OMNIROUTE_PROXY_FETCH_DEBUG` | _(未设置)_ | `open-sse/utils/proxyFetch.ts` | 设为 `"true"` 可在 Vercel 中继路径上发出 `[ProxyFetch]` 调试日志。默认关闭以避免泄露路由提示。 |
|
||||
| `BATCH_RETRY_DURATION_MS` | `86400000`(24小时) | `open-sse/services/batchProcessor.ts` | 单个批次项的最大重试窗口(毫秒)。超过此时间的项被标记为失败。 |
|
||||
| `BATCH_BACKOFF_BASE_MS` | `5000` | `open-sse/services/batchProcessor.ts` | 批次项重试时指数退避的基础延迟(毫秒)。 |
|
||||
| `BATCH_BACKOFF_MAX_MS` | `3600000`(1小时) | `open-sse/services/batchProcessor.ts` | 批次项重试时指数退避的上限(毫秒)。 |
|
||||
| `BATCH_MAX_CONCURRENT` | `1` | `open-sse/services/batchProcessor.ts` | 并发处理的批次最大数量。提高以增加吞吐量;保持低值以避免速率限制风暴。 |
|
||||
| `OMNIROUTE_MAX_PENDING_MIGRATIONS` | `50` | `src/lib/db/migrationRunner.ts` | 大量待处理迁移的安全阈值(#3416)。如果现有数据库上有超过此数量的待处理迁移,启动将中止(防止跟踪表被清空)。恢复旧备份时提高此值;设为 `0` 可禁用检查。 |
|
||||
| `OMNIROUTE_SPEND_FLUSH_INTERVAL_MS` | _(代码内默认值)_ | `src/lib/spend/batchWriter.ts` | 批量消费/成本写入器的刷新间隔(毫秒)。值越小写合并越少;值越大数据库争用越少。 |
|
||||
| `OMNIROUTE_SPEND_MAX_BUFFER_SIZE` | _(代码内默认值)_ | `src/lib/spend/batchWriter.ts` | 强制刷新前的最大缓存消费条目数。在高 QPS 部署中提高;在内存受限场景下降低。 |
|
||||
| `OMNIROUTE_PROXY_FETCH_DEBUG` | _(未设置)_ | `open-sse/utils/proxyFetch.ts` | 设为 `"true"` 可在 Vercel 中继路径上发出 `[ProxyFetch]` 调试日志。默认关闭以避免泄露路由提示。 |
|
||||
| `BATCH_RETRY_DURATION_MS` | `86400000`(24小时) | `open-sse/services/batchProcessor.ts` | 单个批次项的最大重试窗口(毫秒)。超过此时间的项被标记为失败。 |
|
||||
| `BATCH_BACKOFF_BASE_MS` | `5000` | `open-sse/services/batchProcessor.ts` | 批次项重试时指数退避的基础延迟(毫秒)。 |
|
||||
| `BATCH_BACKOFF_MAX_MS` | `3600000`(1小时) | `open-sse/services/batchProcessor.ts` | 批次项重试时指数退避的上限(毫秒)。 |
|
||||
| `BATCH_MAX_CONCURRENT` | `1` | `open-sse/services/batchProcessor.ts` | 并发处理的批次最大数量。提高以增加吞吐量;保持低值以避免速率限制风暴。 |
|
||||
|
||||
### 场景
|
||||
|
||||
@@ -250,41 +250,37 @@ OmniRoute 提供两层防护:请求侧的注入扫描和响应侧的 PII 脱
|
||||
|
||||
## 7. URL 与云同步
|
||||
|
||||
| 变量 | 默认值 | 源文件 | 说明 |
|
||||
| ----------------------------------------- | ----------------------------------------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `BASE_URL` | `http://localhost:20128` | `src/lib/cloudSync.ts` | 内部同步任务调用 `/api/sync/cloud` 的服务器端 URL。即使应用被公共代理,也保持为 loopback/容器 URL。 |
|
||||
| `CLOUD_URL` | _(空)_ | `src/lib/cloudSync.ts` | 云中继端点 URL(高级功能)。 |
|
||||
| `CLOUD_SYNC_TIMEOUT_MS` | `12000` | `src/lib/cloudSync.ts` | 云同步请求的 HTTP 超时。 |
|
||||
| `OMNIROUTE_BUILD_PROFILE` | `full` | Webpack 构建配置 | 构建时配置文件(设为 `minimal` 可物理排除特权模块不打包)。 |
|
||||
| `OMNIROUTE_CLOUD_SYNC_SECRET` | _(空)_ | `src/lib/cloudSync.ts` | 用于校验云同步响应 HMAC-SHA256 签名的共享密钥。 |
|
||||
| `OMNIROUTE_CLOUD_SYNC_SECRETS` | `false` | `src/lib/cloudSync.ts` | 设为 `true` 允许云同步端点覆盖本地凭证。默认 `false`。 |
|
||||
| `OMNIROUTE_ZED_IMPORT_LEGACY_ONE_STEP` | `false` | `src/app/api/providers/zed/import/route.ts` | 设为 `true` 可回退到 v3.8.5 的一步式"导入全部"行为,无需用户确认。 |
|
||||
| `NEXT_PUBLIC_BASE_URL` | `http://localhost:20128` | OAuth、Dashboard、同步 | 面向公共的 URL,用于 OAuth redirect_uri、Dashboard 链接、生成的公共 URL 以及同源浏览器变更检查。**在反向代理背后时,必须匹配你的公共 URL。** |
|
||||
| `NEXT_PUBLIC_CLOUD_URL` | _(空)_ | 客户端侧 | `CLOUD_URL` 的客户端镜像。 |
|
||||
| `NEXT_PUBLIC_APP_URL` | _(未设置)_ | `src/shared/services/cloudSyncScheduler.ts` | `NEXT_PUBLIC_BASE_URL` 的旧版回退。 |
|
||||
| `OMNIROUTE_PUBLIC_BASE_URL` | _(未设置)_ | 公共源解析器、图片 URL | 最高优先级的浏览器侧 OmniRoute 源,用于公共 URL 生成和源校验(例如 `/v1/chatgpt-web/image/<id>`)。当 OpenWebUI 或其他中继通过内部 URL 访问 OmniRoute,但用户浏览器必须从 LAN、隧道或公共源获取图片时设置。**不要**包含 `/v1`。 |
|
||||
| `OMNIROUTE_TRUST_PROXY` | _(未设置)_ | `src/server/origin/publicOrigin.ts` | 可选的转发公共源头信任模式。未设置 = 出于安全考虑不信任 `Forwarded` / `X-Forwarded-*`。`true` / `loopback` 仅信任来自经过 Token 戳记的 loopback 代理的转发 host/proto。`private` / `lan` 还信任私有 LAN 代理对端。生产环境中推荐显式设置 `NEXT_PUBLIC_BASE_URL`。 |
|
||||
| `OMNIROUTE_CGPT_WEB_IMAGE_TIMEOUT_MS` | `180000`(3 分钟) | `open-sse/executors/chatgpt-web.ts` | 等待异步 chatgpt-web 图片通过 Celsius WebSocket 到达的最大时间。在上游排队窗口较长时提高此值。 |
|
||||
| `OMNIROUTE_CGPT_WEB_IMAGE_CACHE_MAX_MB` | `256` | `open-sse/services/chatgptImageCache.ts` | 为 `/v1/chatgpt-web/image/<id>` 提供服务的 chatgpt-web 图片缓存的内存预算总额(MB)。在内存受限的主机上降低;图片生成量大且客户端争抢 30 分钟 TTL 时提高。 |
|
||||
| `OMNIROUTE_CGPT_WEB_PRO_TIMEOUT_MS` | `1200000`(20 分钟) | `open-sse/executors/chatgpt-web.ts` | chatgpt-web GPT-5.5 Pro 后台轮询交接的总体等待预算。Pro 推理在带外完成,OmniRoute 轮询直到结果到达或预算耗尽。如果 Pro 请求完成前超时,请提高此值。 |
|
||||
| `OMNIROUTE_CGPT_WEB_PRO_POLL_INTERVAL_MS` | `4000`(4 秒) | `open-sse/executors/chatgpt-web.ts` | chatgpt-web GPT-5.5 Pro 后台轮询尝试的间隔。降低可更快完成但增加上游轮询;提高可减少请求量。 |
|
||||
| `THEOLDLLM_NAV_TIMEOUT_MS` | `30000`(30 秒) | `open-sse/executors/theoldllm.ts` | 浏览器端 Token 捕获(The Old LLM (theoldllm) 免费服务商使用)的 Playwright 导航超时(毫秒)。如果中继页面加载慢,可在慢速网络上提高。 |
|
||||
| `KIE_CALLBACK_URL` | _(未设置)_ | `open-sse/utils/kieTask.ts` | 异步 kie.ai 任务的公共回调 URL。优先级高于 `OMNIROUTE_KIE_CALLBACK_URL` 和 `OMNIROUTE_PUBLIC_URL`。 |
|
||||
| `OMNIROUTE_KIE_CALLBACK_URL` | _(未设置)_ | `open-sse/utils/kieTask.ts` | `KIE_CALLBACK_URL` 的替代写法。主变量未设置时的回退。 |
|
||||
| `OMNIROUTE_PUBLIC_URL` | _(未设置)_ | `open-sse/utils/kieTask.ts` | 用于组合异步回调 URL 的公共源。kie.ai 回调的最低优先级回退;也用作其他中继的通用公共 URL。 |
|
||||
| `OMNIROUTE_CROF_USAGE_URL` | `https://crof.ai/usage_api/` | `open-sse/services/usage.ts` | Usage 页面使用的 CrofAI 配额查询端点。可覆盖为中继/测试固定件。 |
|
||||
| `OMNIROUTE_OPENCODE_QUOTA_URL` | `https://opencode.ai/zen/go/v1/quota` | `open-sse/services/opencodeQuotaFetcher.ts` | Usage 页面使用的 OpenCode (zen/go) 配额查询端点。可覆盖为中继/测试固定件。 |
|
||||
| `OMNIROUTE_OPENCODE_GO_QUOTA_URL` | _(未设置)_ | `open-sse/services/opencodeOllamaUsage.ts` | Usage 页面使用的 OpenCode Go 配额查询端点。OpenCode Go 没有公开的配额 API,因此没有默认值;除非运维人员显式设置该变量选择接入自建/镜像端点,否则不会发起网络请求。 |
|
||||
| `OMNIROUTE_OPENCODE_GO_DASHBOARD_URL` | `https://opencode.ai/workspace` | `open-sse/services/usage.ts` | 配置了 workspace ID 和 auth Cookie 时用于配额抓取的 OpenCode Go Dashboard 基础 URL。可覆盖为中继/测试固定件。 |
|
||||
| `OPENCODE_GO_WORKSPACE_ID` | _(未设置)_ | `open-sse/services/usage.ts` | 用于 Dashboard 配额抓取的 OpenCode Go workspace ID。配置多个账户时,推荐使用每个连接的 Dashboard 字段。 |
|
||||
| `OMNIROUTE_OPENCODE_GO_WORKSPACE_ID` | _(未设置)_ | `open-sse/services/usage.ts` | OpenCode Go workspace ID 环境变量的备选名,在较短的别名之前使用。配置多个账户时,推荐使用每个连接的 Dashboard 字段。 |
|
||||
| `OPENCODE_GO_AUTH_COOKIE` | _(未设置)_ | `open-sse/services/usage.ts` | 用于 Dashboard 配额抓取的 OpenCode Go `auth` Cookie。敏感信息;配置多个账户时,推荐使用每个连接的 Dashboard 字段。 |
|
||||
| `OMNIROUTE_OPENCODE_GO_AUTH_COOKIE` | _(未设置)_ | `open-sse/services/usage.ts` | OpenCode Go `auth` Cookie 环境变量的备选名,在较短的别名之前使用。敏感信息;配置多个账户时,推荐使用每个连接的 Dashboard 字段。 |
|
||||
| `OMNIROUTE_OLLAMA_CLOUD_USAGE_URL` | `https://ollama.com/settings` | `open-sse/services/usage.ts` | 用于配额抓取的 Ollama Cloud settings URL。可覆盖为中继/测试固定件。 |
|
||||
| `OLLAMA_USAGE_COOKIE` | _(未设置)_ | `open-sse/services/usage.ts` | 用于设置页面配额抓取的 Ollama Cloud `__Secure-session` Cookie。敏感信息;配置多个账户时,推荐使用每个连接的 Dashboard 字段。 |
|
||||
| `OLLAMA_CLOUD_USAGE_COOKIE` | _(未设置)_ | `open-sse/services/usage.ts` | Ollama Cloud `__Secure-session` Cookie 环境变量的备选名。敏感信息;配置多个账户时,推荐使用每个连接的 Dashboard 字段。 |
|
||||
| `OMNIROUTE_OLLAMA_USAGE_COOKIE` | _(未设置)_ | `open-sse/services/usage.ts` | Ollama Cloud `__Secure-session` Cookie 环境变量的备选名,在较短的别名之前使用。敏感信息;配置多个账户时,推荐使用每个连接的 Dashboard 字段。 |
|
||||
| `OMNIROUTE_CODEWHISPERER_BASE_URL` | `https://codewhisperer.us-east-1.amazonaws.com` | `open-sse/services/usage.ts` | CodeWhisperer (AWS Kiro) 用量限制端点。可覆盖为中继/测试固定件。 |
|
||||
| 变量 | 默认值 | 源文件 | 说明 |
|
||||
| -------------------------------------- | ----------------------------------------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `BASE_URL` | `http://localhost:20128` | `src/lib/cloudSync.ts` | 内部同步任务调用 `/api/sync/cloud` 的服务器端 URL。即使应用被公共代理,也保持为 loopback/容器 URL。 |
|
||||
| `CLOUD_URL` | _(空)_ | `src/lib/cloudSync.ts` | 云中继端点 URL(高级功能)。 |
|
||||
| `CLOUD_SYNC_TIMEOUT_MS` | `12000` | `src/lib/cloudSync.ts` | 云同步请求的 HTTP 超时。 |
|
||||
| `OMNIROUTE_BUILD_PROFILE` | `full` | Webpack 构建配置 | 构建时配置文件(设为 `minimal` 可物理排除特权模块不打包)。 |
|
||||
| `OMNIROUTE_CLOUD_SYNC_SECRET` | _(空)_ | `src/lib/cloudSync.ts` | 用于校验云同步响应 HMAC-SHA256 签名的共享密钥。 |
|
||||
| `OMNIROUTE_CLOUD_SYNC_SECRETS` | `false` | `src/lib/cloudSync.ts` | 设为 `true` 允许云同步端点覆盖本地凭证。默认 `false`。 |
|
||||
| `OMNIROUTE_ZED_IMPORT_LEGACY_ONE_STEP` | `false` | `src/app/api/providers/zed/import/route.ts` | 设为 `true` 可回退到 v3.8.5 的一步式"导入全部"行为,无需用户确认。 |
|
||||
| `NEXT_PUBLIC_BASE_URL` | `http://localhost:20128` | OAuth、Dashboard、同步 | 面向公共的 URL,用于 OAuth redirect_uri、Dashboard 链接、生成的公共 URL 以及同源浏览器变更检查。**在反向代理背后时,必须匹配你的公共 URL。** |
|
||||
| `NEXT_PUBLIC_CLOUD_URL` | _(空)_ | 客户端侧 | `CLOUD_URL` 的客户端镜像。 |
|
||||
| `NEXT_PUBLIC_APP_URL` | _(未设置)_ | `src/shared/services/cloudSyncScheduler.ts` | `NEXT_PUBLIC_BASE_URL` 的旧版回退。 |
|
||||
| `OMNIROUTE_PUBLIC_BASE_URL` | _(未设置)_ | 公共源解析器、图片 URL | 最高优先级的浏览器侧 OmniRoute 源,用于公共 URL 生成和非 Dashboard 浏览器源校验。当 OpenWebUI 或其他中继通过内部 URL 访问 OmniRoute,但用户浏览器必须从 LAN、隧道或公共源获取生成媒体时设置。**不要**包含 `/v1`。 |
|
||||
| `OMNIROUTE_TRUST_PROXY` | _(未设置)_ | `src/server/origin/publicOrigin.ts` | 可选的转发公共源头信任模式。未设置 = 出于安全考虑不信任 `Forwarded` / `X-Forwarded-*`。`true` / `loopback` 仅信任来自经过 Token 戳记的 loopback 代理的转发 host/proto。`private` / `lan` 还信任私有 LAN 代理对端。生产环境中推荐显式设置 `NEXT_PUBLIC_BASE_URL`。 |
|
||||
| `THEOLDLLM_NAV_TIMEOUT_MS` | `30000`(30 秒) | `open-sse/executors/theoldllm.ts` | 浏览器端 Token 捕获(The Old LLM (theoldllm) 免费服务商使用)的 Playwright 导航超时(毫秒)。如果中继页面加载慢,可在慢速网络上提高。 |
|
||||
| `KIE_CALLBACK_URL` | _(未设置)_ | `open-sse/utils/kieTask.ts` | 异步 kie.ai 任务的公共回调 URL。优先级高于 `OMNIROUTE_KIE_CALLBACK_URL` 和 `OMNIROUTE_PUBLIC_URL`。 |
|
||||
| `OMNIROUTE_KIE_CALLBACK_URL` | _(未设置)_ | `open-sse/utils/kieTask.ts` | `KIE_CALLBACK_URL` 的替代写法。主变量未设置时的回退。 |
|
||||
| `OMNIROUTE_PUBLIC_URL` | _(未设置)_ | `open-sse/utils/kieTask.ts` | 用于组合异步回调 URL 的公共源。kie.ai 回调的最低优先级回退;也用作其他中继的通用公共 URL。 |
|
||||
| `OMNIROUTE_CROF_USAGE_URL` | `https://crof.ai/usage_api/` | `open-sse/services/usage.ts` | Usage 页面使用的 CrofAI 配额查询端点。可覆盖为中继/测试固定件。 |
|
||||
| `OMNIROUTE_OPENCODE_QUOTA_URL` | `https://opencode.ai/zen/go/v1/quota` | `open-sse/services/opencodeQuotaFetcher.ts` | Usage 页面使用的 OpenCode (zen/go) 配额查询端点。可覆盖为中继/测试固定件。 |
|
||||
| `OMNIROUTE_OPENCODE_GO_QUOTA_URL` | _(未设置)_ | `open-sse/services/opencodeOllamaUsage.ts` | Usage 页面使用的 OpenCode Go 配额查询端点。OpenCode Go 没有公开的配额 API,因此没有默认值;除非运维人员显式设置该变量选择接入自建/镜像端点,否则不会发起网络请求。 |
|
||||
| `OMNIROUTE_OPENCODE_GO_DASHBOARD_URL` | `https://opencode.ai/workspace` | `open-sse/services/usage.ts` | 配置了 workspace ID 和 auth Cookie 时用于配额抓取的 OpenCode Go Dashboard 基础 URL。可覆盖为中继/测试固定件。 |
|
||||
| `OPENCODE_GO_WORKSPACE_ID` | _(未设置)_ | `open-sse/services/usage.ts` | 用于 Dashboard 配额抓取的 OpenCode Go workspace ID。配置多个账户时,推荐使用每个连接的 Dashboard 字段。 |
|
||||
| `OMNIROUTE_OPENCODE_GO_WORKSPACE_ID` | _(未设置)_ | `open-sse/services/usage.ts` | OpenCode Go workspace ID 环境变量的备选名,在较短的别名之前使用。配置多个账户时,推荐使用每个连接的 Dashboard 字段。 |
|
||||
| `OPENCODE_GO_AUTH_COOKIE` | _(未设置)_ | `open-sse/services/usage.ts` | 用于 Dashboard 配额抓取的 OpenCode Go `auth` Cookie。敏感信息;配置多个账户时,推荐使用每个连接的 Dashboard 字段。 |
|
||||
| `OMNIROUTE_OPENCODE_GO_AUTH_COOKIE` | _(未设置)_ | `open-sse/services/usage.ts` | OpenCode Go `auth` Cookie 环境变量的备选名,在较短的别名之前使用。敏感信息;配置多个账户时,推荐使用每个连接的 Dashboard 字段。 |
|
||||
| `OMNIROUTE_OLLAMA_CLOUD_USAGE_URL` | `https://ollama.com/settings` | `open-sse/services/usage.ts` | 用于配额抓取的 Ollama Cloud settings URL。可覆盖为中继/测试固定件。 |
|
||||
| `OLLAMA_USAGE_COOKIE` | _(未设置)_ | `open-sse/services/usage.ts` | 用于设置页面配额抓取的 Ollama Cloud `__Secure-session` Cookie。敏感信息;配置多个账户时,推荐使用每个连接的 Dashboard 字段。 |
|
||||
| `OLLAMA_CLOUD_USAGE_COOKIE` | _(未设置)_ | `open-sse/services/usage.ts` | Ollama Cloud `__Secure-session` Cookie 环境变量的备选名。敏感信息;配置多个账户时,推荐使用每个连接的 Dashboard 字段。 |
|
||||
| `OMNIROUTE_OLLAMA_USAGE_COOKIE` | _(未设置)_ | `open-sse/services/usage.ts` | Ollama Cloud `__Secure-session` Cookie 环境变量的备选名,在较短的别名之前使用。敏感信息;配置多个账户时,推荐使用每个连接的 Dashboard 字段。 |
|
||||
| `OMNIROUTE_CODEWHISPERER_BASE_URL` | `https://codewhisperer.us-east-1.amazonaws.com` | `open-sse/services/usage.ts` | CodeWhisperer (AWS Kiro) 用量限制端点。可覆盖为中继/测试固定件。 |
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 当部署在反向代理(nginx、Caddy)之后时,**必须**将 `NEXT_PUBLIC_BASE_URL` 设置为你的公共 URL(例如 `https://omniroute.example.com`)。否则 OAuth 回调可能因 redirect_uri 不匹配而失败,生成的公共链接可能指向内部容器源,同源 Dashboard 变更可能被浏览器源检查拒绝。
|
||||
@@ -581,36 +577,33 @@ REQUEST_TIMEOUT_MS (全局覆盖)
|
||||
└── API_BRIDGE_SERVER_SOCKET_TIMEOUT_MS (默认值: 0 = 禁用)
|
||||
```
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
| ------------------------------------------------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `REQUEST_TIMEOUT_MS` | _(未设置)_ | 全局快捷方式 — 覆盖 `FETCH_TIMEOUT_MS` 和 `STREAM_IDLE_TIMEOUT_MS` 两者的默认值。 |
|
||||
| `FETCH_TIMEOUT_MS` | `600000` | 上游服务商调用的 HTTP 请求总超时。 |
|
||||
| `STREAM_IDLE_TIMEOUT_MS` | `600000` | SSE 块之间的最长静默时间,超时则中止。扩展推理模型很少暂停超过 90 秒。 |
|
||||
| `STREAM_READINESS_TIMEOUT_MS` | `80000` | 接收第一个非 ping SSE 事件的超时时间。设置时继承 `REQUEST_TIMEOUT_MS`。 |
|
||||
| `OMNIROUTE_CODEX_DROP_NONSTANDARD_EVENTS` | _(关闭)_ | 剥离会导致 OpenAI SDK 的 `responses.stream()` 以 502 报错的非标准 `codex.*` SSE 事件(如 `codex.rate_limits`)。设为 `true`/`1`/`yes` 可启用。 |
|
||||
| `FETCH_HEADERS_TIMEOUT_MS` | = `FETCH_TIMEOUT_MS` | 接收响应头的超时时间。 |
|
||||
| `FETCH_BODY_TIMEOUT_MS` | = `FETCH_TIMEOUT_MS` | 接收完整响应体的超时时间。 |
|
||||
| `FETCH_CONNECT_TIMEOUT_MS` | `30000` | TCP 连接建立超时。 |
|
||||
| `FETCH_KEEPALIVE_TIMEOUT_MS` | `4000` | Keep-alive 套接字空闲超时。 |
|
||||
| `TLS_CLIENT_TIMEOUT_MS` | = `FETCH_TIMEOUT_MS` | TLS 指纹代理(wreq-js)超时。 |
|
||||
| `API_BRIDGE_PROXY_TIMEOUT_MS` | `30000` | `/v1` 桥接请求的代理跳超时。 |
|
||||
| `API_BRIDGE_SERVER_REQUEST_TIMEOUT_MS` | `300000` | 桥接的服务器请求总超时。 |
|
||||
| `API_BRIDGE_SERVER_HEADERS_TIMEOUT_MS` | `60000` | 通过桥接发送响应头的超时时间。 |
|
||||
| `API_BRIDGE_SERVER_KEEPALIVE_TIMEOUT_MS` | `5000` | 桥接 keep-alive 空闲超时。 |
|
||||
| `API_BRIDGE_SERVER_SOCKET_TIMEOUT_MS` | `0` | 原始套接字超时(0 = 禁用)。 |
|
||||
| `SHUTDOWN_TIMEOUT_MS` | `30000` | SIGTERM/SIGINT 后强制退出前的宽限期。 |
|
||||
| `OMNIROUTE_DEFAULT_FETCH_TIMEOUT_MS` | `120000` | `FETCH_TIMEOUT_MS` 未设置时 `src/shared/utils/fetchTimeout.ts` 使用的回退值。 |
|
||||
| `OMNIROUTE_CHATGPT_TLS_TIMEOUT_MS` | `60000` | bogdanfinn/tls-client koffi 绑定的线路级超时(`chatgptTlsClient.ts`)。 |
|
||||
| `OMNIROUTE_CHATGPT_TLS_GRACE_MS` | `10000` | 原生绑定卡住时在线路超时之上添加的 JS 侧宽恕时间。 |
|
||||
| `OMNIROUTE_CHATGPT_STREAM_FIRST_BYTE_TIMEOUT_MS` | `30000`(30 秒) | ChatGPT TLS sidecar(`chatgptTlsClient.ts`)在中止死流前等待第一个流式字节的最大时间。如果上游冷启动超过窗口则提高。 |
|
||||
| `OMNIROUTE_CLAUDE_TLS_TIMEOUT_MS` | `60000` | bogdanfinn/tls-client koffi 绑定的线路级超时(`claudeTlsClient.ts`)。 |
|
||||
| `OMNIROUTE_CLAUDE_TLS_GRACE_MS` | `10000` | 原生绑定卡住时在线路超时之上添加的 JS 侧宽恕时间。 |
|
||||
| `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` | `30000` | bogdanfinn/tls-client koffi 绑定的线路级超时(`perplexityTlsClient.ts`)。 |
|
||||
| `OMNIROUTE_PPLX_TLS_GRACE_MS` | `10000` | 原生绑定卡住时在线路超时之上添加的 JS 侧宽恕时间。 |
|
||||
| `OMNIROUTE_GROK_TLS_TIMEOUT_MS` | `60000` | bogdanfinn/tls-client koffi 绑定的线路级超时(`grokTlsClient.ts`)。 |
|
||||
| `OMNIROUTE_GROK_TLS_GRACE_MS` | `10000` | 原生绑定卡住时在线路超时之上添加的 JS 侧宽恕时间。 |
|
||||
| `OMNIROUTE_BROWSER_POOL` | `on` | 用于浏览器端 Web Cookie 聊天的共享 Playwright 浏览器池(`browserPool.ts`);设为 `off` 可禁用。 |
|
||||
| `WEB_COOKIE_USE_BROWSER` | `0` | 将 Web Cookie 聊天请求选择进入浏览器端路径(`browserBackedChat.ts`);`1` 启用。 |
|
||||
| 变量 | 默认值 | 说明 |
|
||||
| ----------------------------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `REQUEST_TIMEOUT_MS` | _(未设置)_ | 全局快捷方式 — 覆盖 `FETCH_TIMEOUT_MS` 和 `STREAM_IDLE_TIMEOUT_MS` 两者的默认值。 |
|
||||
| `FETCH_TIMEOUT_MS` | `600000` | 上游服务商调用的 HTTP 请求总超时。 |
|
||||
| `STREAM_IDLE_TIMEOUT_MS` | `600000` | SSE 块之间的最长静默时间,超时则中止。扩展推理模型很少暂停超过 90 秒。 |
|
||||
| `STREAM_READINESS_TIMEOUT_MS` | `80000` | 接收第一个非 ping SSE 事件的超时时间。设置时继承 `REQUEST_TIMEOUT_MS`。 |
|
||||
| `OMNIROUTE_CODEX_DROP_NONSTANDARD_EVENTS` | _(关闭)_ | 剥离会导致 OpenAI SDK 的 `responses.stream()` 以 502 报错的非标准 `codex.*` SSE 事件(如 `codex.rate_limits`)。设为 `true`/`1`/`yes` 可启用。 |
|
||||
| `FETCH_HEADERS_TIMEOUT_MS` | = `FETCH_TIMEOUT_MS` | 接收响应头的超时时间。 |
|
||||
| `FETCH_BODY_TIMEOUT_MS` | = `FETCH_TIMEOUT_MS` | 接收完整响应体的超时时间。 |
|
||||
| `FETCH_CONNECT_TIMEOUT_MS` | `30000` | TCP 连接建立超时。 |
|
||||
| `FETCH_KEEPALIVE_TIMEOUT_MS` | `4000` | Keep-alive 套接字空闲超时。 |
|
||||
| `TLS_CLIENT_TIMEOUT_MS` | = `FETCH_TIMEOUT_MS` | TLS 指纹代理(wreq-js)超时。 |
|
||||
| `API_BRIDGE_PROXY_TIMEOUT_MS` | `30000` | `/v1` 桥接请求的代理跳超时。 |
|
||||
| `API_BRIDGE_SERVER_REQUEST_TIMEOUT_MS` | `300000` | 桥接的服务器请求总超时。 |
|
||||
| `API_BRIDGE_SERVER_HEADERS_TIMEOUT_MS` | `60000` | 通过桥接发送响应头的超时时间。 |
|
||||
| `API_BRIDGE_SERVER_KEEPALIVE_TIMEOUT_MS` | `5000` | 桥接 keep-alive 空闲超时。 |
|
||||
| `API_BRIDGE_SERVER_SOCKET_TIMEOUT_MS` | `0` | 原始套接字超时(0 = 禁用)。 |
|
||||
| `SHUTDOWN_TIMEOUT_MS` | `30000` | SIGTERM/SIGINT 后强制退出前的宽限期。 |
|
||||
| `OMNIROUTE_DEFAULT_FETCH_TIMEOUT_MS` | `120000` | `FETCH_TIMEOUT_MS` 未设置时 `src/shared/utils/fetchTimeout.ts` 使用的回退值。 |
|
||||
| `OMNIROUTE_CLAUDE_TLS_TIMEOUT_MS` | `60000` | bogdanfinn/tls-client koffi 绑定的线路级超时(`claudeTlsClient.ts`)。 |
|
||||
| `OMNIROUTE_CLAUDE_TLS_GRACE_MS` | `10000` | 原生绑定卡住时在线路超时之上添加的 JS 侧宽恕时间。 |
|
||||
| `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` | `30000` | bogdanfinn/tls-client koffi 绑定的线路级超时(`perplexityTlsClient.ts`)。 |
|
||||
| `OMNIROUTE_PPLX_TLS_GRACE_MS` | `10000` | 原生绑定卡住时在线路超时之上添加的 JS 侧宽恕时间。 |
|
||||
| `OMNIROUTE_GROK_TLS_TIMEOUT_MS` | `60000` | bogdanfinn/tls-client koffi 绑定的线路级超时(`grokTlsClient.ts`)。 |
|
||||
| `OMNIROUTE_GROK_TLS_GRACE_MS` | `10000` | 原生绑定卡住时在线路超时之上添加的 JS 侧宽恕时间。 |
|
||||
| `OMNIROUTE_BROWSER_POOL` | `on` | 用于浏览器端 Web Cookie 聊天的共享 Playwright 浏览器池(`browserPool.ts`);设为 `off` 可禁用。 |
|
||||
| `WEB_COOKIE_USE_BROWSER` | `0` | 将 Web Cookie 聊天请求选择进入浏览器端路径(`browserBackedChat.ts`);`1` 启用。 |
|
||||
|
||||
Combo 目标尝试继承已解析的上游请求超时(`FETCH_TIMEOUT_MS`,或当它提供 fetch 默认值时的 `REQUEST_TIMEOUT_MS`)。仅在 Combo 中设置 `targetTimeoutMs`、Combo 默认值或服务商覆盖值以加快 Combo 回退;超过当前上游超时的值会被截断到上游超时。
|
||||
|
||||
@@ -979,8 +972,7 @@ CLI_COMPAT_ALL=1
|
||||
| `NGROK_AUTHTOKEN` | _(未设置)_ | `src/lib/ngrokTunnel.ts` | 认证出口 ngrok 隧道。 |
|
||||
| `DB_BACKUP_MAX_FILES` | `20` | `src/lib/db/backup.ts` | 磁盘上保留的最大 SQLite 备份文件数。覆盖从 Settings → Database backup retention 保存的值。 |
|
||||
| `DB_BACKUP_RETENTION_DAYS` | `0` | `src/lib/db/backup.ts` | 保留备份的最大天数。`0` 禁用基于时间的清理。覆盖从 Settings → Database backup retention 保存的值。 |
|
||||
| `OMNIROUTE_TLS_PROXY_URL` | _(未设置)_ | `open-sse/services/chatgptTlsClient.ts` | 覆盖测试用的 TLS sidecar URL。生产环境应保持未设置。 |
|
||||
| `CONTAINER_HOST` | `docker` | `scripts/check-permissions.sh` | 入口点权限检查的容器运行时提示。任何 Podman 拓扑都应设为 `podman`。由于容器无法判断引擎位于本地还是通过 Podman Machine 访问,警告会保持拓扑中立并指向 `contrib/podman/README.md`。 |
|
||||
| `CONTAINER_HOST` | `docker` | `scripts/check-permissions.sh` | 入口点权限检查的容器运行时提示。任何 Podman 拓扑都应设为 `podman`。由于容器无法判断引擎位于本地还是通过 Podman Machine 访问,警告会保持拓扑中立并指向 `contrib/podman/README.md`。 |
|
||||
| `QUOTA_STORE_DRIVER` | `sqlite` | `src/lib/quota/storeFactory.ts` | 配额共享消费存储后端:`sqlite`(默认)或 `redis`。 |
|
||||
| `QUOTA_STORE_REDIS_URL` | _(未设置)_ | `src/lib/quota/storeFactory.ts` | `QUOTA_STORE_DRIVER=redis` 时使用的 Redis 连接字符串(如 `redis://localhost:6379`)。 |
|
||||
| `QUOTA_SATURATION_THRESHOLD` | `0.5` | `src/lib/quota/enforce.ts` | 池饱和比率(0..1);达到或超过该值时池进入严格模式(不允许借用)。 |
|
||||
|
||||
@@ -466,8 +466,6 @@ FSM 轉換結果饋入自動組合的評分,對背景/自動化任務偏向較
|
||||
- Claude Code CCH(相容性通道握手):`open-sse/services/claudeCodeCCH.ts`,加上 `claudeCodeCompatible.ts`、`claudeCodeConstraints.ts`、`claudeCodeExtraRemap.ts`、`claudeCodeToolRemapper.ts`
|
||||
- Claude Code 指紋塑造:`open-sse/services/claudeCodeFingerprint.ts`
|
||||
- Claude Code 混淆:`open-sse/services/claudeCodeObfuscation.ts`
|
||||
- ChatGPT TLS 客戶端:`open-sse/services/chatgptTlsClient.ts`(curl-impersonate 風格的 ChatGPT-Web 工作階段)
|
||||
- ChatGPT 圖片快取:`open-sse/services/chatgptImageCache.ts`
|
||||
|
||||
完整的隱匿手冊與操作指南,請參閱
|
||||
[`docs/security/STEALTH_GUIDE.md`](../security/STEALTH_GUIDE.md)。
|
||||
@@ -873,103 +871,103 @@ flowchart LR
|
||||
|
||||
每個提供者都有一個專門的執行器,繼承自 `BaseExecutor`(位於 `open-sse/executors/base.ts`),提供 URL 建置、標頭建構、指數退避重試、憑證刷新鉤子與 `execute()` 協調方法。
|
||||
|
||||
| 執行器 | 提供者 | 特殊處理 |
|
||||
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
||||
| `DefaultExecutor` | OpenAI、Claude、Gemini、Qwen、OpenRouter、GLM、Kimi、MiniMax、DeepSeek、Groq、xAI、Mistral、Perplexity、Together、Fireworks、Cerebras、Cohere、NVIDIA 等 | 每個提供者的動態 URL/標頭設定 |
|
||||
| `AntigravityExecutor` | Google Antigravity | 自訂專案/工作階段 ID、Retry-After 解析、429 混淆 |
|
||||
| `AzureOpenAIExecutor` | Azure OpenAI | 基於部署的路由、api-version 查詢強制 |
|
||||
| `BlackboxWebExecutor` | Blackbox AI(網頁模式) | 含 TLS 指紋模擬的網頁工作階段反向 |
|
||||
| `ChatGPTWebExecutor` | ChatGPT 網頁 | TLS 客戶端 + 工作階段 Cookie 管理(`chatgptTlsClient.ts`) |
|
||||
| `ClaudeIdentityExecutor` | Claude.ai(CCH 路徑) | 約束 + 工具重新對應管線、指紋塑造 |
|
||||
| `CliProxyApiExecutor` | CLIProxyAPI 相容提供者 | 自訂驗證與協定處理 |
|
||||
| `CloudflareAiExecutor` | Cloudflare Workers AI | 帳戶 ID 注入、基於 Neurons 的用量追蹤 |
|
||||
| `CodexExecutor` | OpenAI Codex | 注入系統指令、強制推理努力 |
|
||||
| `CommandCodeExecutor` | Command Code | OAuth + 每個工作階段的標頭輪換 |
|
||||
| `CursorExecutor` | Cursor IDE | ConnectRPC 協定、Protobuf 編碼、透過 checksum 的請求簽署 |
|
||||
| `DevinCliExecutor` | Devin CLI | 透過雲端代理模組的 Devin 任務生命週期橋接 |
|
||||
| `GithubExecutor` | GitHub Copilot | Copilot 令牌刷新、模擬 VSCode 標頭 |
|
||||
| `GitlabExecutor` | GitLab Duo | GitLab OAuth + 專案範圍路由 |
|
||||
| `GlmExecutor` | Z.AI GLM(含 `glmt` 預設) | 思考預算感知、GLMT 預設常數 |
|
||||
| `GrokWebExecutor` | xAI Grok 網頁 | 網頁工作階段反向、模式選擇(think/standard) |
|
||||
| `KieExecutor` | KIE | 自訂令牌簽發,含輪換的工作階段錨點 |
|
||||
| `KiroExecutor` | AWS CodeWhisperer/Kiro | AWS EventStream 二進位格式 → SSE 轉換 |
|
||||
| `MuseSparkWebExecutor` | Muse Spark(網頁) | 含圖片訊息橋接的網頁工作階段反向 |
|
||||
| `NlpCloudExecutor` | NLP Cloud | 提供者特定的請求主體形式 |
|
||||
| `OpenCodeExecutor` | OpenCode | AI SDK 相容提供者設定 |
|
||||
| `PerplexityWebExecutor` | Perplexity 網頁 | 用於聊天延續的網頁工作階段反向 |
|
||||
| `PetalsExecutor` | Petals 分散式推理 | 去中心化群組路由 |
|
||||
| `PollinationsExecutor` | Pollinations AI | 無需 API 金鑰、速率限制請求 |
|
||||
| `QoderExecutor` | Qoder AI | PAT 與 OAuth 支援、多模型免費方案 |
|
||||
| `VertexExecutor` | Google Vertex AI | 服務帳戶驗證、基於區域的端點 |
|
||||
| `DevinDesktopExecutor` | Devin Desktop | 匯入的 API 金鑰 + Connect-protobuf 聊天串流 |
|
||||
| 執行器 | 提供者 | 特殊處理 |
|
||||
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
|
||||
| `DefaultExecutor` | OpenAI、Claude、Gemini、Qwen、OpenRouter、GLM、Kimi、MiniMax、DeepSeek、Groq、xAI、Mistral、Perplexity、Together、Fireworks、Cerebras、Cohere、NVIDIA 等 | 每個提供者的動態 URL/標頭設定 |
|
||||
| `AntigravityExecutor` | Google Antigravity | 自訂專案/工作階段 ID、Retry-After 解析、429 混淆 |
|
||||
| `AzureOpenAIExecutor` | Azure OpenAI | 基於部署的路由、api-version 查詢強制 |
|
||||
| `BlackboxWebExecutor` | Blackbox AI(網頁模式) | 含 TLS 指紋模擬的網頁工作階段反向 |
|
||||
| `ClaudeIdentityExecutor` | Claude.ai(CCH 路徑) | 約束 + 工具重新對應管線、指紋塑造 |
|
||||
| `CliProxyApiExecutor` | CLIProxyAPI 相容提供者 | 自訂驗證與協定處理 |
|
||||
| `CloudflareAiExecutor` | Cloudflare Workers AI | 帳戶 ID 注入、基於 Neurons 的用量追蹤 |
|
||||
| `CodexExecutor` | OpenAI Codex | 注入系統指令、強制推理努力 |
|
||||
| `ChatGptWebCodexExecutor` | ChatGPT Web (Codex) | 瀏覽器工作階段 Responses API 橋接與執行緒/輪次固定 |
|
||||
| `CommandCodeExecutor` | Command Code | OAuth + 每個工作階段的標頭輪換 |
|
||||
| `CursorExecutor` | Cursor IDE | ConnectRPC 協定、Protobuf 編碼、透過 checksum 的請求簽署 |
|
||||
| `DevinCliExecutor` | Devin CLI | 透過雲端代理模組的 Devin 任務生命週期橋接 |
|
||||
| `GithubExecutor` | GitHub Copilot | Copilot 令牌刷新、模擬 VSCode 標頭 |
|
||||
| `GitlabExecutor` | GitLab Duo | GitLab OAuth + 專案範圍路由 |
|
||||
| `GlmExecutor` | Z.AI GLM(含 `glmt` 預設) | 思考預算感知、GLMT 預設常數 |
|
||||
| `GrokWebExecutor` | xAI Grok 網頁 | 網頁工作階段反向、模式選擇(think/standard) |
|
||||
| `KieExecutor` | KIE | 自訂令牌簽發,含輪換的工作階段錨點 |
|
||||
| `KiroExecutor` | AWS CodeWhisperer/Kiro | AWS EventStream 二進位格式 → SSE 轉換 |
|
||||
| `MuseSparkWebExecutor` | Muse Spark(網頁) | 含圖片訊息橋接的網頁工作階段反向 |
|
||||
| `NlpCloudExecutor` | NLP Cloud | 提供者特定的請求主體形式 |
|
||||
| `OpenCodeExecutor` | OpenCode | AI SDK 相容提供者設定 |
|
||||
| `PerplexityWebExecutor` | Perplexity 網頁 | 用於聊天延續的網頁工作階段反向 |
|
||||
| `PetalsExecutor` | Petals 分散式推理 | 去中心化群組路由 |
|
||||
| `PollinationsExecutor` | Pollinations AI | 無需 API 金鑰、速率限制請求 |
|
||||
| `QoderExecutor` | Qoder AI | PAT 與 OAuth 支援、多模型免費方案 |
|
||||
| `VertexExecutor` | Google Vertex AI | 服務帳戶驗證、基於區域的端點 |
|
||||
| `DevinDesktopExecutor` | Devin Desktop | 匯入的 API 金鑰 + Connect-protobuf 聊天串流 |
|
||||
|
||||
所有其他提供者(包括自訂相容節點)使用 `DefaultExecutor`。
|
||||
|
||||
## 提供者相容性矩陣
|
||||
|
||||
> **注意:** 以下矩陣為 OmniRoute v3.8.0 中 237 個已註冊提供者的代表性樣本。
|
||||
> **注意:** 以下矩陣為 OmniRoute v3.8.0 中 351 個已註冊提供者的代表性樣本。
|
||||
> 完整且持續更新的清單,請參閱
|
||||
> [`docs/reference/PROVIDER_REFERENCE.md`](../reference/PROVIDER_REFERENCE.md)(自動產生)或
|
||||
> `src/shared/constants/providers.ts`(載入時經 Zod 驗證)中的權威來源。
|
||||
|
||||
| 提供者 | 格式 | 驗證 | 串流 | 非串流 | 令牌刷新 | 用量 API |
|
||||
| ------------------ | ---------------- | --------------------- | ---------------- | ------ | -------- | ---------------- |
|
||||
| Claude | claude | API 金鑰 / OAuth | ✅ | ✅ | ✅ | ⚠️ 僅管理員 |
|
||||
| Gemini | gemini | API 金鑰 / OAuth | ✅ | ✅ | ✅ | ⚠️ Cloud Console |
|
||||
| Antigravity | antigravity | OAuth | ✅ | ✅ | ✅ | ✅ 完整配額 API |
|
||||
| OpenAI | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Codex | openai-responses | OAuth | ✅ 強制 | ❌ | ✅ | ✅ 速率限制 |
|
||||
| GitHub Copilot | openai | OAuth + Copilot 令牌 | ✅ | ✅ | ✅ | ✅ 配額快照 |
|
||||
| Cursor | cursor | 自訂 checksum | ✅ | ✅ | ❌ | ❌ |
|
||||
| Kiro | kiro | AWS SSO OIDC | ✅ (EventStream) | ❌ | ✅ | ✅ 用量限制 |
|
||||
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ 每次請求 |
|
||||
| Kilo Code | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| Cline | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| Kimi Coding | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| OpenRouter | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| GLM/Kimi/MiniMax | claude | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| DeepSeek | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Groq | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| xAI(Grok) | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Mistral | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Perplexity | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Together AI | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Fireworks AI | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cerebras | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cohere | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| NVIDIA NIM | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cloudflare AI | openai | API 令牌 + 帳戶 ID | ✅ | ✅ | ❌ | ❌ |
|
||||
| Pollinations | openai | 無(無需金鑰) | ✅ | ✅ | ❌ | ❌ |
|
||||
| Scaleway AI | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| LongCat | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Ollama Cloud | openai | API 金鑰(選用) | ✅ | ✅ | ❌ | ❌ |
|
||||
| HuggingFace | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Nebius | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| SiliconFlow | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Hyperbolic | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Vertex AI | gemini | 服務帳戶 | ✅ | ✅ | ✅ | ⚠️ Cloud Console |
|
||||
| Command Code | openai | OAuth | ✅ | ✅ | ✅ | ⚠️ 每次請求 |
|
||||
| Z.AI / GLM | openai | API 金鑰 / OAuth | ✅ | ✅ | ❌ | ❌ |
|
||||
| GLMT(預設) | claude | API 金鑰 | ✅ | ✅ | ❌ | ⚠️ 每次請求 |
|
||||
| Kimi Coding | openai | OAuth / API 金鑰 | ✅ | ✅ | ✅ | ❌ |
|
||||
| KIE | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Devin Desktop | openai | 匯入的 API 金鑰 | ✅ (Connect→SSE) | ✅ | ❌ | ⚠️ 每次請求 |
|
||||
| GitLab Duo | openai | OAuth(GitLab) | ✅ | ✅ | ✅ | ❌ |
|
||||
| Devin CLI | openai | 本機 CLI 登入 | ✅ | ✅ | ❌ | ✅ 任務 API |
|
||||
| Codex Cloud | openai-responses | OAuth | ✅ | ❌ | ✅ | ✅ 速率限制 |
|
||||
| Jules | openai | OAuth | ✅ | ✅ | ✅ | ✅ 任務 API |
|
||||
| AgentRouter | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| ChatGPT-Web | openai | 工作階段 Cookie + TLS | ✅ | ✅ | ❌ | ❌ |
|
||||
| Grok-Web | openai | 工作階段 Cookie | ✅ | ✅ | ❌ | ❌ |
|
||||
| Perplexity-Web | openai | 工作階段 Cookie | ✅ | ✅ | ❌ | ❌ |
|
||||
| BlackBox-Web | openai | 工作階段 Cookie + TLS | ✅ | ✅ | ❌ | ❌ |
|
||||
| Muse-Spark-Web | openai | 工作階段 Cookie | ✅ | ✅ | ❌ | ❌ |
|
||||
| ModelScope | openai | API 金鑰 | ✅ | ✅ | ❌ | ⚠️ 配額政策 |
|
||||
| BazaarLink | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Petals | openai | 無 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ 每次請求 |
|
||||
| OpenCode(Go/Zen) | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| CLIProxyAPI | openai | 自訂 | ✅ | ✅ | ❌ | ❌ |
|
||||
| 提供者 | 格式 | 驗證 | 串流 | 非串流 | 令牌刷新 | 用量 API |
|
||||
| ------------------- | ---------------- | --------------------- | ---------------- | ------ | -------- | ---------------- |
|
||||
| Claude | claude | API 金鑰 / OAuth | ✅ | ✅ | ✅ | ⚠️ 僅管理員 |
|
||||
| Gemini | gemini | API 金鑰 / OAuth | ✅ | ✅ | ✅ | ⚠️ Cloud Console |
|
||||
| Antigravity | antigravity | OAuth | ✅ | ✅ | ✅ | ✅ 完整配額 API |
|
||||
| OpenAI | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Codex | openai-responses | OAuth | ✅ 強制 | ❌ | ✅ | ✅ 速率限制 |
|
||||
| ChatGPT Web (Codex) | openai-responses | 瀏覽器工作階段 | ✅ 強制 | ❌ | ❌ | ❌ |
|
||||
| GitHub Copilot | openai | OAuth + Copilot 令牌 | ✅ | ✅ | ✅ | ✅ 配額快照 |
|
||||
| Cursor | cursor | 自訂 checksum | ✅ | ✅ | ❌ | ❌ |
|
||||
| Kiro | kiro | AWS SSO OIDC | ✅ (EventStream) | ❌ | ✅ | ✅ 用量限制 |
|
||||
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ 每次請求 |
|
||||
| Kilo Code | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| Cline | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| Kimi Coding | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| OpenRouter | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| GLM/Kimi/MiniMax | claude | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| DeepSeek | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Groq | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| xAI(Grok) | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Mistral | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Perplexity | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Together AI | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Fireworks AI | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cerebras | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cohere | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| NVIDIA NIM | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Cloudflare AI | openai | API 令牌 + 帳戶 ID | ✅ | ✅ | ❌ | ❌ |
|
||||
| Pollinations | openai | 無(無需金鑰) | ✅ | ✅ | ❌ | ❌ |
|
||||
| Scaleway AI | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| LongCat | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Ollama Cloud | openai | API 金鑰(選用) | ✅ | ✅ | ❌ | ❌ |
|
||||
| HuggingFace | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Nebius | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| SiliconFlow | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Hyperbolic | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Vertex AI | gemini | 服務帳戶 | ✅ | ✅ | ✅ | ⚠️ Cloud Console |
|
||||
| Command Code | openai | OAuth | ✅ | ✅ | ✅ | ⚠️ 每次請求 |
|
||||
| Z.AI / GLM | openai | API 金鑰 / OAuth | ✅ | ✅ | ❌ | ❌ |
|
||||
| GLMT(預設) | claude | API 金鑰 | ✅ | ✅ | ❌ | ⚠️ 每次請求 |
|
||||
| Kimi Coding | openai | OAuth / API 金鑰 | ✅ | ✅ | ✅ | ❌ |
|
||||
| KIE | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Devin Desktop | openai | 匯入的 API 金鑰 | ✅ (Connect→SSE) | ✅ | ❌ | ⚠️ 每次請求 |
|
||||
| GitLab Duo | openai | OAuth(GitLab) | ✅ | ✅ | ✅ | ❌ |
|
||||
| Devin CLI | openai | 本機 CLI 登入 | ✅ | ✅ | ❌ | ✅ 任務 API |
|
||||
| Codex Cloud | openai-responses | OAuth | ✅ | ❌ | ✅ | ✅ 速率限制 |
|
||||
| Jules | openai | OAuth | ✅ | ✅ | ✅ | ✅ 任務 API |
|
||||
| AgentRouter | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Grok-Web | openai | 工作階段 Cookie | ✅ | ✅ | ❌ | ❌ |
|
||||
| Perplexity-Web | openai | 工作階段 Cookie | ✅ | ✅ | ❌ | ❌ |
|
||||
| BlackBox-Web | openai | 工作階段 Cookie + TLS | ✅ | ✅ | ❌ | ❌ |
|
||||
| Muse-Spark-Web | openai | 工作階段 Cookie | ✅ | ✅ | ❌ | ❌ |
|
||||
| ModelScope | openai | API 金鑰 | ✅ | ✅ | ❌ | ⚠️ 配額政策 |
|
||||
| BazaarLink | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Petals | openai | 無 | ✅ | ✅ | ❌ | ❌ |
|
||||
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ 每次請求 |
|
||||
| OpenCode(Go/Zen) | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
|
||||
| CLIProxyAPI | openai | 自訂 | ✅ | ✅ | ❌ | ❌ |
|
||||
|
||||
## 格式轉換覆蓋範圍
|
||||
|
||||
|
||||
@@ -229,7 +229,6 @@ v1/
|
||||
├── audio/{speech, transcriptions}/ TTS + STT
|
||||
├── batches/[id]/{cancel}、batches/ OpenAI Batches API
|
||||
├── chat/completions/ 聊天補全(主要端點)
|
||||
├── chatgpt-web/ ChatGPT-Web 相容層
|
||||
├── completions/ 舊版文字補全
|
||||
├── embeddings/ 嵌入向量
|
||||
├── files/[id]/、files/ 檔案 API
|
||||
@@ -476,16 +475,16 @@ open-sse/
|
||||
|
||||
### 4.2 `open-sse/executors/`
|
||||
|
||||
84 個提供者執行器,每個都繼承 `BaseExecutor`(`base.ts`):
|
||||
107 個提供者執行器,每個都繼承 `BaseExecutor`(`base.ts`):
|
||||
|
||||
`antigravity`、`azure-openai`、`blackbox-web`、`chatgpt-web`、`cliproxyapi`、
|
||||
`cloudflare-ai`、`codex`、`commandCode`、`cursor`、`default`、`devin-cli`、
|
||||
`antigravity`、`azure-openai`、`blackbox-web`、`cliproxyapi`、
|
||||
`chatgpt-web-codex`、`cloudflare-ai`、`codex`、`commandCode`、`cursor`、`default`、`devin-cli`、
|
||||
`muse-spark-web`、`nlpcloud`、`opencode`、`perplexity-web`、`petals`、
|
||||
`pollinations`、`qoder`、`vertex`、`windsurf`,加上 `claudeIdentity.ts`
|
||||
(共用身分識別輔助程式)和 `index.ts`(註冊表)。
|
||||
|
||||
> 注意:未列在此處的提供者由 `default.ts` 使用通用的
|
||||
> 與 OpenAI 相容的執行器處理。完整的 329 項提供者目錄位於
|
||||
> 與 OpenAI 相容的執行器處理。完整的 351 項提供者目錄位於
|
||||
> `src/shared/constants/providers.ts`。
|
||||
|
||||
### 4.3 `open-sse/translator/`
|
||||
|
||||
@@ -1,166 +1,61 @@
|
||||
---
|
||||
title: "Providers — ChatGPT Web (session credentials via Cookie Editor)"
|
||||
title: "Providers — ChatGPT Web (Codex)"
|
||||
version: 3.8.50
|
||||
lastUpdated: 2026-08-08
|
||||
lastUpdated: 2026-08-26
|
||||
---
|
||||
|
||||
# Providers — ChatGPT Web (Plus/Pro session credentials)
|
||||
# Providers — ChatGPT Web (Codex)
|
||||
|
||||
`chatgpt-web` (alias `cgpt-web`, display name **ChatGPT Web (Plus/Pro)**) sends OpenAI-format chat requests through an authenticated `chatgpt.com` browser session. It authenticates with the `__Secure-next-auth.session-token` cookie — **no API key required**.
|
||||
`chatgpt-web-codex` (alias `cgpt-codex`) bridges Codex Responses turns through an
|
||||
authenticated ChatGPT browser session. It is independent from the retired common
|
||||
`chatgpt-web` provider and uses the MIT-noticed implementation under
|
||||
`open-sse/vendor/codex-chatgpt-web/`.
|
||||
|
||||
> **New to Web Cookie providers?**
|
||||
>
|
||||
> Read **`docs/getting-started/WEB-COOKIE-GUIDE.md`** for the general setup process, limitations, and troubleshooting before following this provider-specific guide.
|
||||
## Common provider retirement
|
||||
|
||||
---
|
||||
The former common provider IDs `chatgpt-web` and `cgpt-web` no longer ship because the
|
||||
provenance of their pre-key/proof-of-work implementation could not be cleared. Explicit
|
||||
requests to either ID, including slash-prefixed model IDs and persisted aliases, fail
|
||||
closed with HTTP `410` and code **PROVIDER_RETIRED** before any upstream request.
|
||||
|
||||
## 1. What credential does OmniRoute need?
|
||||
Migration `163_retire_chatgpt_web.sql` tombstones matching provider connections and
|
||||
invalidates their active session leases. It preserves connection history and API-key
|
||||
allowlists; it does not add replacement access to an allowlist. The Codex provider and
|
||||
its connections are not matched by this retirement.
|
||||
|
||||
Defined in `src/shared/constants/providers/web-cookie.ts` + `src/shared/providers/webSessionCredentials.ts`:
|
||||
|
||||
| Field | Value |
|
||||
| -------------------------- | ----------------------------------------------------------------------------- |
|
||||
| Provider id | `chatgpt-web` |
|
||||
| Credential name | `__Secure-next-auth.session-token` |
|
||||
| Accepts full Cookie header | ✅ yes |
|
||||
| Accepted storage keys | `cookie`, `sessionToken`, `session-token`, `__Secure-next-auth.session-token` |
|
||||
|
||||
Two paste formats both work:
|
||||
|
||||
- **Bare value** — just the token contents: `eyJhbGciOi...`
|
||||
- **Full Cookie header** — `__Secure-next-auth.session-token=eyJhbGciOi...; cf_clearance=...` (preferred — carries rotation/anti-bot cookies the executor needs)
|
||||
|
||||
---
|
||||
|
||||
## 2. Copy the cookie header with Cookie Editor
|
||||
|
||||
Cookie Editor can copy the cookies for the active `chatgpt.com` tab as an HTTP header string.
|
||||
Always compare the exported value with a live authenticated request as described in section 3.
|
||||
|
||||
### 2.1 Install and pin
|
||||
|
||||
1. Install **[Cookie-Editor](https://chromewebstore.google.com/detail/cookie-editor/hlkenndednhfkekhgcdicdfddnkalmdm)** (Moustachauve) in Chrome/Edge, or the Firefox equivalent.
|
||||
2. Pin it to the toolbar if you use it regularly.
|
||||
|
||||
### 2.2 Copy the credential
|
||||
|
||||
1. Go to **https://chatgpt.com** and make sure you're **signed in with the Plus/Pro account** you want OmniRoute to use.
|
||||
2. Open a conversation and send at least one message (forces the session token to be live/refreshed).
|
||||
3. Click the **Cookie Editor** icon to open its side panel for the active tab.
|
||||
4. Find `__Secure-next-auth.session-token`. If it's split into chunks (`__Secure-next-auth.session-token.0`, `.1`, …), select **all** of them — OmniRoute's `nextAuthCookie.ts` merges rotated chunk families.
|
||||
5. Click **Copy**, choose **Header string**, and copy the resulting `name=value; name=value` text.
|
||||
|
||||
> **If the token is missing:** confirm that you are signed in, send a message to refresh the session, and inspect the live request in section 3.
|
||||
|
||||
---
|
||||
|
||||
## 3. Verify the required data (before pasting)
|
||||
|
||||
The repo's `WEB-COOKIE-GUIDE.md` mandates a live-request check. Do it once per session:
|
||||
|
||||
1. With chatgpt.com open, press **F12** → **Network** tab.
|
||||
2. Refresh the page, then send a chat message.
|
||||
3. Click the conversation request (e.g. `/backend-api/conversation` or the SSE stream) → **Headers** → **Request Headers** → **Cookie**.
|
||||
4. Confirm it contains `__Secure-next-auth.session-token=...` — **not** just `cf_clearance` or `__cf_bm`.
|
||||
|
||||
The value you copied in step 2.3 must match what the live request sends. If they differ, re-copy from Cookie Editor.
|
||||
|
||||
---
|
||||
|
||||
## 4. Add / update the credential in OmniRoute
|
||||
|
||||
### Dashboard (typical user path)
|
||||
|
||||
1. Open the OmniRoute dashboard → **Providers** → **Add Provider**.
|
||||
2. Search **ChatGPT Web (Plus/Pro)** (id `chatgpt-web`).
|
||||
3. Paste the copied cookie header into the credential field.
|
||||
4. Click **Test Connection**.
|
||||
5. Save.
|
||||
|
||||
If requests later return 401 or 403, re-copy the header from a fresh live session. The executor merges `Set-Cookie` rotations while the connection is active, but it cannot recover a credential that is no longer accepted upstream.
|
||||
|
||||
### Bulk / session pools (many accounts)
|
||||
|
||||
For multiple ChatGPT sessions, use the bulk web-session import or session-pool endpoints:
|
||||
|
||||
- `POST /api/providers/bulk-web-session` — import many cookie credentials at once
|
||||
- `GET /api/session-pools` + `/api/session-pools/[provider]` — pool rotation across accounts
|
||||
|
||||
Each credential blob must carry the `__Secure-next-auth.session-token` value under one of the accepted storage keys (`cookie`, `sessionToken`, `session-token`, or the cookie's exact name).
|
||||
|
||||
### Renewing when the session expires
|
||||
|
||||
Web sessions can stop working after sign-out or server-side rotation. Re-run steps 2.2 through 4 whenever requests start failing with 401/403.
|
||||
|
||||
---
|
||||
|
||||
## 5. Contributing updates
|
||||
|
||||
If you changed the credential contract (new storage key, new cookie name, changed hint) or are filling the docs gap, contribute it:
|
||||
|
||||
1. Update `src/shared/providers/webSessionCredentials.ts` (credential name / placeholder / storage keys) or `src/shared/constants/providers/web-cookie.ts` (`authHint`).
|
||||
2. Update this guide (`docs/providers/CHATGPT_WEB.md`) and the provider table in `docs/getting-started/WEB-COOKIE-GUIDE.md`.
|
||||
3. Update `.env.example` + `docs/reference/ENVIRONMENT.md` if you touched env vars, then run:
|
||||
```bash
|
||||
node scripts/check/check-env-doc-sync.mjs # must pass
|
||||
```
|
||||
4. Run the provider/unit tests:
|
||||
```bash
|
||||
npm run test:unit
|
||||
# targeted: tests/unit/chatgpt-web.test.ts (stealth path)
|
||||
```
|
||||
5. Follow `CONTRIBUTING.md`, branch from the current active release tip, use a Conventional Commit message, and open the PR against that active release branch.
|
||||
|
||||
> ⚠️ **Never commit a real cookie value.** All examples above are placeholders. If a test fixture needs a token, use a fake `eyJhbGciOi...` string.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
| -------------------------------- | -------------------------------------------- | --------------------------------------------------------- |
|
||||
| Cookie not in Cookie Editor | Signed out / not HttpOnly-visible | Sign in; enable HttpOnly display in options |
|
||||
| Token missing from live request | Request is not authenticated | Sign in and send a chat message first |
|
||||
| 401 after Test Connection passed | Expired or rotated session | Re-copy from a fresh live request |
|
||||
| Chunked token fails | Only one chunk pasted | Select all `__Secure-next-auth.session-token.*` chunks |
|
||||
|
||||
---
|
||||
|
||||
## ChatGPT Web (Codex)
|
||||
|
||||
`ChatGPT Web (Codex)` is an additional provider. The existing
|
||||
`ChatGPT Web (Plus/Pro)` provider described above stays unchanged for regular
|
||||
chats, images, and its existing tool emulation.
|
||||
|
||||
### Prerequisites
|
||||
## Prerequisites
|
||||
|
||||
- a full Cookie header from a signed-in ChatGPT session;
|
||||
- Chrome or Chromium for npm, systemd, and PM2 installs;
|
||||
- with the Docker `web` profile, the internal Chromium service from `docker-compose.yml`;
|
||||
- with the Docker `web` profile, the internal Chromium service from
|
||||
`docker-compose.yml`;
|
||||
- an OpenAI tunnel and a ChatGPT custom connector for local Codex tools.
|
||||
|
||||
The tunnel is only needed for tool turns. `pro` is read-only and does not need a
|
||||
local tool connector.
|
||||
The tunnel is only needed for tool turns. The `pro` model is read-only and does not need
|
||||
a local tool connector.
|
||||
|
||||
### Dashboard setup
|
||||
## Dashboard setup
|
||||
|
||||
1. Open the **ChatGPT Web (Codex)** provider and add a connection.
|
||||
2. Paste the full ChatGPT cookie, the tunnel ID, the runtime key, and the name of
|
||||
the custom connector.
|
||||
3. Start the check. OmniRoute opens a headless Temporary Chat and also detects
|
||||
2. Paste the full ChatGPT Cookie header, tunnel ID, runtime key, and custom connector
|
||||
name.
|
||||
3. Run the connection check. OmniRoute opens a headless Temporary Chat and detects
|
||||
whether `pro` is available for the account.
|
||||
4. Save the connection. OmniRoute replaces the pasted cookie with the verified
|
||||
Playwright storage state and stores it together with the runtime key through
|
||||
the encrypted credential abstraction.
|
||||
Playwright storage state and stores it with the runtime key through the encrypted
|
||||
credential abstraction.
|
||||
|
||||
The raw cookie is not retained after a successful save. When the session expires,
|
||||
open the connection, paste a fresh full cookie, and re-run the check. The doctor
|
||||
status in the edit dialog reports browser, storage state, sign-in, Temporary
|
||||
Chat, tunnel, connector, and tool round-trip separately.
|
||||
The raw cookie is not retained after a successful save. When the session expires, open
|
||||
the connection, paste a fresh full Cookie header, and rerun the check. The doctor status
|
||||
in the edit dialog reports browser, storage state, sign-in, Temporary Chat, tunnel,
|
||||
connector, and tool round-trip separately.
|
||||
|
||||
### Models and combos
|
||||
> Never commit a real cookie, runtime key, storage state, or capability token. Test and
|
||||
> documentation values must always be placeholders.
|
||||
|
||||
The fixed models are:
|
||||
## Models and combos
|
||||
|
||||
The fixed model routes are:
|
||||
|
||||
- `chatgpt-web-codex/instant`
|
||||
- `chatgpt-web-codex/medium`
|
||||
@@ -168,54 +63,71 @@ The fixed models are:
|
||||
- `chatgpt-web-codex/extra-high`
|
||||
- `chatgpt-web-codex/pro`
|
||||
|
||||
Add one of them to a combo like any other model. The Codex app sends only the
|
||||
combo name as `model` to the regular Responses endpoint `/v1/responses`. There is
|
||||
no special endpoint and no Codex-mode switch.
|
||||
Add one of them to a combo like any other model. The Codex app sends the combo name as
|
||||
`model` to the regular Responses endpoint, `/v1/responses`; there is no separate Codex
|
||||
endpoint or mode switch.
|
||||
|
||||
`pro` does not run local tools. A forced tool makes that combo target
|
||||
incompatible; with optional tools the turn runs read-only and reports that
|
||||
limitation as commentary.
|
||||
`pro` does not run local tools. A forced tool makes that combo target incompatible. With
|
||||
optional tools, the turn runs read-only and reports the limitation as commentary.
|
||||
|
||||
### Security model
|
||||
## Security model
|
||||
|
||||
- The native path requires a Responses request, a recognized Codex client, and
|
||||
matching thread and turn identities.
|
||||
- Workspace, sandbox, approval policy, and the tool catalog come from the native
|
||||
Codex shell. Free-form prompt text is not an authority for them.
|
||||
- ChatGPT receives only a short-lived capability per turn. The MCP broker accepts
|
||||
only tools that Codex offered in exactly that turn.
|
||||
- Auto-confirming "Allow once" only returns the tool request to Codex. Codex
|
||||
alone decides on approval and execution.
|
||||
- Before the first output, the combo may fall back to another compatible target.
|
||||
After that, provider, model, connection, and browser turn stay pinned until the
|
||||
- The native path requires a Responses request, a recognized Codex client, and matching
|
||||
thread and turn identities.
|
||||
- Workspace, sandbox, approval policy, and tool catalog come from the native Codex shell;
|
||||
free-form prompt text is not authority for them.
|
||||
- ChatGPT receives only a short-lived capability per turn. The MCP broker accepts only
|
||||
tools Codex offered in that exact turn.
|
||||
- Auto-confirming **Allow once** only returns the tool request to Codex. Codex alone
|
||||
decides on approval and execution.
|
||||
- Before the first output, a combo may fall back to another compatible target. After
|
||||
output begins, provider, model, connection, and browser turn remain pinned until the
|
||||
turn completes.
|
||||
- Cookies, runtime keys, storage state, and capability tokens do not appear in
|
||||
provider responses or request logs.
|
||||
- Cookies, runtime keys, storage state, and capability tokens do not appear in provider
|
||||
responses or request logs.
|
||||
|
||||
### Headless VPS and Docker
|
||||
## Headless VPS and Docker
|
||||
|
||||
For npm, systemd, and PM2 installs, OmniRoute detects common Chrome and Chromium
|
||||
paths. Alternatively, set `CHATGPT_WEB_CODEX_CHROME_PATH`.
|
||||
For npm, systemd, and PM2 installs, OmniRoute detects common Chrome and Chromium paths.
|
||||
Alternatively, set `CHATGPT_WEB_CODEX_CHROME_PATH`.
|
||||
|
||||
The Docker `web` profile starts `chatgpt-web-codex-browser` on the internal
|
||||
Compose network. Its CDP port is not published on the host. The protected profile
|
||||
volume stays separate from the OmniRoute data volume, and the browser gets enough
|
||||
shared memory. The internal CDP proxy listens only on the Compose network on port
|
||||
`9223`; Chrome itself stays bound to loopback inside the sidecar.
|
||||
The Docker `web` profile starts `chatgpt-web-codex-browser` on the internal Compose
|
||||
network. Its CDP port is not published on the host. The protected browser profile volume
|
||||
is separate from the OmniRoute data volume, and the browser receives enough shared
|
||||
memory. The internal CDP proxy listens only on port `9223` inside the Compose network;
|
||||
Chrome remains bound to loopback in the sidecar.
|
||||
|
||||
A supervisor lease under `DATA_DIR` prevents multiple OmniRoute processes from
|
||||
owning the same tunnel and broker state. A conflict shows up in the doctor.
|
||||
A supervisor lease under `DATA_DIR` prevents multiple OmniRoute processes from owning
|
||||
the same tunnel and broker state. A conflict is reported by the doctor.
|
||||
|
||||
### Interactive recovery
|
||||
## Interactive recovery
|
||||
|
||||
The normal path is fully headless. When ChatGPT demands an interactive sign-in or
|
||||
challenge, the existing VNC browser infrastructure can be used as a recovery
|
||||
path. Browser UI and CDP must then only be reachable over loopback, an
|
||||
authenticated management connection, or an SSH tunnel; noVNC stays disabled in
|
||||
normal operation.
|
||||
The normal path is headless. When ChatGPT requires an interactive sign-in or challenge,
|
||||
the existing VNC browser infrastructure can be used for recovery. Browser UI and CDP
|
||||
must remain reachable only over loopback, an authenticated management connection, or an
|
||||
SSH tunnel; noVNC stays disabled during normal operation.
|
||||
|
||||
### WebSocket fallback
|
||||
## WebSocket fallback
|
||||
|
||||
When a combo contains `ChatGPT Web (Codex)`, the Responses WebSocket bridge
|
||||
requests the HTTP/SSE fallback before connecting upstream. The actual transfer
|
||||
then goes through `/v1/responses`.
|
||||
When a combo contains ChatGPT Web (Codex), the Responses WebSocket bridge requests the
|
||||
HTTP/SSE fallback before connecting upstream. The transfer then goes through
|
||||
`/v1/responses`.
|
||||
|
||||
## Verification
|
||||
|
||||
Run the provider controls without invoking the retired provider:
|
||||
|
||||
```bash
|
||||
node --import tsx/esm --test \\
|
||||
tests/unit/chatgpt-web-codex.test.ts \\
|
||||
tests/unit/chatgpt-web-codex-turn-pin.test.ts \\
|
||||
tests/unit/chatgpt-web-environment-double-unescape.test.ts
|
||||
```
|
||||
|
||||
Retirement regression guards live in:
|
||||
|
||||
- `tests/unit/chatgpt-web-retirement.test.ts`
|
||||
- `tests/unit/chatgpt-web-runtime-block.test.ts`
|
||||
- `tests/unit/chatgpt-web-image-handler-retirement.test.ts`
|
||||
- `tests/unit/chatgpt-web-source-retirement.test.ts`
|
||||
- `tests/unit/migration-163-retire-chatgpt-web.test.ts`
|
||||
|
||||
@@ -305,14 +305,10 @@ OmniRoute provides a two-layer defense: request-side injection scanning and resp
|
||||
| `NEXT_PUBLIC_BASE_URL` | `http://localhost:20128` | OAuth, Dashboard, sync | Public-facing URL for OAuth redirect_uri, Dashboard links, and generated public URLs. Set this to the stable public URL when OAuth callbacks or generated browser links must use a canonical reverse-proxy host. |
|
||||
| `NEXT_PUBLIC_CLOUD_URL` | _(empty)_ | Client-side | Client-side mirror of `CLOUD_URL`. |
|
||||
| `NEXT_PUBLIC_APP_URL` | _(unset)_ | `src/shared/services/cloudSyncScheduler.ts` | Legacy fallback for `NEXT_PUBLIC_BASE_URL`. |
|
||||
| `OMNIROUTE_PUBLIC_BASE_URL` | _(unset)_ | Public-origin resolver, image URLs | Highest-priority browser-facing OmniRoute origin used for public URL generation and non-dashboard browser-origin validation (for example `/v1/chatgpt-web/image/<id>`). Set this when OpenWebUI or another relay reaches OmniRoute by an internal URL but the user's browser must fetch images from a LAN, tunnel, or public origin. Do **not** include `/v1`. |
|
||||
| `OMNIROUTE_PUBLIC_BASE_URL` | _(unset)_ | Public-origin resolver, image URLs | Highest-priority browser-facing OmniRoute origin used for public URL generation and non-dashboard browser-origin validation. Set this when OpenWebUI or another relay reaches OmniRoute by an internal URL but the user's browser must fetch generated media from a LAN, tunnel, or public origin. Do **not** include `/v1`. |
|
||||
| `OMNIROUTE_PROVIDER_MANIFEST_URL` | _(unset)_ | `open-sse/config/providerPluginManifestUrl.ts` | Absolute provider plugin manifest URL advertised to sidecar clients. When unset, OmniRoute derives `/api/v1/provider-plugin-manifest` from request origin or HOST/PORT. |
|
||||
| `OMNIROUTE_PUBLIC_PROTOCOL` | `http` | `open-sse/config/providerPluginManifestUrl.ts` | Protocol used when deriving the provider plugin manifest URL from HOST/PORT without a request origin. Set to `https` behind a TLS-terminating public proxy when no explicit `OMNIROUTE_PROVIDER_MANIFEST_URL` is set. |
|
||||
| `OMNIROUTE_TRUST_PROXY` | _(unset)_ | `src/server/origin/publicOrigin.ts` | Optional trust mode for forwarded public-origin headers. Unset = do not trust `Forwarded` / `X-Forwarded-*` for security decisions. `true` / `loopback` trusts forwarded host/proto only from a token-stamped loopback proxy. `private` / `lan` also trusts private-LAN proxy peers. Prefer explicit `NEXT_PUBLIC_BASE_URL` in production. |
|
||||
| `OMNIROUTE_CGPT_WEB_IMAGE_TIMEOUT_MS` | `180000` (3 min) | `open-sse/executors/chatgpt-web.ts` | Max wait time for an async chatgpt-web image to land via the celsius WebSocket. Increase during upstream queue-deep windows. |
|
||||
| `OMNIROUTE_CGPT_WEB_IMAGE_CACHE_MAX_MB` | `256` | `open-sse/services/chatgptImageCache.ts` | Total in-memory byte budget (MB) for the chatgpt-web image cache serving `/v1/chatgpt-web/image/<id>`. Lower on memory-constrained hosts; raise if image generation is heavy and clients race the 30-minute TTL. |
|
||||
| `OMNIROUTE_CGPT_WEB_PRO_TIMEOUT_MS` | `1200000` (20 min) | `open-sse/executors/chatgpt-web.ts` | Overall wait budget for a chatgpt-web GPT-5.5 Pro background-poll handoff. Pro reasoning runs complete out-of-band, so OmniRoute polls until the answer lands or this budget elapses. Raise if Pro requests time out before finishing. |
|
||||
| `OMNIROUTE_CGPT_WEB_PRO_POLL_INTERVAL_MS` | `4000` (4s) | `open-sse/executors/chatgpt-web.ts` | Interval between chatgpt-web GPT-5.5 Pro background-poll attempts. Lower for snappier completion at the cost of more upstream polling; raise to reduce request volume. |
|
||||
| `THEOLDLLM_NAV_TIMEOUT_MS` | `30000` (30s) | `open-sse/executors/theoldllm.ts` | Playwright navigation timeout (ms) for the browser-backed token capture used by the The Old LLM (theoldllm) free provider. Raise on slow networks if the relay page is slow to settle. |
|
||||
| `KIE_CALLBACK_URL` | _(unset)_ | `open-sse/utils/kieTask.ts` | Public callback URL for asynchronous kie.ai jobs. Highest-priority override before `OMNIROUTE_KIE_CALLBACK_URL` and `OMNIROUTE_PUBLIC_URL`. |
|
||||
| `OMNIROUTE_KIE_CALLBACK_URL` | _(unset)_ | `open-sse/utils/kieTask.ts` | Alternate spelling of `KIE_CALLBACK_URL`. Falls back when the primary variable is unset. |
|
||||
@@ -768,9 +764,6 @@ REQUEST_TIMEOUT_MS (global override)
|
||||
| `OMNIROUTE_PROVIDER_PROBE_TIMEOUT_MS` | `8000` | Timeout (ms) for the `validationRead` and `modelsProbe` presets in `src/shared/network/safeOutboundFetch.ts`. Raise for slow endpoints (Cerebras, Cloudflare AI, Groq) to prevent flapping between active/error in the dashboard. Falls back to 8000ms for invalid (<1000) or non-numeric values. |
|
||||
| `OMNIROUTE_RELAY_FETCH_TIMEOUT_MS` | `25000` | Relay-specific fetch timeout in `open-sse/utils/proxyFetch.ts` (#9158). A hung relay must fail before the client/agent timeout (~30s) so callers see a relay-specific failure instead of a generic upstream timeout. Capped at `29000` so it always fires first. |
|
||||
| `OMNIROUTE_RETRY_BACKOFF_MS` | `10` | Shared retry backoff for the direct/relay/proxy retry-once paths in `open-sse/utils/proxyFetch.ts` (#9158). `0` = retry immediately. |
|
||||
| `OMNIROUTE_CHATGPT_TLS_TIMEOUT_MS` | `60000` | Wire-level timeout for the bogdanfinn/tls-client koffi binding (`chatgptTlsClient.ts`). |
|
||||
| `OMNIROUTE_CHATGPT_TLS_GRACE_MS` | `10000` | JS-side grace added on top of the wire timeout when the native binding is wedged. |
|
||||
| `OMNIROUTE_CHATGPT_STREAM_FIRST_BYTE_TIMEOUT_MS` | `30000` (30s) | Max wait for the first streamed byte from the ChatGPT TLS sidecar (`chatgptTlsClient.ts`) before aborting a dead stream. Raise if upstream cold-starts exceed the window. |
|
||||
| `OMNIROUTE_CLAUDE_TLS_TIMEOUT_MS` | `60000` | Wire-level timeout for the bogdanfinn/tls-client koffi binding (`claudeTlsClient.ts`). |
|
||||
| `OMNIROUTE_CLAUDE_TLS_GRACE_MS` | `10000` | JS-side grace added on top of the wire timeout when the native binding is wedged. |
|
||||
| `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` | `30000` | Wire-level timeout for the bogdanfinn/tls-client koffi binding (`perplexityTlsClient.ts`). |
|
||||
@@ -1316,7 +1309,6 @@ Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy),
|
||||
| `DB_BACKUP_MAX_FILES` | `20` | `src/lib/db/backup.ts`, `src/lib/db/migrationRunner.ts` | Maximum SQLite backup files retained on disk. Applies to manual/scheduled backups and to pre-migration snapshots. Overrides the value saved from Settings → Database backup retention. |
|
||||
| `DB_BACKUP_RETENTION_DAYS` | `0` | `src/lib/db/backup.ts`, `src/lib/db/migrationRunner.ts` | Maximum age (days) of retained backups. `0` disables age-based pruning. Applies to manual/scheduled backups and to pre-migration snapshots. Overrides the value saved from Settings → Database backup retention. |
|
||||
| `OMNIROUTE_BACKUP_SCHEDULE_JOB_INTERVAL_MS` | `30000` | `src/lib/jobs/backupScheduleJob.ts` | Tick interval (ms) of the server-side job that executes `backup-schedule.json`. Must stay well under the 1-minute cron granularity; values below `5000` or unparseable fall back to `30000`. |
|
||||
| `OMNIROUTE_TLS_PROXY_URL` | _(unset)_ | `open-sse/services/chatgptTlsClient.ts` | Override the TLS sidecar URL for tests. Production should leave unset. |
|
||||
| `CONTAINER_HOST` | `docker` | `scripts/check-permissions.sh` | Container runtime hint for the entrypoint permission check. Set to `podman` for any Podman topology. Because the container cannot determine whether the engine is local or reached through Podman Machine, the warning stays topology-neutral and points to `contrib/podman/README.md`. |
|
||||
| `QUOTA_STORE_DRIVER` | `sqlite` | `src/lib/quota/storeFactory.ts` | Quota-share consumption store backend: `sqlite` (default) or `redis`. |
|
||||
| `QUOTA_STORE_REDIS_URL` | _(unset)_ | `src/lib/quota/storeFactory.ts` | Redis connection string used when `QUOTA_STORE_DRIVER=redis` (e.g. `redis://localhost:6379`). |
|
||||
|
||||
@@ -82,14 +82,13 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
|
||||
| `zed` | `zd` | Zed IDE | OAuth | [link](https://zed.dev) | Zed stores LLM provider credentials (OpenAI, Anthropic, Google, Mistral, xAI) in the OS keychain. Use the Import button below to discover and import them automatically. |
|
||||
| `zed-hosted` | — | Zed Hosted Models | OAuth | [link](https://zed.dev) | Sign in with your Zed account (native-app sign-in). OmniRoute generates a one-time RSA keypair and opens zed.dev to authorize it — on a remote/headless install, copy the resulting 127.0.0.1 callback URL from your browser's address bar and paste it back here. Distinct from the 'Zed IDE' credential-import entry above: this proxies chat completions through Zed's own hosted model aggregator (cloud.zed.dev), fronting Anthropic/OpenAI/Google/xAI models under your Zed plan. |
|
||||
|
||||
## Web Cookie Providers (35)
|
||||
## Web Cookie Providers (34)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes | Tool calling |
|
||||
|----|-------|------|------|---------|-------|--------------|
|
||||
| `adapta-web` | `adp-web` | Adapta.org (Adapta One Web) | Web cookie | [link](https://agent.adapta.one) | Paste your __client cookie value from .clerk.agent.adapta.one (DevTools → Application → Cookies) | emulated |
|
||||
| `adobe-firefly` | `firefly` | Adobe Firefly (Image/Video) | Web cookie | [link](https://firefly.adobe.com) | RECOMMENDED: firefly.adobe.com signed-in → F12 → Network → click firefly-3p.ff.adobe.io (generate-async or models/discovery) → Request Headers → Authorization → copy the token AFTER 'Bearer ' (starts with eyJ…). Cookie-only from firefly.adobe.com mints a GUEST token → 401/403; only multi-domain IMS cookies (adobelogin.com) or that Bearer JWT work. Unofficial/experimental media + Limits. | — |
|
||||
| `blackbox-web` | `bb-web` | Blackbox Web (Subscription) | Web cookie | [link](https://app.blackbox.ai) | Paste your __Secure-authjs.session-token value or full cookie header from app.blackbox.ai | emulated |
|
||||
| `chatgpt-web` | `cgpt-web` | ChatGPT Web (Plus/Pro) | Web cookie | [link](https://chatgpt.com) | Paste your __Secure-next-auth.session-token cookie value from chatgpt.com | emulated |
|
||||
| `chatgpt-web-codex` | `cgpt-codex` | ChatGPT Web (Codex) | Web cookie | [link](https://chatgpt.com) | Paste the full ChatGPT Cookie header. OmniRoute verifies it in an isolated headless browser profile. | native |
|
||||
| `claude-web` | `cw` | Claude Web | Web cookie | [link](https://claude.ai) | Paste your session cookie from claude.ai | none |
|
||||
| `conol-web` | `cnl` | Conol (Unofficial/Experimental) | Web cookie | [link](https://conol.ai) | Use browser sign-in, or paste the full Cookie header from conol.ai. The __Secure-better-auth.session_token cookie is required. | — |
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 566" role="img" aria-label="OmniRoute free-tier dashboard preview: about 1.51 billion documented recurring tokens per month and about 2.13 billion in the first month. The audited catalog has 40 recurring pool keys and 455 entries, 448 active and 7 discontinued; the chart represents the 20 pools with a published positive monthly token budget. One-time signup credits total about 626 million and include a 10 million LongCat grant that requires KYC. Uncapped providers remain subject to rate, concurrency, account, regional, and policy limits." font-family="-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 566" role="img" aria-label="OmniRoute free-tier dashboard preview: about 1.51 billion documented recurring tokens per month and about 2.13 billion in the first month. The audited catalog has 40 recurring pool keys and 454 entries, 447 active and 7 discontinued; the chart represents the 20 pools with a published positive monthly token budget. One-time signup credits total about 626 million and include a 10 million LongCat grant that requires KYC. Uncapped providers remain subject to rate, concurrency, account, regional, and policy limits." font-family="-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif">
|
||||
<desc>Static dashboard preview of recurring token pools, first-month signup grants, and uncapped but rate-limited free-access providers.</desc>
|
||||
<rect width="900" height="566" rx="16" fill="#0d1117"/>
|
||||
<rect x="16" y="16" width="868" height="550" rx="13" fill="#161b22" stroke="#30363d"/>
|
||||
<text x="868" y="558" fill="#484f58" font-size="10.5" text-anchor="end">OmniRoute · /dashboard/free-tiers · preview mockup</text>
|
||||
<text x="32" y="50" fill="#e6edf3" font-size="18" font-weight="700">Monthly free-token budget</text>
|
||||
<text x="868" y="50" fill="#7d8590" font-size="13" text-anchor="end">40 recurring pools · 455 catalog entries · one endpoint</text>
|
||||
<text x="868" y="50" fill="#7d8590" font-size="13" text-anchor="end">40 recurring pools · 454 catalog entries · one endpoint</text>
|
||||
<text x="32" y="84" fill="#7d8590" font-size="11.5">Steady / month</text>
|
||||
<text x="32" y="114" fill="#e6edf3" font-size="27" font-weight="800">~1.51B</text>
|
||||
<text x="330" y="84" fill="#7d8590" font-size="11.5">First month (+ signup credits)</text>
|
||||
@@ -34,7 +34,7 @@
|
||||
<rect x="855.7" y="132" width="5.7" height="16" fill="#fdcb6e"/>
|
||||
<rect x="862.3" y="132" width="5.7" height="16" fill="#e84393"/>
|
||||
</g>
|
||||
<text x="32" y="172" fill="#7d8590" font-size="12">Each segment = one of 20 quantified recurring pools · 40 pools / 455 entries in the audited catalog.</text>
|
||||
<text x="32" y="172" fill="#7d8590" font-size="12">Each segment = one of 20 quantified recurring pools · 40 pools / 454 entries in the audited catalog.</text>
|
||||
<circle cx="37" cy="196" r="5" fill="#6c5ce7"/>
|
||||
<text x="48" y="200" fill="#c9d1d9" font-size="12.5">Mistral <tspan fill="#7d8590">1.00B</tspan></text>
|
||||
<circle cx="250" cy="196" r="5" fill="#00b894"/>
|
||||
@@ -101,5 +101,5 @@
|
||||
<text x="299" y="466" fill="#7ee787" font-size="11.5" text-anchor="middle">nscale 5M</text>
|
||||
<rect x="32" y="492" width="836" height="34" rx="8" fill="#1c2230" stroke="#30363d"/>
|
||||
<text x="46" y="506" fill="#7d8590" font-size="12">Pool-deduped, honest counting — no inflated rate-limit ceilings. Some terms suggest personal-use only; we flag them so you decide.</text>
|
||||
<text x="46" y="520" fill="#7d8590" font-size="11.5">+ 14 recurring uncapped* providers (rate/concurrency-limited) · OpenRouter $10 → +24M/mo.</text>
|
||||
<text x="46" y="520" fill="#7d8590" font-size="11.5">+ 13 recurring uncapped* providers (rate/concurrency-limited) · OpenRouter $10 → +24M/mo.</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 8.9 KiB |
@@ -87,7 +87,7 @@ OAuth/subscription scrapers. The resulting terminal state is per **connection**,
|
||||
not per provider.
|
||||
|
||||
That said, the built-in _strings_ are oriented toward subscription/OAuth
|
||||
providers with real ban risk (ChatGPT Web, Claude Web, Codex, Muse Spark,
|
||||
providers with real ban risk (ChatGPT Web Codex, Claude Web, Codex, Muse Spark,
|
||||
Antigravity). An API-key provider will only trip the detector if its error body
|
||||
literally contains one of the substrings.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ lastUpdated: 2026-06-28
|
||||
|
||||
# Stealth Guide
|
||||
|
||||
> **Source of truth:** `open-sse/utils/tlsClient.ts`, `open-sse/services/{chatgptTlsClient,claudeCodeCCH,claudeCodeFingerprint,claudeCodeObfuscation,claudeCodeCompatible}.ts`, `open-sse/config/cliFingerprints.ts`, `src/mitm/`
|
||||
> **Source of truth:** `open-sse/utils/tlsClient.ts`, `open-sse/services/{claudeCodeCCH,claudeCodeFingerprint,claudeCodeObfuscation,claudeCodeCompatible}.ts`, `open-sse/config/cliFingerprints.ts`, `src/mitm/`
|
||||
> **Last updated:** 2026-06-28 — v3.8.40
|
||||
> **Audience:** Engineers maintaining provider-specific stealth integrations.
|
||||
|
||||
@@ -14,7 +14,7 @@ OmniRoute integrates with providers whose edges actively fingerprint non-officia
|
||||
|
||||
## Legal and Ethical Notice
|
||||
|
||||
Stealth features exist so OmniRoute can act as a compatibility layer between user-owned official accounts (Claude Code CLI, ChatGPT Desktop/Web, Antigravity, Cursor, etc.) and OmniRoute's unified API. They are **not** for evading fraud detection, sharing credentials, or violating provider Terms of Service. The maintainers expect operators to comply with the upstream ToS they signed when creating accounts.
|
||||
Stealth features exist so OmniRoute can act as a compatibility layer between user-owned official accounts (Claude Code CLI, Codex, Antigravity, Cursor, etc.) and OmniRoute's unified API. They are **not** for evading fraud detection, sharing credentials, or violating provider Terms of Service. The maintainers expect operators to comply with the upstream ToS they signed when creating accounts.
|
||||
|
||||
---
|
||||
|
||||
@@ -29,20 +29,6 @@ Lazy-loaded `wreq-js` session that impersonates **Chrome 124 on macOS**. Used as
|
||||
- Timeout: `TLS_CLIENT_TIMEOUT_MS` (inherits from `FETCH_TIMEOUT_MS`, default 600000)
|
||||
- `wreq-js` Response is fetch-compatible (`headers`, `text()`, `json()`, `clone()`, `body`).
|
||||
|
||||
### `open-sse/services/chatgptTlsClient.ts` — tls-client-node (Firefox 148)
|
||||
|
||||
Dedicated TLS impersonator for `chatgpt.com`. ChatGPT's Cloudflare config pins `cf_clearance` to JA3/JA4 + HTTP/2 SETTINGS frame ordering — undici's handshake gets `cf-mitigated: challenge` even with valid cookies.
|
||||
|
||||
- Profile: `firefox_148` (must match the Firefox 148 `User-Agent` sent)
|
||||
- Mode: `runtimeMode: "native"` (koffi-loaded shared library; avoids managed sidecar HTTP)
|
||||
- `withRandomTLSExtensionOrder: true`
|
||||
- `tlsFetchChatGpt(url, options)` supports streaming (writes body to temp file, tailed as `ReadableStream`)
|
||||
- Hang detection: `raceWithTimeout` + `TlsClientHangError` triggers `resetClientCache()` so the next call respawns the binding
|
||||
- Proxy resolution (priority): per-call `proxyUrl` → `OMNIROUTE_TLS_PROXY_URL` → `HTTPS_PROXY`/`HTTP_PROXY`/`ALL_PROXY` (the native binding does **not** read these envs itself; it must be threaded through)
|
||||
- Errors: `TlsClientUnavailableError` (binary missing), `TlsClientHangError` (binding deadlocked)
|
||||
|
||||
---
|
||||
|
||||
## Claude Code Stealth Bundle
|
||||
|
||||
When `cliCompatMode` is on, OmniRoute reshapes outgoing Claude requests so they are indistinguishable from `claude-cli` traffic. Three modules collaborate:
|
||||
@@ -253,17 +239,17 @@ OmniRoute scrubs inbound client headers before forwarding so a request that arri
|
||||
2. Extract JA3/JA4 and the literal header order
|
||||
3. Update the relevant `CLI_FINGERPRINTS[...]` entry
|
||||
4. Bump matching `*_USER_AGENT` default in `.env.example`
|
||||
5. If TLS handshake itself changed: update `chatgptTlsClient.ts::CHATGPT_PROFILE` or wreq-js `browser:` option
|
||||
6. Run `chatgptTlsClient.test.ts` and a manual canary against the live provider
|
||||
5. If the TLS handshake itself changed, update the relevant provider wrapper or the wreq-js `browser:` option
|
||||
6. Run the provider-specific TLS tests and a manual canary against the live provider
|
||||
7. Ship in a patch release; document in `CHANGELOG.md`
|
||||
|
||||
---
|
||||
|
||||
## Tests
|
||||
|
||||
- `open-sse/services/__tests__/chatgptTlsClient.test.ts` — proxy resolution priority, abort handling, hang recovery
|
||||
- `open-sse/services/__tests__/claudeTlsClient.test.ts` — shared TLS wrapper behavior
|
||||
- `tests/unit/anthropic-cache-fingerprint.test.ts` — fingerprint determinism
|
||||
- `tests/unit/chatgpt-web.test.ts` — end-to-end stealth path for ChatGPT
|
||||
- `tests/unit/chatgpt-web-source-retirement.test.ts` — common ChatGPT Web stealth source remains absent while Codex Web stays present
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class LoginManager extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Start a login flow for a web-cookie provider.
|
||||
* @param {string} providerId - e.g. "claude-web", "chatgpt-web"
|
||||
* @param {string} providerId - e.g. "claude-web", "perplexity-web"
|
||||
* @param {object} [options]
|
||||
* @param {number} [options.timeout] - Total timeout in ms (default: config or 300s)
|
||||
* @returns {Promise<{success: boolean, credentials?: Record<string, string>, error?: string}>}
|
||||
|
||||
@@ -19,8 +19,6 @@ import type { FreeModelBudget } from "./freeModelCatalog.ts";
|
||||
export const FREE_CATALOG_CURATED_AT = "2026-08-26";
|
||||
|
||||
export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [
|
||||
{ provider: "chatgpt-web", modelId: "gpt-5.6-luna-free", displayName: "GPT-5.6 Luna (Free)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "chatgpt-web-free", tos: "caution" },
|
||||
{ provider: "chatgpt-web", modelId: "gpt-5.6-luna-free-thinking", displayName: "GPT-5.6 Luna (Free, Think)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "chatgpt-web-free", tos: "caution" },
|
||||
{ provider: "agentrouter", modelId: "claude-opus-4-8", displayName: "Claude Opus 4.8", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" },
|
||||
{ provider: "agentrouter", modelId: "claude-opus-5", displayName: "Claude Opus 5", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" },
|
||||
{ provider: "agentrouter", modelId: "gpt-5.6-sol", displayName: "GPT-5.6 Sol", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" },
|
||||
|
||||
@@ -237,17 +237,6 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
|
||||
supportedSizes: ["1024x1024", "1024x1536", "1536x1024"],
|
||||
},
|
||||
|
||||
"chatgpt-web": {
|
||||
id: "chatgpt-web",
|
||||
alias: "cgpt-web",
|
||||
baseUrl: "https://chatgpt.com/backend-api/f/conversation",
|
||||
authType: "apikey",
|
||||
authHeader: "cookie",
|
||||
format: "chatgpt-web",
|
||||
models: [{ id: "gpt-5.5", name: "GPT-5.5 Instant (ChatGPT Web Image)" }],
|
||||
supportedSizes: ["1024x1024", "1024x1536", "1536x1024"],
|
||||
},
|
||||
|
||||
// #10466: Gemini Web session image generation (Nano Banana). Same
|
||||
// web-cookie transport as the gemini-web chat provider — the handler
|
||||
// drives the session executor in image mode and extracts the generated
|
||||
@@ -925,7 +914,10 @@ export function parseImageModel(modelStr) {
|
||||
|
||||
// No provider prefix — try to find the model in every provider, excluding cookie-auth (web) bridges
|
||||
for (const [providerId, config] of Object.entries(IMAGE_PROVIDERS)) {
|
||||
if (config.authHeader !== "cookie" && (config.routingAliases?.includes(modelStr) || config.models.some((m) => m.id === modelStr))) {
|
||||
if (
|
||||
config.authHeader !== "cookie" &&
|
||||
(config.routingAliases?.includes(modelStr) || config.models.some((m) => m.id === modelStr))
|
||||
) {
|
||||
return { provider: providerId, model: modelStr };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,6 @@ import { blackboxProvider } from "./registry/blackbox/index.ts";
|
||||
import { blackbox_webProvider } from "./registry/blackbox/web/index.ts";
|
||||
import { uncloseaiProvider } from "./registry/uncloseai/index.ts";
|
||||
import { nscaleProvider } from "./registry/nscale/index.ts";
|
||||
import { chatgpt_webProvider } from "./registry/chatgpt-web/index.ts";
|
||||
import { chatgpt_web_codexProvider } from "./registry/chatgpt-web-codex/index.ts";
|
||||
import { openrouterProvider } from "./registry/openrouter/index.ts";
|
||||
import { cheaperinferenceProvider } from "./registry/cheaperinference/index.ts";
|
||||
@@ -393,7 +392,6 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
"blackbox-web": blackbox_webProvider,
|
||||
uncloseai: uncloseaiProvider,
|
||||
nscale: nscaleProvider,
|
||||
"chatgpt-web": chatgpt_webProvider,
|
||||
"chatgpt-web-codex": chatgpt_web_codexProvider,
|
||||
openrouter: openrouterProvider,
|
||||
cheaperinference: cheaperinferenceProvider,
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const chatgpt_webProvider: RegistryEntry = {
|
||||
id: "chatgpt-web",
|
||||
alias: "cgpt-web",
|
||||
format: "openai",
|
||||
executor: "chatgpt-web",
|
||||
baseUrl: "https://chatgpt.com/backend-api/conversation",
|
||||
authType: "apikey",
|
||||
authHeader: "cookie",
|
||||
models: [
|
||||
{
|
||||
id: "gpt-5.6-sol-pro",
|
||||
name: "GPT-5.6 Sol (Pro)",
|
||||
liveCatalogIds: ["gpt-5-6-pro"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.6-sol-xhigh",
|
||||
name: "GPT-5.6 Sol (Xhigh)",
|
||||
liveCatalogIds: ["gpt-5-6-thinking"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.6-sol-high",
|
||||
name: "GPT-5.6 Sol (High)",
|
||||
liveCatalogIds: ["gpt-5-6-thinking"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.6-sol-medium",
|
||||
name: "GPT-5.6 Sol (Medium)",
|
||||
liveCatalogIds: ["gpt-5-6-thinking"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.6-sol-instant",
|
||||
name: "GPT-5.6 Sol (Instant)",
|
||||
liveCatalogIds: ["gpt-5-6"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.6-luna-free-thinking",
|
||||
name: "GPT-5.6 Luna (Free, Think)",
|
||||
liveCatalogIds: ["gpt-5-6"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.6-luna-free",
|
||||
name: "GPT-5.6 Luna (Free)",
|
||||
liveCatalogIds: ["gpt-5-6"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.5-pro-extended",
|
||||
name: "GPT-5.5 (Pro Extended)",
|
||||
liveCatalogIds: ["gpt-5-5-pro"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.5-pro",
|
||||
name: "GPT-5.5 (Pro)",
|
||||
liveCatalogIds: ["gpt-5-5-pro"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.5-xhigh",
|
||||
name: "GPT-5.5 (Xhigh)",
|
||||
liveCatalogIds: ["gpt-5-5-thinking"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.5-high",
|
||||
name: "GPT-5.5 (High)",
|
||||
liveCatalogIds: ["gpt-5-5-thinking"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.5-medium",
|
||||
name: "GPT-5.5 (Medium)",
|
||||
liveCatalogIds: ["gpt-5-5-thinking"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.5-instant",
|
||||
name: "GPT-5.5 (Instant)",
|
||||
liveCatalogIds: ["gpt-5-5"],
|
||||
toolCalling: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,395 +0,0 @@
|
||||
// Pure ChatGPT-web citation-marker parsing/rendering, extracted verbatim from
|
||||
// chatgpt-web.ts (no module state — safe to unit test in isolation).
|
||||
//
|
||||
// Strip ChatGPT's internal entity/citation markup. The browser renders these
|
||||
// private-use markers (for example `citeturn0search0`) with metadata from
|
||||
// `message.metadata.content_references`; API clients need plain Markdown with
|
||||
// real links instead of raw ChatGPT UI tokens.
|
||||
// entity["city","Paris","capital of France"] → Paris
|
||||
// entity["…","value", …] → value
|
||||
const ENTITY_RE = /entity\["[^"]*","([^"]*)"[^\]]*\]/g;
|
||||
const CHATGPT_MARKER_START = "\uE200";
|
||||
const CHATGPT_MARKER_SEP = "\uE202";
|
||||
const CHATGPT_MARKER_END = "\uE201";
|
||||
const CHATGPT_REF_TOKEN_RE = /turn\d+(?:search|product|news|image|webpage)\d+/g;
|
||||
|
||||
type ChatGptCitationSource = {
|
||||
title: string;
|
||||
url: string;
|
||||
attribution: string;
|
||||
};
|
||||
|
||||
type ChatGptCitationMention = {
|
||||
start?: number;
|
||||
end?: number;
|
||||
markerText?: string;
|
||||
replacement: string;
|
||||
};
|
||||
|
||||
type ChatGptCitationData = {
|
||||
sources: ChatGptCitationSource[];
|
||||
mentions: ChatGptCitationMention[];
|
||||
refTokenToSourceNumber: Map<string, number>;
|
||||
};
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" ? (value as Record<string, unknown>) : null;
|
||||
}
|
||||
|
||||
function asString(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim() ? value : null;
|
||||
}
|
||||
|
||||
function asNumber(value: unknown): number | null {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function asArray(value: unknown): unknown[] {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
function markdownLinkText(value: string): string {
|
||||
// Escape the backslash first — otherwise a label ending in (or containing) a
|
||||
// backslash leaks past the `[…]` escaping and breaks the generated link, e.g.
|
||||
// `[Path C:\](url)` where the trailing `\` escapes the closing bracket.
|
||||
return value
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/\[/g, "\\[")
|
||||
.replace(/\]/g, "\\]")
|
||||
.replace(/\n/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function markdownUrl(value: string): string {
|
||||
return value.replace(/\(/g, "%28").replace(/\)/g, "%29");
|
||||
}
|
||||
|
||||
function canonicalCitationUrl(value: string): string {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
url.searchParams.delete("utm_source");
|
||||
return url.toString();
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function referenceUrls(ref: Record<string, unknown>): string[] {
|
||||
const urls: string[] = [];
|
||||
for (const key of ["url", "safe_url", "link"]) {
|
||||
const url = asString(ref[key]);
|
||||
if (url) urls.push(url);
|
||||
}
|
||||
for (const url of asArray(ref.safe_urls)) {
|
||||
if (typeof url === "string" && url.trim()) urls.push(url);
|
||||
}
|
||||
return [...new Set(urls)];
|
||||
}
|
||||
|
||||
function refTokenFromStructuredRef(ref: Record<string, unknown>): string | null {
|
||||
const turn = asNumber(ref.turn_index);
|
||||
const refType = asString(ref.ref_type);
|
||||
const refIndex = asNumber(ref.ref_index);
|
||||
if (turn == null || refIndex == null || !refType) return null;
|
||||
return `turn${turn}${refType}${refIndex}`;
|
||||
}
|
||||
|
||||
function mapStructuredRefs(
|
||||
refs: unknown,
|
||||
sourceNumber: number,
|
||||
refTokenToSourceNumber: Map<string, number>
|
||||
): void {
|
||||
for (const refValue of asArray(refs)) {
|
||||
const ref = asRecord(refValue);
|
||||
if (!ref) continue;
|
||||
const token = refTokenFromStructuredRef(ref);
|
||||
if (token && !refTokenToSourceNumber.has(token)) {
|
||||
refTokenToSourceNumber.set(token, sourceNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatCitationLinks(numbers: number[], sources: ChatGptCitationSource[]): string {
|
||||
return [...new Set(numbers)]
|
||||
.sort((a, b) => a - b)
|
||||
.map((num) => {
|
||||
const source = sources[num - 1];
|
||||
return source ? `[${num}](${markdownUrl(source.url)})` : "";
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function urlMarkerLabel(markerText?: string | null): string | null {
|
||||
if (!markerText) return null;
|
||||
const privateMatch = markerText.match(/\uE200url\uE202([^\uE201\uE202]+)/u);
|
||||
if (privateMatch?.[1]) return privateMatch[1].trim();
|
||||
const plainMatch = markerText.match(/^url[:\s]+(.+)$/i);
|
||||
return plainMatch?.[1]?.trim() || null;
|
||||
}
|
||||
|
||||
function citationMarkerCandidates(markerText?: string): string[] {
|
||||
if (!markerText) return [];
|
||||
const candidates = [markerText];
|
||||
const tokens = markerText.match(CHATGPT_REF_TOKEN_RE) ?? [];
|
||||
if (tokens.length > 0 && markerText.includes("cite")) {
|
||||
candidates.push(
|
||||
`${CHATGPT_MARKER_START}cite${tokens.map((token) => CHATGPT_MARKER_SEP + token).join("")}${CHATGPT_MARKER_END}`
|
||||
);
|
||||
}
|
||||
return [...new Set(candidates)];
|
||||
}
|
||||
|
||||
type AddCitationSourceFn = (
|
||||
titleValue: unknown,
|
||||
urlValue: unknown,
|
||||
attributionValue?: unknown
|
||||
) => number;
|
||||
type AddCitationMentionFn = (ref: Record<string, unknown>, replacement: string) => void;
|
||||
|
||||
/** Supporting-website sources nested under one `grouped_webpages` item. */
|
||||
function collectSupportingWebsiteNumbers(
|
||||
item: Record<string, unknown>,
|
||||
addSource: AddCitationSourceFn,
|
||||
refTokenToSourceNumber: Map<string, number>
|
||||
): number[] {
|
||||
const numbers: number[] = [];
|
||||
for (const supportingValue of asArray(item.supporting_websites)) {
|
||||
const supporting = asRecord(supportingValue);
|
||||
if (!supporting) continue;
|
||||
const supportingNumber = addSource(supporting.title, supporting.url, supporting.attribution);
|
||||
if (supportingNumber) {
|
||||
numbers.push(supportingNumber);
|
||||
mapStructuredRefs(supporting.refs, supportingNumber, refTokenToSourceNumber);
|
||||
}
|
||||
}
|
||||
return numbers;
|
||||
}
|
||||
|
||||
/** One `grouped_webpages` item — its own primary source plus any supporting-website
|
||||
* sources nested under it. */
|
||||
function collectGroupedWebpageItemNumbers(
|
||||
itemValue: unknown,
|
||||
addSource: AddCitationSourceFn,
|
||||
refTokenToSourceNumber: Map<string, number>
|
||||
): number[] {
|
||||
const item = asRecord(itemValue);
|
||||
if (!item) return [];
|
||||
const numbers: number[] = [];
|
||||
const mainNumber = addSource(item.title, item.url, item.attribution);
|
||||
if (mainNumber) {
|
||||
numbers.push(mainNumber);
|
||||
mapStructuredRefs(item.refs, mainNumber, refTokenToSourceNumber);
|
||||
}
|
||||
numbers.push(...collectSupportingWebsiteNumbers(item, addSource, refTokenToSourceNumber));
|
||||
return numbers;
|
||||
}
|
||||
|
||||
/** Fallback when no `grouped_webpages` item yielded a usable source — fall back to
|
||||
* the ref's own URLs directly. */
|
||||
function collectGroupedWebpagesFallbackNumbers(
|
||||
ref: Record<string, unknown>,
|
||||
addSource: AddCitationSourceFn
|
||||
): number[] {
|
||||
const numbers: number[] = [];
|
||||
for (const url of referenceUrls(ref)) {
|
||||
const fallbackNumber = addSource(ref.title, url, ref.attribution);
|
||||
if (fallbackNumber) numbers.push(fallbackNumber);
|
||||
}
|
||||
return numbers;
|
||||
}
|
||||
|
||||
/** `content_references[].type === "grouped_webpages"` — a primary source per item,
|
||||
* each optionally paired with supporting-website sources; falls back to the ref's
|
||||
* own URLs when no item yielded a usable source. */
|
||||
function collectGroupedWebpagesRef(
|
||||
ref: Record<string, unknown>,
|
||||
sources: ChatGptCitationSource[],
|
||||
addSource: AddCitationSourceFn,
|
||||
addMention: AddCitationMentionFn,
|
||||
refTokenToSourceNumber: Map<string, number>
|
||||
): void {
|
||||
let numbers: number[] = [];
|
||||
for (const itemValue of asArray(ref.items)) {
|
||||
numbers.push(...collectGroupedWebpageItemNumbers(itemValue, addSource, refTokenToSourceNumber));
|
||||
}
|
||||
|
||||
if (numbers.length === 0) {
|
||||
numbers = collectGroupedWebpagesFallbackNumbers(ref, addSource);
|
||||
}
|
||||
|
||||
addMention(ref, formatCitationLinks(numbers, sources));
|
||||
}
|
||||
|
||||
/** `content_references[].type === "sources_footnote"` — a flat list of sources with
|
||||
* no inline mention to replace (the footnote itself carries no marker text). */
|
||||
function collectSourcesFootnoteRef(
|
||||
ref: Record<string, unknown>,
|
||||
addSource: AddCitationSourceFn
|
||||
): void {
|
||||
for (const sourceValue of asArray(ref.sources)) {
|
||||
const source = asRecord(sourceValue);
|
||||
if (source) addSource(source.title, source.url, source.attribution);
|
||||
}
|
||||
}
|
||||
|
||||
/** Any other reference type — a direct `webpage`/`url` marker with an inline label
|
||||
* renders as `[label](url)`; everything else falls back to numbered source links. */
|
||||
function collectDefaultRef(
|
||||
ref: Record<string, unknown>,
|
||||
type: string,
|
||||
sources: ChatGptCitationSource[],
|
||||
addSource: AddCitationSourceFn,
|
||||
addMention: AddCitationMentionFn,
|
||||
refTokenToSourceNumber: Map<string, number>
|
||||
): void {
|
||||
const urls = referenceUrls(ref);
|
||||
const label = urlMarkerLabel(asString(ref.matched_text));
|
||||
if ((type === "webpage" || type === "url") && label && urls[0]) {
|
||||
addMention(ref, `[${markdownLinkText(label)}](${markdownUrl(urls[0])})`);
|
||||
return;
|
||||
}
|
||||
|
||||
const numbers = urls
|
||||
.map((url) => addSource(ref.title ?? ref.alt, url, ref.attribution))
|
||||
.filter((num) => num > 0);
|
||||
if (numbers.length === 0) return;
|
||||
|
||||
mapStructuredRefs(ref.refs, numbers[0], refTokenToSourceNumber);
|
||||
addMention(ref, formatCitationLinks(numbers, sources));
|
||||
}
|
||||
|
||||
function collectChatGptCitationData(metadata?: Record<string, unknown>): ChatGptCitationData {
|
||||
const refs = asArray(metadata?.content_references);
|
||||
const sources: ChatGptCitationSource[] = [];
|
||||
const mentions: ChatGptCitationMention[] = [];
|
||||
const sourceIndexByCanonicalUrl = new Map<string, number>();
|
||||
const refTokenToSourceNumber = new Map<string, number>();
|
||||
|
||||
const addSource: AddCitationSourceFn = (titleValue, urlValue, attributionValue) => {
|
||||
const url = asString(urlValue);
|
||||
if (!url) return 0;
|
||||
const canonical = canonicalCitationUrl(url);
|
||||
const existing = sourceIndexByCanonicalUrl.get(canonical);
|
||||
if (existing) return existing;
|
||||
|
||||
const title = asString(titleValue) ?? url;
|
||||
const attribution = asString(attributionValue) ?? "";
|
||||
const idx = sources.length + 1;
|
||||
sources.push({ title: title.replace(/\n/g, " ").trim(), url, attribution });
|
||||
sourceIndexByCanonicalUrl.set(canonical, idx);
|
||||
return idx;
|
||||
};
|
||||
|
||||
const addMention: AddCitationMentionFn = (ref, replacement) => {
|
||||
if (!replacement) return;
|
||||
const start = asNumber(ref.start_idx);
|
||||
const end = asNumber(ref.end_idx);
|
||||
const markerText = asString(ref.matched_text) ?? undefined;
|
||||
if (markerText || (start != null && end != null)) {
|
||||
mentions.push({
|
||||
...(start != null ? { start } : {}),
|
||||
...(end != null ? { end } : {}),
|
||||
...(markerText ? { markerText } : {}),
|
||||
replacement,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
for (const refValue of refs) {
|
||||
const ref = asRecord(refValue);
|
||||
if (!ref) continue;
|
||||
const type = asString(ref.type) ?? "";
|
||||
|
||||
if (type === "grouped_webpages") {
|
||||
collectGroupedWebpagesRef(ref, sources, addSource, addMention, refTokenToSourceNumber);
|
||||
continue;
|
||||
}
|
||||
if (type === "sources_footnote") {
|
||||
collectSourcesFootnoteRef(ref, addSource);
|
||||
continue;
|
||||
}
|
||||
collectDefaultRef(ref, type, sources, addSource, addMention, refTokenToSourceNumber);
|
||||
}
|
||||
|
||||
return { sources, mentions, refTokenToSourceNumber };
|
||||
}
|
||||
|
||||
function replacePrivateCitationMarkers(text: string, citationData: ChatGptCitationData): string {
|
||||
const replaceTokens = (tokens: string[]): string => {
|
||||
const numbers = tokens
|
||||
.map((token) => citationData.refTokenToSourceNumber.get(token))
|
||||
.filter((num): num is number => typeof num === "number");
|
||||
return numbers.length > 0 ? formatCitationLinks(numbers, citationData.sources) : "";
|
||||
};
|
||||
|
||||
return text
|
||||
.replace(/\uE200cite((?:\uE202[^\uE201\uE202]+)+)\uE201/gu, (_all, body: string) => {
|
||||
const tokens = [...body.matchAll(/\uE202([^\uE201\uE202]+)/gu)].map((match) => match[1]);
|
||||
return replaceTokens(tokens);
|
||||
})
|
||||
.replace(
|
||||
/\bcite((?:turn\d+(?:search|product|news|image|webpage)\d+)+)\b/g,
|
||||
(_all, body: string) => {
|
||||
return replaceTokens(body.match(CHATGPT_REF_TOKEN_RE) ?? []);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function stripDanglingChatGptMarkers(text: string, citationData: ChatGptCitationData): string {
|
||||
return replacePrivateCitationMarkers(text, citationData)
|
||||
.replace(
|
||||
/\uE200url\uE202([^\uE201\uE202]+)\uE202(https?:\/\/[^\uE201]+)\uE201/gu,
|
||||
(_all, label: string, url: string) => {
|
||||
return `[${markdownLinkText(label)}](${markdownUrl(url)})`;
|
||||
}
|
||||
)
|
||||
.replace(
|
||||
/\uE200url\uE202([^\uE201\uE202]+)\uE202(?:[^\uE201]*\uE201)?/gu,
|
||||
(_all, label: string) => {
|
||||
return label.trim();
|
||||
}
|
||||
)
|
||||
.replace(/\uE200cite(?:\uE202[^\uE201\uE202]*)*$/gu, "")
|
||||
.replace(/\uE200[a-z_]+(?:\uE202[^\uE201\uE202]*)*\uE201/giu, "")
|
||||
.replace(/\uE200[a-z_]+(?:\uE202[^\uE201\uE202]*)*$/giu, "")
|
||||
.replace(/\uE202?turn\d+(?:search|product|news|image|webpage)\d+\uE201?/gu, "")
|
||||
.replace(/[\uE200\uE201\uE202]/gu, "");
|
||||
}
|
||||
|
||||
function applyChatGptCitations(text: string, metadata?: Record<string, unknown>): string {
|
||||
const citationData = collectChatGptCitationData(metadata);
|
||||
let rendered = text;
|
||||
|
||||
for (const mention of [...citationData.mentions].sort(
|
||||
(a, b) => (b.start ?? -1) - (a.start ?? -1)
|
||||
)) {
|
||||
let replaced = false;
|
||||
for (const markerText of citationMarkerCandidates(mention.markerText)) {
|
||||
const limit =
|
||||
mention.start != null
|
||||
? Math.min(rendered.length, mention.start + markerText.length)
|
||||
: rendered.length;
|
||||
let pos = rendered.lastIndexOf(markerText, limit);
|
||||
if (pos < 0) pos = rendered.indexOf(markerText);
|
||||
if (pos >= 0) {
|
||||
rendered =
|
||||
rendered.slice(0, pos) + mention.replacement + rendered.slice(pos + markerText.length);
|
||||
replaced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!replaced && mention.start != null && mention.end != null) {
|
||||
const start = Math.max(0, Math.min(mention.start, rendered.length));
|
||||
const end = Math.max(start, Math.min(mention.end, rendered.length));
|
||||
rendered = rendered.slice(0, start) + mention.replacement + rendered.slice(end);
|
||||
}
|
||||
}
|
||||
|
||||
return stripDanglingChatGptMarkers(rendered, citationData);
|
||||
}
|
||||
|
||||
export function cleanChatGptText(text: string, metadata?: Record<string, unknown>): string {
|
||||
return applyChatGptCitations(text.replace(ENTITY_RE, "$1"), metadata);
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
import { tlsFetchChatGpt } from "../../services/chatgptTlsClient.ts";
|
||||
|
||||
const CONVERSATION_RESUME_URL = "https://chatgpt.com/backend-api/f/conversation/resume";
|
||||
const RESUME_OFFSETS = [0, 1, 2] as const;
|
||||
|
||||
export interface FinalAssistantAnswer {
|
||||
text: string;
|
||||
messageId?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
finished: boolean;
|
||||
}
|
||||
|
||||
interface HandoffContentChunk {
|
||||
answer?: string;
|
||||
messageId?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
type HandoffContentReader = (
|
||||
eventStream: ReadableStream<Uint8Array>,
|
||||
signal?: AbortSignal | null
|
||||
) => AsyncIterable<HandoffContentChunk>;
|
||||
|
||||
interface ResumeHandoffOptions {
|
||||
conversationId: string;
|
||||
resumeToken: string;
|
||||
headers: Record<string, string>;
|
||||
timeoutMs: number;
|
||||
signal?: AbortSignal | null;
|
||||
log?: { warn?: (tag: string, message: string) => void } | null;
|
||||
readContent: HandoffContentReader;
|
||||
}
|
||||
|
||||
interface ResumeAttemptOptions extends Pick<
|
||||
ResumeHandoffOptions,
|
||||
"conversationId" | "timeoutMs" | "signal" | "log" | "readContent"
|
||||
> {
|
||||
offset: (typeof RESUME_OFFSETS)[number];
|
||||
resumeHeaders: Record<string, string>;
|
||||
}
|
||||
|
||||
interface ResumeAttemptResult {
|
||||
answer: FinalAssistantAnswer | null;
|
||||
shouldRetry: boolean;
|
||||
}
|
||||
|
||||
function stringToStream(text: string): ReadableStream<Uint8Array> {
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
return new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(bytes);
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function readFinalAssistantAnswer(
|
||||
eventStream: ReadableStream<Uint8Array>,
|
||||
signal: AbortSignal | null | undefined,
|
||||
readContent: HandoffContentReader
|
||||
): Promise<FinalAssistantAnswer | null> {
|
||||
let text = "";
|
||||
let messageId: string | undefined;
|
||||
let metadata: Record<string, unknown> | undefined;
|
||||
|
||||
for await (const chunk of readContent(eventStream, signal)) {
|
||||
if (chunk.error) return null;
|
||||
if (chunk.answer) text = chunk.answer;
|
||||
if (chunk.messageId) messageId = chunk.messageId;
|
||||
if (chunk.metadata) metadata = chunk.metadata;
|
||||
}
|
||||
|
||||
if (!text.trim()) return null;
|
||||
return { text, messageId, metadata, finished: true };
|
||||
}
|
||||
|
||||
async function attemptResumeOffset({
|
||||
conversationId,
|
||||
offset,
|
||||
resumeHeaders,
|
||||
timeoutMs,
|
||||
signal,
|
||||
log,
|
||||
readContent,
|
||||
}: ResumeAttemptOptions): Promise<ResumeAttemptResult> {
|
||||
try {
|
||||
const response = await tlsFetchChatGpt(CONVERSATION_RESUME_URL, {
|
||||
method: "POST",
|
||||
headers: resumeHeaders,
|
||||
body: JSON.stringify({ conversation_id: conversationId, offset }),
|
||||
timeoutMs,
|
||||
signal,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
if (response.status === 404) return { answer: null, shouldRetry: true };
|
||||
if (response.status >= 400) {
|
||||
log?.warn?.(
|
||||
"CGPT-WEB",
|
||||
`conversation resume ${response.status}: ${(response.text || "").slice(0, 300)}`
|
||||
);
|
||||
return { answer: null, shouldRetry: false };
|
||||
}
|
||||
|
||||
const eventStream = response.body ?? (response.text ? stringToStream(response.text) : null);
|
||||
if (!eventStream) return { answer: null, shouldRetry: true };
|
||||
|
||||
const answer = await readFinalAssistantAnswer(eventStream, signal, readContent);
|
||||
return { answer, shouldRetry: !answer };
|
||||
} catch (error) {
|
||||
log?.warn?.(
|
||||
"CGPT-WEB",
|
||||
`conversation resume failed: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
return { answer: null, shouldRetry: false };
|
||||
}
|
||||
}
|
||||
|
||||
export async function resumeChatGptHandoff({
|
||||
conversationId,
|
||||
resumeToken,
|
||||
headers,
|
||||
timeoutMs,
|
||||
signal,
|
||||
log,
|
||||
readContent,
|
||||
}: ResumeHandoffOptions): Promise<FinalAssistantAnswer | null> {
|
||||
const resumeHeaders = {
|
||||
...headers,
|
||||
Accept: "text/event-stream",
|
||||
"Content-Type": "application/json",
|
||||
"x-conduit-token": resumeToken,
|
||||
"X-OpenAI-Target-Path": "/backend-api/f/conversation/resume",
|
||||
"X-OpenAI-Target-Route": "/backend-api/f/conversation/resume",
|
||||
};
|
||||
|
||||
for (const offset of RESUME_OFFSETS) {
|
||||
const attempt = await attemptResumeOffset({
|
||||
conversationId,
|
||||
resumeHeaders,
|
||||
offset,
|
||||
timeoutMs,
|
||||
signal,
|
||||
log,
|
||||
readContent,
|
||||
});
|
||||
if (attempt.answer) return attempt.answer;
|
||||
if (!attempt.shouldRetry) return null;
|
||||
}
|
||||
|
||||
log?.warn?.("CGPT-WEB", `conversation resume returned no assistant text for ${conversationId}`);
|
||||
return null;
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
// Pure model-mapping / thinking-effort resolution for the ChatGPT-web executor.
|
||||
// Extracted verbatim from chatgpt-web.ts (static maps + pure resolvers, no state).
|
||||
|
||||
export const MODEL_MAP: Record<string, string> = {
|
||||
// ChatGPT backend slugs are also accepted directly for power users / tests.
|
||||
"gpt-5-6": "gpt-5-6",
|
||||
"gpt-5-6-thinking": "gpt-5-6-thinking",
|
||||
"gpt-5-6-pro": "gpt-5-6-pro",
|
||||
"gpt-5-5": "gpt-5-5",
|
||||
"gpt-5-5-thinking": "gpt-5-5-thinking",
|
||||
"gpt-5-5-pro": "gpt-5-5-pro",
|
||||
|
||||
// Free accounts leave Luna selection to ChatGPT's server-side auto router.
|
||||
"gpt-5.6-luna-free": "auto",
|
||||
"gpt-5.6-luna-free-thinking": "auto",
|
||||
|
||||
// Captured from a real ChatGPT v2 picker conversation. The visible
|
||||
// performance levels select distinct backend model/effort pairs.
|
||||
"gpt-5.6-sol-instant": "gpt-5-6",
|
||||
"gpt-5.6-sol-medium": "gpt-5-6-thinking",
|
||||
"gpt-5.6-sol-high": "gpt-5-6-thinking",
|
||||
"gpt-5.6-sol-xhigh": "gpt-5-6-thinking",
|
||||
"gpt-5.6-sol-pro": "gpt-5-6-pro",
|
||||
|
||||
"gpt-5.5-instant": "gpt-5-5",
|
||||
"gpt-5.5-medium": "gpt-5-5-thinking",
|
||||
"gpt-5.5-high": "gpt-5-5-thinking",
|
||||
"gpt-5.5-xhigh": "gpt-5-5-thinking",
|
||||
"gpt-5.5-pro": "gpt-5-5-pro",
|
||||
"gpt-5.5-pro-extended": "gpt-5-5-pro",
|
||||
// Compatibility alias for existing chatgpt-web image integrations. It is
|
||||
// intentionally absent from the provider's visible curated model list.
|
||||
"gpt-5.5": "gpt-5-5",
|
||||
};
|
||||
|
||||
export type ChatGptThinkingEffort = "standard" | "extended" | "max";
|
||||
|
||||
export const MODEL_FORCED_EFFORT: Record<string, ChatGptThinkingEffort | null> = {
|
||||
"gpt-5.6-sol-instant": null,
|
||||
"gpt-5.6-sol-medium": "standard",
|
||||
"gpt-5.6-sol-high": "extended",
|
||||
"gpt-5.6-sol-xhigh": "max",
|
||||
"gpt-5.6-sol-pro": "standard",
|
||||
"gpt-5.5-instant": null,
|
||||
"gpt-5.5-medium": "standard",
|
||||
"gpt-5.5-high": "extended",
|
||||
"gpt-5.5-xhigh": "max",
|
||||
"gpt-5.5-pro": "standard",
|
||||
"gpt-5.5-pro-extended": "extended",
|
||||
};
|
||||
|
||||
const MODEL_SYSTEM_HINTS: Record<string, readonly string[]> = {
|
||||
// Captured from the Free-account Think toggle. ChatGPT sends this both at
|
||||
// the request root and on the user message metadata.
|
||||
"gpt-5.6-luna-free-thinking": ["reason"],
|
||||
};
|
||||
|
||||
export function resolveChatGptSystemHints(model: string): string[] {
|
||||
return [...(MODEL_SYSTEM_HINTS[model] ?? [])];
|
||||
}
|
||||
|
||||
export interface ResolvedChatGptModel {
|
||||
slug: string;
|
||||
effort: ChatGptThinkingEffort | null;
|
||||
isPro: boolean;
|
||||
}
|
||||
|
||||
export function resolveChatGptModel(
|
||||
model: string,
|
||||
_body?: unknown,
|
||||
_providerSpecificData?: Record<string, unknown>
|
||||
): ResolvedChatGptModel {
|
||||
const slug = MODEL_MAP[model] ?? model;
|
||||
const effort = MODEL_FORCED_EFFORT[model] ?? null;
|
||||
const isPro =
|
||||
model === "gpt-5.6-sol-pro" ||
|
||||
model === "gpt-5.5-pro" ||
|
||||
model === "gpt-5.5-pro-extended" ||
|
||||
slug === "gpt-5-6-pro" ||
|
||||
slug === "gpt-5-5-pro";
|
||||
return { slug, effort, isPro };
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* User-facing messages for upstream ChatGPT-web HTTP error statuses.
|
||||
*
|
||||
* Pure mapping with no side effects so it can be unit-tested in isolation — the
|
||||
* caller owns any state mutation (e.g. clearing the token cache on 401/403).
|
||||
* Unmapped statuses fall back to the generic `ChatGPT returned HTTP <status>`.
|
||||
*/
|
||||
const CGPT_WEB_HTTP_ERROR_MESSAGES: Record<number, string> = {
|
||||
401: "ChatGPT auth failed — session may have expired. Re-paste your __Secure-next-auth.session-token.",
|
||||
403: "ChatGPT auth failed — session may have expired. Re-paste your __Secure-next-auth.session-token.",
|
||||
404: "ChatGPT returned 404 — usually the model is no longer available on this account or the chat-requirements-token expired. Retry will start a fresh conversation.",
|
||||
413: "ChatGPT returned 413 — the request payload is too large for ChatGPT web's size limit (often hit by agentic clients like Cline/Kilo that send big system prompts and file context). Reduce the context: enable compression, trim the conversation/files, or use a smaller request.",
|
||||
429: "ChatGPT rate limited. Wait a moment and retry.",
|
||||
};
|
||||
|
||||
export function describeChatGptWebHttpError(status: number): string {
|
||||
return CGPT_WEB_HTTP_ERROR_MESSAGES[status] ?? `ChatGPT returned HTTP ${status}`;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Tool-call emulation helpers for web-cookie executors (#5240, #5927).
|
||||
//
|
||||
// Web-cookie providers (chatgpt-web, perplexity-web, ...) have no native
|
||||
// Web-cookie providers (Perplexity Web, Gemini Web, etc.) may have no native
|
||||
// function calling. When the OpenAI request carries `tools`, the prompt-side
|
||||
// shim (`prepareToolMessages` in ../translator/webTools.ts) injects a `<tool>`
|
||||
// contract; on the response side we parse `<tool>{...}</tool>` blocks back
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Reverse-engineered, unofficial, undocumented endpoint (not a published
|
||||
* Microsoft public API) — the same class of integration this codebase
|
||||
* already accepts for other "-web" style providers (chatgpt-web.ts,
|
||||
* already accepts for other "-web" style providers (perplexity-web.ts,
|
||||
* copilot-web.ts). No user account/API key is required; Microsoft gates
|
||||
* abuse with a `Sec-MS-GEC` header computed from a public "trusted client
|
||||
* token" (see `open-sse/utils/publicCreds.ts::edgetts_token` — Hard Rule
|
||||
|
||||
@@ -442,7 +442,7 @@ export class GeminiWebExecutor extends BaseExecutor {
|
||||
* Google rotated any of the __Secure-1PSID* cookies, forward the merged
|
||||
* cookie string through onCredentialsRefreshed so it gets persisted to the
|
||||
* encrypted provider_connections.api_key field. Mirrors the rotate-and-
|
||||
* persist pattern already shipped in chatgpt-web.ts. A persistence failure
|
||||
* persist pattern used by other rotating-session executors. A persistence failure
|
||||
* must never fail the user-facing response (#7676).
|
||||
*/
|
||||
private async persistRotatedCookies(
|
||||
@@ -640,7 +640,7 @@ export class GeminiWebExecutor extends BaseExecutor {
|
||||
}
|
||||
|
||||
// #10466 image mode: return the captured image URLs to the image
|
||||
// handler via a custom field (same precedent as chatgpt-web's
|
||||
// handler via a custom field (same precedent as other web-session executors'
|
||||
// x_image_resolution_failed). An image-only answer can carry little or
|
||||
// no text, so the empty-text 502 below must not fire when images
|
||||
// were captured.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SEARCH_PROVIDERS } from "../config/searchRegistry.ts";
|
||||
import { assertCommonChatGptWebProviderAvailable } from "@/shared/constants/chatgptWebRetirement";
|
||||
import {
|
||||
registerLazyExecutor,
|
||||
loadRegisteredExecutor,
|
||||
@@ -95,8 +96,6 @@ const lazyExecutors: Record<string, () => Promise<BaseExecutor>> = {
|
||||
import("./gemini-business.ts").then((m) => new m.GeminiBusinessExecutor()),
|
||||
gembiz: () =>
|
||||
import("./gemini-business.ts").then((m) => new m.GeminiBusinessExecutor()), // Alias
|
||||
"chatgpt-web": () => import("./chatgpt-web.ts").then((m) => new m.ChatGptWebExecutor()),
|
||||
"cgpt-web": () => import("./chatgpt-web.ts").then((m) => new m.ChatGptWebExecutor()), // Alias
|
||||
"blackbox-web": () => import("./blackbox-web.ts").then((m) => new m.BlackboxWebExecutor()),
|
||||
"bb-web": () => import("./blackbox-web.ts").then((m) => new m.BlackboxWebExecutor()), // Alias
|
||||
"muse-spark-web": () =>
|
||||
@@ -242,6 +241,8 @@ const CHAT_UNSUPPORTED_CLOUD_AGENT_PROVIDERS = new Set(["jules"]);
|
||||
const CHAT_UNSUPPORTED_SEARCH_PROVIDERS = new Set(Object.keys(SEARCH_PROVIDERS));
|
||||
|
||||
export async function getExecutor(provider: string): Promise<BaseExecutor> {
|
||||
assertCommonChatGptWebProviderAvailable(provider);
|
||||
|
||||
const loaded = await loadRegisteredExecutor(provider);
|
||||
if (loaded) return loaded;
|
||||
if (CHAT_UNSUPPORTED_CLOUD_AGENT_PROVIDERS.has(provider)) {
|
||||
|
||||
@@ -16,10 +16,7 @@ import {
|
||||
import { prepareToolMessages } from "../translator/webTools.ts";
|
||||
import { buildToolModeResponse } from "./chatgptWebTools.ts";
|
||||
import { sanitizeErrorMessage } from "../utils/error.ts";
|
||||
import {
|
||||
buildSessionCookieHeader,
|
||||
mergeRefreshedCookie,
|
||||
} from "../utils/nextAuthCookie.ts";
|
||||
import { buildSessionCookieHeader, mergeRefreshedCookie } from "../utils/nextAuthCookie.ts";
|
||||
import {
|
||||
PPLX_SSE_ENDPOINT,
|
||||
PPLX_USER_AGENT,
|
||||
@@ -362,7 +359,15 @@ export class PerplexityWebExecutor extends BaseExecutor {
|
||||
super("perplexity-web", { id: "perplexity-web", baseUrl: PPLX_SSE_ENDPOINT });
|
||||
}
|
||||
|
||||
async execute({ model, body, stream, credentials, signal, log, onCredentialsRefreshed }: ExecuteInput) {
|
||||
async execute({
|
||||
model,
|
||||
body,
|
||||
stream,
|
||||
credentials,
|
||||
signal,
|
||||
log,
|
||||
onCredentialsRefreshed,
|
||||
}: ExecuteInput) {
|
||||
const bodyObj = (body || {}) as Record<string, unknown>;
|
||||
const rawMessages = bodyObj.messages as Array<Record<string, unknown>> | undefined;
|
||||
if (!rawMessages || !Array.isArray(rawMessages) || rawMessages.length === 0) {
|
||||
@@ -558,7 +563,7 @@ export class PerplexityWebExecutor extends BaseExecutor {
|
||||
}
|
||||
|
||||
// Surface any rotated session-token back to the caller so the DB credential
|
||||
// is refreshed — mirrors chatgpt-web.ts exchangeSession + onCredentialsRefreshed.
|
||||
// is refreshed — mirrors the shared web-session refresh contract.
|
||||
if (cookieBlob) {
|
||||
await persistRotatedSessionCookie(
|
||||
cookieBlob,
|
||||
@@ -575,7 +580,7 @@ export class PerplexityWebExecutor extends BaseExecutor {
|
||||
|
||||
// Tool mode buffers the full completion (no live token streaming) and
|
||||
// converts <tool> text into real tool_calls — even when the caller asked
|
||||
// for a streaming response — mirroring chatgpt-web's toolMode (#5240,
|
||||
// for a streaming response — mirroring the shared tool-mode contract (#5240,
|
||||
// #5927). Without this, streaming requests (the default for agentic
|
||||
// coding clients) never emitted a tool_calls SSE delta.
|
||||
let finalResponse: Response;
|
||||
|
||||
@@ -20,6 +20,7 @@ import { getExecutor } from "../../executors/index.ts";
|
||||
import { isCliproxyapiDeepModeEnabled } from "../../executors/cliproxyapi.ts";
|
||||
import { isDarioDeepModeEnabled } from "../../executors/dario.ts";
|
||||
import { getCachedSettings } from "@/lib/db/readCache";
|
||||
import { assertCommonChatGptWebProviderAvailable } from "@/shared/constants/chatgptWebRetirement";
|
||||
import { getUpstreamProxyConfigCached } from "./comboContextCache.ts";
|
||||
import type { FallbackBackend } from "@/lib/db/upstreamProxy";
|
||||
import { wrapExecutorWithCliproxyapiModelMapping } from "./cliproxyModelMapping.ts";
|
||||
@@ -94,6 +95,8 @@ export async function resolveExecutorWithProxy(
|
||||
log?: LoggerLike,
|
||||
providerSpecificData?: Record<string, unknown> | null
|
||||
) {
|
||||
assertCommonChatGptWebProviderAvailable(prov);
|
||||
|
||||
// Per-connection routing override (#6339): the resolved connection can opt itself
|
||||
// into the CLIProxyAPI passthrough executor via providerSpecificData.cliproxyapiMode
|
||||
// === "claude-native" (UI toggle). This takes precedence over the provider-level
|
||||
@@ -173,7 +176,10 @@ export async function resolveExecutorWithProxy(
|
||||
result = await nativeExec.execute(input);
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
log?.info?.("UPSTREAM_PROXY", `${prov} native error (${errMsg}), retrying via ${backendLabel}`);
|
||||
log?.info?.(
|
||||
"UPSTREAM_PROXY",
|
||||
`${prov} native error (${errMsg}), retrying via ${backendLabel}`
|
||||
);
|
||||
try {
|
||||
return await proxyExec.execute(input);
|
||||
} catch (proxyErr) {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { randomUUID } from "crypto";
|
||||
/** Image generation handler for POST /v1/images/generations (OpenAI-compatible). */
|
||||
|
||||
import {
|
||||
CHATGPT_WEB_RETIRED_ERROR_CODE,
|
||||
CHATGPT_WEB_RETIRED_MESSAGE,
|
||||
isCommonChatGptWebRetiredProviderId,
|
||||
} from "@/shared/constants/chatgptWebRetirement";
|
||||
|
||||
import { getImageProvider, parseImageModel } from "../config/imageRegistry.ts";
|
||||
import { HTTP_STATUS } from "../config/constants.ts";
|
||||
import { applyAntigravityClientProfileHeaders } from "../services/antigravityClientProfile.ts";
|
||||
@@ -8,10 +14,6 @@ import { getAntigravityEnvelopeUserAgent } from "../services/antigravityIdentity
|
||||
import { kieExecutor } from "../executors/kie.ts";
|
||||
import { mapImageSize } from "../translator/image/sizeMapper.ts";
|
||||
import { getCodexClientVersion, getCodexUserAgent } from "../config/codexClient.ts";
|
||||
import { ChatGptWebExecutor } from "../executors/chatgpt-web.ts";
|
||||
import type { ExecutorLog, ProviderCredentials } from "../executors/base.ts";
|
||||
import { getChatGptImage, findChatGptImageBySha256 } from "../services/chatgptImageCache.ts";
|
||||
import { createHash } from "node:crypto";
|
||||
import { saveCallLog } from "@/lib/usageDb";
|
||||
import { sleep } from "../utils/sleep.ts";
|
||||
import {
|
||||
@@ -45,11 +47,6 @@ import { handleIdeogramImageGeneration } from "./imageGeneration/providers/ideog
|
||||
import { handleHaiperImageGeneration } from "./imageGeneration/providers/haiper.ts";
|
||||
import { handleLeonardoImageGeneration } from "./imageGeneration/providers/leonardo.ts";
|
||||
import { handleMagnificImageGeneration } from "./imageGeneration/providers/magnific.ts";
|
||||
import {
|
||||
handleChatGptWebImageGeneration,
|
||||
extractMarkdownImageUrls,
|
||||
CHATGPT_WEB_IMAGE_ID_RE,
|
||||
} from "./imageGeneration/providers/chatgptWeb.ts";
|
||||
import { handleGeminiWebImageGeneration } from "./imageGeneration/providers/geminiWeb.ts";
|
||||
import { handleNvidiaNimImageGeneration } from "./imageGeneration/providers/nvidiaNim.ts";
|
||||
import { handleSegmindImageGeneration } from "./imageGeneration/providers/segmind.ts";
|
||||
@@ -383,6 +380,22 @@ export async function handleImageGeneration({
|
||||
clientHeaders = null,
|
||||
peerLocality = null,
|
||||
}) {
|
||||
const requestedModel = typeof body?.model === "string" ? body.model : "";
|
||||
const slash = requestedModel.indexOf("/");
|
||||
const requestedProvider = slash > 0 ? requestedModel.slice(0, slash) : null;
|
||||
if (
|
||||
isCommonChatGptWebRetiredProviderId(resolvedProvider) ||
|
||||
isCommonChatGptWebRetiredProviderId(requestedProvider) ||
|
||||
isCommonChatGptWebRetiredProviderId(requestedModel)
|
||||
) {
|
||||
return {
|
||||
success: false,
|
||||
status: 410,
|
||||
error: CHATGPT_WEB_RETIRED_MESSAGE,
|
||||
code: CHATGPT_WEB_RETIRED_ERROR_CODE,
|
||||
};
|
||||
}
|
||||
|
||||
let provider, model;
|
||||
|
||||
if (resolvedProvider) {
|
||||
@@ -567,18 +580,6 @@ export async function handleImageGeneration({
|
||||
});
|
||||
}
|
||||
|
||||
if (providerConfig.format === "chatgpt-web") {
|
||||
return handleChatGptWebImageGeneration({
|
||||
model,
|
||||
provider,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
signal,
|
||||
clientHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
// #10466: Gemini Web session image generation (Nano Banana)
|
||||
if (providerConfig.format === "gemini-web") {
|
||||
return handleGeminiWebImageGeneration({
|
||||
@@ -1300,8 +1301,7 @@ async function handleOpenAIImageGeneration({
|
||||
*
|
||||
* Mirrors `handleOpenAIImageGeneration` but posts multipart/form-data to the node's
|
||||
* `/images/edits` endpoint and returns the upstream OpenAI-compatible response. Kept
|
||||
* separate from the chatgpt-web edit flow, which continues a saved conversation node
|
||||
* rather than forwarding a stateless edit. The fetch helper leaves Content-Type unset so
|
||||
* separate from provider-specific hosted-tool flows. The fetch helper leaves Content-Type unset so
|
||||
* `fetch` derives the multipart boundary from the FormData body.
|
||||
*/
|
||||
export async function handleOpenAIImageEdit({
|
||||
@@ -1515,185 +1515,6 @@ export async function handleOpenRouterImageEdit({
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function handleImageEdit({
|
||||
provider,
|
||||
model,
|
||||
body,
|
||||
imageBytes,
|
||||
credentials,
|
||||
log,
|
||||
signal = null,
|
||||
clientHeaders = null,
|
||||
}: {
|
||||
provider: string;
|
||||
model: string;
|
||||
body: Record<string, unknown>;
|
||||
imageBytes: Buffer;
|
||||
imageMime?: string; // accepted for symmetry with route layer; not used
|
||||
credentials: ProviderCredentials | null | undefined;
|
||||
log: ExecutorLog | null | undefined;
|
||||
signal?: AbortSignal | null;
|
||||
clientHeaders?: Record<string, string> | null;
|
||||
}) {
|
||||
const startTime = Date.now();
|
||||
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
||||
if (!prompt) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 400,
|
||||
startTime,
|
||||
error: "Prompt is required for image edit",
|
||||
});
|
||||
}
|
||||
|
||||
if (!credentials?.apiKey) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 401,
|
||||
startTime,
|
||||
error: "ChatGPT Web credentials missing session cookie",
|
||||
});
|
||||
}
|
||||
|
||||
const imageHash = createHash("sha256").update(imageBytes).digest("hex");
|
||||
const cached = findChatGptImageBySha256(imageHash);
|
||||
|
||||
const wantsBase64 = body.response_format === "b64_json";
|
||||
const requestBody = {
|
||||
model,
|
||||
prompt: prompt.slice(0, 500),
|
||||
size: body.size || undefined,
|
||||
image_hash: imageHash.slice(0, 16),
|
||||
image_bytes: imageBytes.length,
|
||||
cached_match: Boolean(cached?.entry.context),
|
||||
};
|
||||
|
||||
if (!cached?.entry.context) {
|
||||
// chatgpt-web's image_gen tool can only edit an image when we continue
|
||||
// the original conversation node. If we never generated this image (or
|
||||
// its 30-minute TTL elapsed), there's no node to continue. Return a
|
||||
// clear, actionable error — much better than silently spawning an
|
||||
// unrelated image and confusing the user.
|
||||
log?.warn?.(
|
||||
"IMAGE",
|
||||
`chatgpt-web edit: no cached match for sha256=${imageHash.slice(0, 16)} (bytes=${imageBytes.length}); returning 400`
|
||||
);
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 400,
|
||||
startTime,
|
||||
error:
|
||||
"chatgpt-web image edit only works for images recently generated through this OmniRoute instance " +
|
||||
"(cache window: 30 minutes). Re-generate the image and try the edit immediately, or disable image-edit " +
|
||||
"in your client to use plain chat-completion edit prompts instead.",
|
||||
requestBody,
|
||||
});
|
||||
}
|
||||
|
||||
// Build a synthetic chat thread that surfaces the cached image URL on
|
||||
// the assistant turn. The executor's parseOpenAIMessages picks up the
|
||||
// URL, findCachedImageContext resolves it to {conversationId,
|
||||
// parentMessageId}, and looksLikeImageEditRequest fires on the user
|
||||
// prompt — together producing a continuation request that actually
|
||||
// edits the saved image.
|
||||
//
|
||||
// The synthetic user prompt is anchored with both an edit verb AND an
|
||||
// image-gen verb so the executor's heuristics fire regardless of what
|
||||
// wording the caller used ("now make it brighter", "tweak this", ...):
|
||||
// - looksLikeImageEditRequest: matches "edit" + "image" within 120 chars
|
||||
// - looksLikeImageGenRequest: matches "generate" + "image" within 40 chars
|
||||
// Either match alone would set forImageGen, but covering both is cheap
|
||||
// insurance for prompts that don't fit common phrasings.
|
||||
const messages: Array<{ role: string; content: string }> = [
|
||||
{
|
||||
role: "assistant",
|
||||
// The base URL is irrelevant — only the path is parsed by
|
||||
// CACHED_IMAGE_URL_RE in the executor's findCachedImageContext.
|
||||
content: ``,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `Edit the image and generate the new image: ${prompt}`,
|
||||
},
|
||||
];
|
||||
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model,
|
||||
body: { messages },
|
||||
stream: false,
|
||||
credentials,
|
||||
signal,
|
||||
log,
|
||||
clientHeaders,
|
||||
});
|
||||
|
||||
const responseText = await result.response.text();
|
||||
if (result.response.status >= 400) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: result.response.status,
|
||||
startTime,
|
||||
error: responseText,
|
||||
requestBody,
|
||||
});
|
||||
}
|
||||
|
||||
let content = "";
|
||||
try {
|
||||
const json = JSON.parse(responseText);
|
||||
content = String(json?.choices?.[0]?.message?.content || "");
|
||||
} catch {
|
||||
content = responseText;
|
||||
}
|
||||
|
||||
const urls = extractMarkdownImageUrls(content);
|
||||
if (urls.length === 0) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 502,
|
||||
startTime,
|
||||
error: `ChatGPT Web edit completed without returning image markdown: ${content.slice(0, 300)}`,
|
||||
requestBody,
|
||||
});
|
||||
}
|
||||
|
||||
const images: Array<{ url?: string; b64_json?: string }> = [];
|
||||
for (const url of urls) {
|
||||
if (!wantsBase64) {
|
||||
images.push({ url });
|
||||
continue;
|
||||
}
|
||||
const id = url.match(CHATGPT_WEB_IMAGE_ID_RE)?.[1];
|
||||
const cachedNew = id ? getChatGptImage(id) : null;
|
||||
if (!cachedNew) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 502,
|
||||
startTime,
|
||||
error: "ChatGPT Web image bytes expired before b64_json conversion",
|
||||
requestBody,
|
||||
});
|
||||
}
|
||||
images.push({ b64_json: cachedNew.bytes.toString("base64") });
|
||||
}
|
||||
|
||||
return saveImageSuccessResult({
|
||||
provider,
|
||||
model,
|
||||
startTime,
|
||||
requestBody,
|
||||
responseBody: { images_count: images.length, edit_match: Boolean(cached?.entry.context) },
|
||||
images,
|
||||
});
|
||||
}
|
||||
|
||||
async function handleFalAIImageGeneration({
|
||||
model,
|
||||
provider,
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
// Auto-extracted from open-sse/handlers/imageGeneration.ts in PR-#4582-batch
|
||||
// Family: chatgpt-web | Module: chatgptWeb | Lines: 1102-1282 (181 LOC)
|
||||
// Ref: see open-sse/handlers/imageGeneration.ts top-of-file comment for split rationale
|
||||
|
||||
import { ChatGptWebExecutor } from "../../../executors/chatgpt-web.ts";
|
||||
import { getChatGptImage } from "../../../services/chatgptImageCache.ts";
|
||||
import { saveImageErrorResult, saveImageSuccessResult } from "../../imageGeneration.ts";
|
||||
|
||||
export const CHATGPT_WEB_IMAGE_MARKDOWN_RE = /!\[[^\]]*\]\(([^)\s]+)\)/g;
|
||||
export const CHATGPT_WEB_IMAGE_ID_RE =
|
||||
/\/v1\/chatgpt-web\/image\/([a-f0-9]{16,64})(?=[?\s"'<>)]|$)/i;
|
||||
|
||||
export function extractMarkdownImageUrls(text: string): string[] {
|
||||
const urls: string[] = [];
|
||||
// String.prototype.matchAll consumes a fresh iterator and ignores the
|
||||
// regex's lastIndex, so no manual reset is required.
|
||||
for (const match of text.matchAll(CHATGPT_WEB_IMAGE_MARKDOWN_RE)) {
|
||||
if (match[1]) urls.push(match[1]);
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
export function buildChatGptWebImagePrompt(body): string {
|
||||
const prompt = String(body.prompt || "").trim();
|
||||
const details: string[] = [`Create an image for this prompt: ${prompt}`];
|
||||
if (typeof body.size === "string" && body.size.trim()) {
|
||||
details.push(`Requested size: ${body.size.trim()}.`);
|
||||
}
|
||||
if (typeof body.quality === "string" && body.quality.trim()) {
|
||||
details.push(`Requested quality: ${body.quality.trim()}.`);
|
||||
}
|
||||
if (typeof body.style === "string" && body.style.trim()) {
|
||||
details.push(`Requested style: ${body.style.trim()}.`);
|
||||
}
|
||||
return details.join("\n");
|
||||
}
|
||||
|
||||
export async function handleChatGptWebImageGeneration({
|
||||
model,
|
||||
provider,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
signal,
|
||||
clientHeaders,
|
||||
// Injectable so unit tests can drive the handler without a live ChatGPT
|
||||
// session; production uses the real executor.
|
||||
executorFactory = () => new ChatGptWebExecutor(),
|
||||
}) {
|
||||
const startTime = Date.now();
|
||||
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
||||
if (!prompt) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 400,
|
||||
startTime,
|
||||
error: "Prompt is required for ChatGPT Web image generation",
|
||||
});
|
||||
}
|
||||
|
||||
if (!credentials?.apiKey) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 401,
|
||||
startTime,
|
||||
error: "ChatGPT Web credentials missing session cookie",
|
||||
});
|
||||
}
|
||||
|
||||
// Each image is one chatgpt.com chat turn (~30s). Cap at 4 (matches OpenAI's
|
||||
// own limit for GPT Image models) so a stray n=1000 doesn't pin the
|
||||
// executor for hours before the upstream HTTP timeout fires.
|
||||
const CHATGPT_WEB_IMAGE_N_MAX = 4;
|
||||
const rawCount = Number.isInteger(body.n) && (body.n as number) > 0 ? (body.n as number) : 1;
|
||||
if (rawCount > CHATGPT_WEB_IMAGE_N_MAX) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 400,
|
||||
startTime,
|
||||
error: `ChatGPT Web image generation supports n=1..${CHATGPT_WEB_IMAGE_N_MAX} (got ${rawCount}); each n is a separate ~30s chat turn.`,
|
||||
});
|
||||
}
|
||||
const requestedCount = rawCount;
|
||||
if (log && requestedCount > 1) {
|
||||
log.warn(
|
||||
"IMAGE",
|
||||
`ChatGPT Web returns one image per chat turn; requested n=${requestedCount} will run sequentially`
|
||||
);
|
||||
}
|
||||
|
||||
const wantsBase64 = body.response_format === "b64_json";
|
||||
const images: Array<{ url?: string; b64_json?: string }> = [];
|
||||
const requestBody = {
|
||||
model,
|
||||
prompt: prompt.slice(0, 500),
|
||||
size: body.size || undefined,
|
||||
quality: body.quality || undefined,
|
||||
};
|
||||
|
||||
for (let i = 0; i < requestedCount; i++) {
|
||||
const executor = executorFactory();
|
||||
const result = await executor.execute({
|
||||
model,
|
||||
body: {
|
||||
messages: [{ role: "user", content: buildChatGptWebImagePrompt(body) }],
|
||||
},
|
||||
stream: false,
|
||||
credentials,
|
||||
signal,
|
||||
log,
|
||||
clientHeaders,
|
||||
});
|
||||
|
||||
const responseText = await result.response.text();
|
||||
if (result.response.status >= 400) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: result.response.status,
|
||||
startTime,
|
||||
error: responseText,
|
||||
requestBody,
|
||||
});
|
||||
}
|
||||
|
||||
let content = "";
|
||||
let imageResolutionFailed = false;
|
||||
try {
|
||||
const json = JSON.parse(responseText);
|
||||
content = String(json?.choices?.[0]?.message?.content || "");
|
||||
imageResolutionFailed = json?.x_image_resolution_failed === true;
|
||||
} catch {
|
||||
content = responseText;
|
||||
}
|
||||
|
||||
const urls = extractMarkdownImageUrls(content);
|
||||
if (urls.length === 0) {
|
||||
// Distinguish "image was generated upstream but OmniRoute could not
|
||||
// retrieve it" (executor flagged the unresolved asset pointer) from
|
||||
// "no image was produced at all" — the former is our bug/limitation,
|
||||
// not a failed prompt, so the message must not read as "no image made".
|
||||
const error = imageResolutionFailed
|
||||
? `ChatGPT Web generated an image but OmniRoute could not retrieve it (the image asset could not be downloaded — the URL may have expired or ChatGPT changed its image delivery format). Please retry; if it persists, report it. Assistant text: ${content.slice(0, 200)}`
|
||||
: `ChatGPT Web completed without returning image markdown: ${content.slice(0, 300)}`;
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 502,
|
||||
startTime,
|
||||
error,
|
||||
requestBody,
|
||||
});
|
||||
}
|
||||
|
||||
for (const url of urls) {
|
||||
if (!wantsBase64) {
|
||||
images.push({ url });
|
||||
continue;
|
||||
}
|
||||
const id = url.match(CHATGPT_WEB_IMAGE_ID_RE)?.[1];
|
||||
const cached = id ? getChatGptImage(id) : null;
|
||||
if (!cached) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 502,
|
||||
startTime,
|
||||
error: "ChatGPT Web image bytes expired before b64_json conversion",
|
||||
requestBody,
|
||||
});
|
||||
}
|
||||
images.push({ b64_json: cached.bytes.toString("base64") });
|
||||
}
|
||||
}
|
||||
|
||||
return saveImageSuccessResult({
|
||||
provider,
|
||||
model,
|
||||
startTime,
|
||||
requestBody,
|
||||
responseBody: { images_count: images.length },
|
||||
images,
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Gemini Web image generation handler (#10466).
|
||||
//
|
||||
// Exposes the gemini-web session provider through POST /v1/images/generations.
|
||||
// Follows the chatgpt-web precedent (./chatgptWeb.ts): the web-session chat
|
||||
// Uses the shared web-session pattern: the chat
|
||||
// executor is driven with an image-generation prompt, and the generated
|
||||
// assets are extracted from the response.
|
||||
//
|
||||
@@ -22,7 +22,7 @@ import { fetchRemoteImage } from "@/shared/network/remoteImageFetch";
|
||||
import { saveImageErrorResult, saveImageSuccessResult } from "../../imageGeneration.ts";
|
||||
import { sanitizeErrorMessage } from "../../../utils/error.ts";
|
||||
|
||||
/** Each image is one gemini.google.com turn (~30-60s). Cap like chatgpt-web. */
|
||||
/** Each image is one gemini.google.com turn (~30-60s); keep fan-out bounded. */
|
||||
const GEMINI_WEB_IMAGE_N_MAX = 4;
|
||||
|
||||
export function buildGeminiWebImagePrompt(body: Record<string, unknown>): string {
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
/**
|
||||
* Regression tests for the proxy-leak fix in chatgptTlsClient.
|
||||
*
|
||||
* Bug context (#2022): tlsFetchChatGpt() built its native tls-client-node
|
||||
* requestOptions without a `proxyUrl` field, so every chatgpt-web call
|
||||
* egressed with the bare host IP regardless of the dashboard proxy config
|
||||
* or HTTP_PROXY / HTTPS_PROXY env vars (the koffi-loaded Go binary does not
|
||||
* consult Go's `http.ProxyFromEnvironment`).
|
||||
*
|
||||
* These tests pin the resolution-order contract:
|
||||
* 1. Per-call `options.proxyUrl` wins.
|
||||
* 2. OMNIROUTE_TLS_PROXY_URL env var (single-flag opt-in).
|
||||
* 3. POSIX-standard HTTPS_PROXY / HTTP_PROXY / ALL_PROXY (and lowercase variants).
|
||||
* 4. Otherwise undefined (no proxy).
|
||||
*
|
||||
* They also pin that the resolved proxy is actually placed on the
|
||||
* requestOptions object handed to the native binding — the original bug
|
||||
* was that nothing called `proxyUrl` at all, so a client.request spy that
|
||||
* captures opts.proxyUrl is the right shape of regression.
|
||||
*/
|
||||
|
||||
import { describe, it, beforeEach, afterEach, expect } from "vitest";
|
||||
|
||||
import { tlsFetchChatGpt, __setTlsFetchOverrideForTesting } from "../chatgptTlsClient.ts";
|
||||
|
||||
const PROXY_ENV_KEYS = [
|
||||
"OMNIROUTE_TLS_PROXY_URL",
|
||||
"HTTPS_PROXY",
|
||||
"https_proxy",
|
||||
"HTTP_PROXY",
|
||||
"http_proxy",
|
||||
"ALL_PROXY",
|
||||
"all_proxy",
|
||||
] as const;
|
||||
|
||||
function clearProxyEnv(): Record<string, string | undefined> {
|
||||
const saved: Record<string, string | undefined> = {};
|
||||
for (const k of PROXY_ENV_KEYS) {
|
||||
saved[k] = process.env[k];
|
||||
delete process.env[k];
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
|
||||
function restoreProxyEnv(saved: Record<string, string | undefined>): void {
|
||||
for (const k of PROXY_ENV_KEYS) {
|
||||
if (saved[k] === undefined) delete process.env[k];
|
||||
else process.env[k] = saved[k];
|
||||
}
|
||||
}
|
||||
|
||||
describe("chatgptTlsClient — proxy plumbing (#2022)", async () => {
|
||||
let savedEnv: Record<string, string | undefined> = {};
|
||||
|
||||
beforeEach(() => {
|
||||
savedEnv = clearProxyEnv();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
__setTlsFetchOverrideForTesting(null);
|
||||
restoreProxyEnv(savedEnv);
|
||||
});
|
||||
|
||||
it("per-call proxyUrl overrides everything", async () => {
|
||||
process.env.OMNIROUTE_TLS_PROXY_URL = "http://env-omni:0/";
|
||||
process.env.HTTPS_PROXY = "http://env-https:0/";
|
||||
|
||||
let observedUrl: string | undefined;
|
||||
let observedOpts: Record<string, unknown> = {};
|
||||
__setTlsFetchOverrideForTesting(async (url, options) => {
|
||||
observedUrl = url;
|
||||
observedOpts = options as unknown as Record<string, unknown>;
|
||||
// Mimic what the real path does so the resolveProxyUrl branch runs.
|
||||
// (When testOverride is set, tlsFetchChatGpt short-circuits — so we
|
||||
// keep the override semantics but still validate that callers are
|
||||
// free to pass `proxyUrl` through TlsFetchOptions.)
|
||||
return { status: 200, headers: new Headers(), text: "{}", body: null };
|
||||
});
|
||||
|
||||
const r = await tlsFetchChatGpt("https://chatgpt.com/api/auth/session", {
|
||||
method: "GET",
|
||||
proxyUrl: "http://per-call:0/",
|
||||
});
|
||||
|
||||
expect(r.status).toBe(200);
|
||||
expect(observedUrl).toBe("https://chatgpt.com/api/auth/session");
|
||||
expect((observedOpts as { proxyUrl?: string }).proxyUrl).toBe("http://per-call:0/");
|
||||
});
|
||||
|
||||
it("TlsFetchOptions accepts proxyUrl typed as string", () => {
|
||||
// Compile-time check via runtime assignment: if proxyUrl were not in
|
||||
// the interface, this object literal would be a TypeScript error.
|
||||
const opts: { proxyUrl?: string } = { proxyUrl: "http://x:0/" };
|
||||
expect(opts.proxyUrl).toBe("http://x:0/");
|
||||
});
|
||||
});
|
||||
@@ -9,9 +9,8 @@
|
||||
*
|
||||
* These tests pin the resolution-order contract:
|
||||
* 1. Per-call `options.proxyUrl` wins.
|
||||
* 2. OMNIROUTE_TLS_PROXY_URL env var (single-flag opt-in).
|
||||
* 3. POSIX-standard HTTPS_PROXY / HTTP_PROXY / ALL_PROXY (and lowercase variants).
|
||||
* 4. Otherwise undefined (no proxy).
|
||||
* 2. POSIX-standard HTTPS_PROXY / HTTP_PROXY / ALL_PROXY (and lowercase variants).
|
||||
* 3. Otherwise undefined (no proxy).
|
||||
*
|
||||
* They also pin that the resolved proxy is actually placed on the
|
||||
* requestOptions object handed to the native binding — the original bug
|
||||
@@ -24,7 +23,6 @@ import { describe, it, beforeEach, afterEach, expect } from "vitest";
|
||||
import { tlsFetchGrok, __setTlsFetchOverrideForTesting } from "../grokTlsClient.ts";
|
||||
|
||||
const PROXY_ENV_KEYS = [
|
||||
"OMNIROUTE_TLS_PROXY_URL",
|
||||
"HTTPS_PROXY",
|
||||
"https_proxy",
|
||||
"HTTP_PROXY",
|
||||
@@ -62,7 +60,6 @@ describe("grokTlsClient — proxy plumbing (#3180)", async () => {
|
||||
});
|
||||
|
||||
it("per-call proxyUrl overrides everything", async () => {
|
||||
process.env.OMNIROUTE_TLS_PROXY_URL = "http://env-omni:0/";
|
||||
process.env.HTTPS_PROXY = "http://env-https:0/";
|
||||
|
||||
let observedUrl: string | undefined;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getSettings } from "@/lib/db/settings";
|
||||
import { getProviderRegistry } from "./providerRegistryAccessor";
|
||||
import type { ConnectionFields } from "@/lib/db/encryption";
|
||||
import { NOAUTH_PROVIDERS } from "@/shared/constants/providers";
|
||||
import { isCommonChatGptWebRetiredProviderId } from "@/shared/constants/chatgptWebRetirement";
|
||||
import { hasUsableWebSessionCredential } from "@/shared/providers/webSessionCredentials";
|
||||
import { toNumber } from "@/shared/utils/numeric";
|
||||
import { isCompatibleProviderConnectionId } from "@/shared/utils/compatibleProviderId";
|
||||
@@ -583,15 +584,17 @@ export async function prepareVirtualAutoComboInputs(
|
||||
resolutionSnapshot?: ModelCapabilityResolutionSnapshot;
|
||||
} = {}
|
||||
): Promise<PreparedVirtualAutoComboInputs> {
|
||||
const [connections, disabledNoAuthConnections, settings] = await Promise.all([
|
||||
const [rawConnections, rawDisabledNoAuthConnections, settings] = await Promise.all([
|
||||
getCachedProviderConnections({ isActive: true }) as Promise<VirtualFactoryConn[]>,
|
||||
// #6557: no-auth providers (opencode/mimocode/etc.) don't get an isActive
|
||||
// filter applied above since their credential is synthetic, but a real
|
||||
// provider_connections row CAN exist for them (created via "Add Account")
|
||||
// and its own isActive=false must gate the auto-combo pool too — not just
|
||||
// #6557: synthetic no-auth credentials bypass active filtering, but a real Add Account
|
||||
// row may exist; its isActive=false must also gate auto-combo.
|
||||
getCachedProviderConnections({ isActive: false }) as Promise<VirtualFactoryConn[]>,
|
||||
getSettings().catch(() => ({}) as Record<string, unknown>),
|
||||
]);
|
||||
const available = (conn: VirtualFactoryConn) =>
|
||||
!isCommonChatGptWebRetiredProviderId(conn.provider);
|
||||
const connections = rawConnections.filter(available);
|
||||
const disabledNoAuthConnections = rawDisabledNoAuthConnections.filter(available);
|
||||
const blockedProviders = new Set(
|
||||
Array.isArray(settings.blockedProviders) ? (settings.blockedProviders as string[]) : []
|
||||
);
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
/**
|
||||
* In-memory cache for ChatGPT-generated images so we can serve them via a
|
||||
* regular HTTP URL instead of inlining megabytes of base64 into SSE deltas.
|
||||
*
|
||||
* Why: chatgpt.com's `image_asset_pointer` resolves to a session-signed
|
||||
* `estuary/content` URL that 403s for any anonymous client. We have to
|
||||
* download the bytes server-side (with the user's session) and re-serve
|
||||
* them. Streaming the raw base64 back through SSE works but Open WebUI's
|
||||
* progressive markdown renderer displays each chunk as text mid-stream —
|
||||
* the user sees ~3 MB of base64 scroll past before the final `)` arrives
|
||||
* and the renderer recognizes it as an image. Hosting the image on a
|
||||
* regular URL avoids that entirely: we emit a tiny ``
|
||||
* markdown delta and the browser fetches the image normally.
|
||||
*
|
||||
* The cache is in-memory only, with a short TTL — these URLs are single-use
|
||||
* artifacts of one chat turn, not persistent assets. If the user reloads
|
||||
* the conversation in a few hours the URLs will 404; that's expected.
|
||||
*/
|
||||
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
|
||||
interface CachedImage {
|
||||
bytes: Buffer;
|
||||
mime: string;
|
||||
expiresAt: number;
|
||||
context?: ChatGptImageConversationContext;
|
||||
/** sha256(bytes) — used by /v1/images/edits to correlate an uploaded
|
||||
* image (Open WebUI re-uploads the bytes via multipart) back to the
|
||||
* conversation context we cached when the image was first generated. */
|
||||
bytesSha256: string;
|
||||
}
|
||||
|
||||
const cache = new Map<string, CachedImage>();
|
||||
let cacheBytes = 0;
|
||||
const DEFAULT_TTL_MS = 30 * 60 * 1000;
|
||||
const MAX_ENTRIES = 25;
|
||||
// Per-entry images cap at 8 MB (enforced upstream in the executor) so 10 MB
|
||||
// covers ~1 large image. The byte cap matters more than entry count: a hot
|
||||
// loop of 8 MB images would otherwise pin 1.6 GB of RSS before count
|
||||
// eviction kicked in. Tune via OMNIROUTE_CGPT_WEB_IMAGE_CACHE_MAX_MB.
|
||||
const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
||||
|
||||
function configuredMaxBytes(): number {
|
||||
const raw = Number(process.env.OMNIROUTE_CGPT_WEB_IMAGE_CACHE_MAX_MB);
|
||||
if (!Number.isFinite(raw) || raw <= 0) return DEFAULT_MAX_BYTES;
|
||||
return Math.floor(raw * 1024 * 1024);
|
||||
}
|
||||
|
||||
export interface ChatGptImageConversationContext {
|
||||
conversationId: string;
|
||||
parentMessageId: string;
|
||||
}
|
||||
|
||||
function deleteEntry(id: string): void {
|
||||
const entry = cache.get(id);
|
||||
if (!entry) return;
|
||||
cacheBytes -= entry.bytes.length;
|
||||
cache.delete(id);
|
||||
}
|
||||
|
||||
function evictExpired(now = Date.now()): void {
|
||||
for (const [id, entry] of cache) {
|
||||
if (now >= entry.expiresAt) deleteEntry(id);
|
||||
}
|
||||
}
|
||||
|
||||
function evictUntilWithinLimits(maxBytes: number, incomingBytes: number): void {
|
||||
// Drop oldest until both the entry-count and total-byte caps are satisfied.
|
||||
// Map iteration is insertion-ordered so the first key is the oldest entry.
|
||||
while ((cache.size >= MAX_ENTRIES || cacheBytes + incomingBytes > maxBytes) && cache.size > 0) {
|
||||
const firstKey = cache.keys().next().value;
|
||||
if (!firstKey) break;
|
||||
deleteEntry(firstKey);
|
||||
}
|
||||
}
|
||||
|
||||
export function storeChatGptImage(
|
||||
bytes: Buffer,
|
||||
mime: string,
|
||||
ttlMs = DEFAULT_TTL_MS,
|
||||
context?: ChatGptImageConversationContext
|
||||
): string {
|
||||
evictExpired();
|
||||
evictUntilWithinLimits(configuredMaxBytes(), bytes.length);
|
||||
const id = randomUUID().replace(/-/g, "");
|
||||
const bytesSha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
cache.set(id, {
|
||||
bytes,
|
||||
mime,
|
||||
expiresAt: Date.now() + ttlMs,
|
||||
context,
|
||||
bytesSha256,
|
||||
});
|
||||
cacheBytes += bytes.length;
|
||||
return id;
|
||||
}
|
||||
|
||||
export function getChatGptImage(id: string): CachedImage | null {
|
||||
evictExpired();
|
||||
const entry = cache.get(id);
|
||||
if (!entry) return null;
|
||||
if (Date.now() >= entry.expiresAt) {
|
||||
deleteEntry(id);
|
||||
return null;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
export function getChatGptImageConversationContext(
|
||||
id: string
|
||||
): ChatGptImageConversationContext | null {
|
||||
return getChatGptImage(id)?.context ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a cached entry by sha256(bytes). Used by /v1/images/edits to
|
||||
* correlate Open WebUI's re-uploaded image back to the conversation
|
||||
* context we cached at generation time, so the executor can continue the
|
||||
* saved chatgpt.com conversation node and actually edit the image instead
|
||||
* of generating an unrelated one from scratch.
|
||||
*/
|
||||
export function findChatGptImageBySha256(hash: string): { id: string; entry: CachedImage } | null {
|
||||
evictExpired();
|
||||
const target = hash.toLowerCase();
|
||||
for (const [id, entry] of cache.entries()) {
|
||||
if (entry.bytesSha256 === target) {
|
||||
if (Date.now() < entry.expiresAt) return { id, entry };
|
||||
deleteEntry(id);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Test-only: clear the cache between tests. */
|
||||
export function __resetChatGptImageCacheForTesting(): void {
|
||||
cache.clear();
|
||||
cacheBytes = 0;
|
||||
}
|
||||
|
||||
/** Test-only: peek at current resident-byte total. */
|
||||
export function __getChatGptImageCacheBytesForTesting(): number {
|
||||
return cacheBytes;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* Browser-TLS-impersonating HTTP client for chatgpt.com.
|
||||
*
|
||||
* Thin re-export over the shared `tlsClientBase.ts` factory
|
||||
* (`createTlsClientModule`). All provider-agnostic logic (sidecar lifecycle,
|
||||
* streaming tail-file, proxy resolution, error classes, SSE detection) lives
|
||||
* in the base module; this file supplies only ChatGPT-specific config and
|
||||
* preserves the original public export surface.
|
||||
*/
|
||||
|
||||
import {
|
||||
createTlsClientModule,
|
||||
type TlsFetchOptions,
|
||||
type TlsFetchResult,
|
||||
} from "./tlsClientBase.ts";
|
||||
|
||||
const DEFAULT_TIMEOUT_MS =
|
||||
Number.parseInt(process.env.OMNIROUTE_CHATGPT_TLS_TIMEOUT_MS || "", 10) || 60_000;
|
||||
const HARD_TIMEOUT_GRACE_MS =
|
||||
Number.parseInt(process.env.OMNIROUTE_CHATGPT_TLS_GRACE_MS || "", 10) || 10_000;
|
||||
const STREAM_FIRST_BYTE_TIMEOUT_MS =
|
||||
Number.parseInt(process.env.OMNIROUTE_CHATGPT_STREAM_FIRST_BYTE_TIMEOUT_MS || "", 10) || 30_000;
|
||||
|
||||
export const tlsClientModule = createTlsClientModule({
|
||||
providerName: "ChatGPT",
|
||||
tlsProfile: "firefox_148",
|
||||
domain: "https://chatgpt.com",
|
||||
tempDirPrefix: "cgpt-stream-",
|
||||
tailFileVariant: "A",
|
||||
responseValidation: "sse",
|
||||
exportCloudflareCheck: false,
|
||||
exposeStreamingForTesting: true,
|
||||
defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
|
||||
hardTimeoutGraceMs: HARD_TIMEOUT_GRACE_MS,
|
||||
firstByteTimeoutMs: STREAM_FIRST_BYTE_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
export const tlsFetchChatGpt = (
|
||||
url: string,
|
||||
options: TlsFetchOptions = {}
|
||||
): Promise<TlsFetchResult> => tlsClientModule.tlsFetch(url, options);
|
||||
export const __tlsFetchStreamingForTesting = tlsClientModule.__tlsFetchStreamingForTesting;
|
||||
|
||||
export const __setTlsFetchOverrideForTesting = tlsClientModule.__setTlsFetchOverrideForTesting;
|
||||
|
||||
export { TlsClientHangError, TlsClientUnavailableError } from "./tlsClientBase.ts";
|
||||
export type { TlsFetchOptions, TlsFetchResult } from "./tlsClientBase.ts";
|
||||
export { looksLikeSse } from "./tlsClientBase.ts";
|
||||
@@ -28,6 +28,7 @@ import type {
|
||||
ResolvedComboTarget,
|
||||
} from "./types.ts";
|
||||
import { extractSessionAffinityKey } from "@/sse/services/auth";
|
||||
import { isCommonChatGptWebRetiredProviderId } from "@/shared/constants/chatgptWebRetirement";
|
||||
import { filterChatSelectableModels } from "../modelEndpointPolicy.ts";
|
||||
import { DEFAULT_INTENT_CONFIG, type IntentClassifierConfig } from "../intentClassifier.ts";
|
||||
import { getTaskFitness } from "../autoCombo/taskFitness.ts";
|
||||
@@ -424,6 +425,13 @@ export async function expandAutoComboCandidatePool(
|
||||
eligibleTargets: ResolvedComboTarget[],
|
||||
combo: { autoConfig?: unknown; config?: unknown } | null | undefined
|
||||
): Promise<ResolvedComboTarget[]> {
|
||||
for (let index = eligibleTargets.length - 1; index >= 0; index -= 1) {
|
||||
const target = eligibleTargets[index];
|
||||
if (isCommonChatGptWebRetiredProviderId(target.providerId || target.provider)) {
|
||||
eligibleTargets.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const localAutoConfig =
|
||||
(combo?.autoConfig as Record<string, unknown> | undefined) ||
|
||||
(isRecord((combo?.config as Record<string, unknown>)?.auto)
|
||||
@@ -457,7 +465,10 @@ export async function expandAutoComboCandidatePool(
|
||||
...new Set(
|
||||
(allConnections as Array<{ provider?: unknown }>)
|
||||
.map((c) => c.provider)
|
||||
.filter((p): p is string => typeof p === "string" && p.length > 0)
|
||||
.filter(
|
||||
(p): p is string =>
|
||||
typeof p === "string" && p.length > 0 && !isCommonChatGptWebRetiredProviderId(p)
|
||||
)
|
||||
),
|
||||
];
|
||||
// Pre-build a Set of already-present modelStr values so candidate-pool
|
||||
|
||||
@@ -46,7 +46,7 @@ import type {
|
||||
* #8488 / #5240: web-cookie (and similar) providers honestly advertise
|
||||
* registry toolCalling:false but still run the prompt-emulated tool shim.
|
||||
* Combo tools filters must keep those targets eligible so fail-closed does
|
||||
* not regress emulation-only combos (e.g. all chatgpt-web).
|
||||
* not regress emulation-only web-provider combos.
|
||||
*/
|
||||
export function providerSupportsEmulatedToolCalling(
|
||||
providerIdOrAlias: string | null | undefined
|
||||
|
||||
@@ -363,8 +363,8 @@ export function classifyProviderError(
|
||||
if (recoverableProject403) {
|
||||
return PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR;
|
||||
}
|
||||
// #8813 — ChatGPT Web's Cloudflare Sentinel/Turnstile 403 is a TERMINAL
|
||||
// block: the user's IP/session needs a browser Turnstile challenge, and
|
||||
// A Cloudflare Sentinel/Turnstile 403 is a TERMINAL block for browser-session
|
||||
// providers: the user's IP/session needs a browser Turnstile challenge, and
|
||||
// retrying the same connection will keep 403ing. Classify as FORBIDDEN so
|
||||
// the connection gets banned and combo routing falls back to other providers.
|
||||
// Must be checked BEFORE the generic apikey-403→null return below, which
|
||||
|
||||
@@ -53,7 +53,7 @@ export class InAppLoginService extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Start a login flow for a web-cookie provider using Playwright.
|
||||
* @param providerId - e.g. "claude-web", "chatgpt-web"
|
||||
* @param providerId - e.g. "claude-web", "perplexity-web"
|
||||
* @param options.timeout - Total timeout in ms (default: config value or 300s)
|
||||
*/
|
||||
async startLogin(providerId: string, options?: { timeout?: number }): Promise<LoginResult> {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* SessionFactory — Creates initialized Session instances
|
||||
*
|
||||
* For zero-auth providers (Pollinations): just assigns a fingerprint.
|
||||
* For cookie-based providers (ChatGPT Web, DeepSeek Web): would launch
|
||||
* For browser-session providers (DeepSeek Web, etc.): would launch
|
||||
* headless Playwright, solve Turnstile, and extract cookies.
|
||||
*
|
||||
* Currently only zero-auth is implemented. Cookie-based provider support
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* headers (fingerprint-based User-Agent, Sec-CH-UA, etc.) and handles
|
||||
* 429/5xx responses with pool-level cooldown management.
|
||||
*
|
||||
* Future: For cookie-based providers (ChatGPT Web, DeepSeek Web, etc.)
|
||||
* Future: For browser-session providers (DeepSeek Web, etc.)
|
||||
* the wrapper will also inject cookies from the Playwright-authenticated
|
||||
* session.
|
||||
*/
|
||||
|
||||
@@ -104,16 +104,6 @@ const RAW_CONFIGS: TokenExtractionConfig[] = [
|
||||
"Log in to your Claude account at claude.ai. After login, the session cookie will be extracted automatically."
|
||||
),
|
||||
|
||||
// ── ChatGPT Web ───────────────────────────────────────────
|
||||
config(
|
||||
"chatgpt-web",
|
||||
"ChatGPT Web",
|
||||
"https://chatgpt.com/auth/login",
|
||||
"https://chatgpt.com",
|
||||
[{ type: "cookie", name: "__Secure-next-auth.session-token", domain: ".chatgpt.com" }],
|
||||
"Log in to ChatGPT. The __Secure-next-auth.session-token cookie will be extracted after login."
|
||||
),
|
||||
|
||||
// ── Gemini Web ────────────────────────────────────────────
|
||||
config(
|
||||
"gemini-web",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Tool-call translation for web-cookie providers (deepseek-web, chatgpt-web, ...).
|
||||
// Tool-call translation for web-cookie providers (DeepSeek Web, Perplexity Web, etc.).
|
||||
//
|
||||
// The web UIs accept only a single plain prompt string and have no native function
|
||||
// calling — they reply with tool invocations as raw text. To let agentic clients use
|
||||
@@ -532,12 +532,12 @@ function buildToolReminder(toolPrompt: string): string {
|
||||
* Placement matters: the contract used to be PREPENDED as the first system
|
||||
* message. Executors fold all system messages into one block, so with agentic
|
||||
* clients whose system prompts exceed ~28K chars the contract sat at the head
|
||||
* of a huge block and web models (chatgpt-web observed) ignored it, answering
|
||||
* of a huge block and observed web models ignored it, answering
|
||||
* "tool X is not in my tool set" instead of emitting <tool> blocks. Dual
|
||||
* placement fixes it: the full contract goes AFTER the client messages (folds
|
||||
* to the tail of the system block) and a one-line reminder rides at the end of
|
||||
* the latest user message. Measured on cgpt-web/gpt-5.5-thinking with a
|
||||
* 30K-char system prompt: prepend 0/3 tool calls, dual placement 16/17 across
|
||||
* the latest user message. Captured long-context trials showed prepend 0/3 tool
|
||||
* calls and dual placement 16/17 across
|
||||
* 30K-250K prompts, 30-tool sets, multi-turn tool history, and streaming.
|
||||
*/
|
||||
export function prepareToolMessages(
|
||||
|
||||
@@ -343,13 +343,22 @@ export function errorResponseWithComboDiagnostics(
|
||||
* @param {string} message - Error message
|
||||
* @returns {Response} HTTP Response object
|
||||
*/
|
||||
export function errorResponse(statusCode: number, message: string): Response {
|
||||
return new Response(JSON.stringify(buildErrorBody(statusCode, sanitizeErrorMessage(message))), {
|
||||
status: statusCode,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
export function errorResponse(
|
||||
statusCode: number,
|
||||
message: string,
|
||||
classification?: ErrorBodyClassification
|
||||
): Response {
|
||||
return new Response(
|
||||
JSON.stringify(
|
||||
buildErrorBody(statusCode, sanitizeErrorMessage(message), undefined, classification)
|
||||
),
|
||||
{
|
||||
status: statusCode,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* NextAuth session-token cookie helpers shared by web-cookie providers that
|
||||
* authenticate via __Secure-next-auth.session-token (chatgpt-web, perplexity-web, …).
|
||||
* authenticate via __Secure-next-auth.session-token (for example, Perplexity Web).
|
||||
*
|
||||
* Rotation can change the shape (unchunked → chunked or vice versa). When that
|
||||
* happens, every old family member must be dropped — keeping the stale variant
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
// Runtime-portable SHA3-512 (FIPS-202).
|
||||
//
|
||||
// The ChatGPT-Web sentinel proof-of-work (open-sse/executors/chatgpt-web.ts)
|
||||
// hashes with SHA3-512. Node built against OpenSSL has it natively, but the
|
||||
// **Electron desktop app** ships Node built against **BoringSSL, which does not
|
||||
// implement the SHA-3 family** (electron/electron#30530). There,
|
||||
// `createHash("sha3-512")` throws `Error: Digest method not supported`, so every
|
||||
// chatgpt-web request fails with `502 ChatGPT sentinel failed: Digest method not
|
||||
// supported` and the provider is unusable on the desktop app (#5531).
|
||||
//
|
||||
// This module prefers the native digest (fast path on servers / OpenSSL) and
|
||||
// transparently falls back to a dependency-free pure-JS Keccak-f[1600] when the
|
||||
// runtime's crypto lacks SHA-3. The pure-JS path is validated bit-for-bit
|
||||
// against the native digest and the published FIPS-202 vectors in the unit test.
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
// ─── Keccak-f[1600] (BigInt lanes — correctness-first; only the fallback runs it) ──
|
||||
|
||||
const MASK = (1n << 64n) - 1n;
|
||||
|
||||
// Round constants RC[0..23].
|
||||
const RC: bigint[] = [
|
||||
0x0000000000000001n,
|
||||
0x0000000000008082n,
|
||||
0x800000000000808an,
|
||||
0x8000000080008000n,
|
||||
0x000000000000808bn,
|
||||
0x0000000080000001n,
|
||||
0x8000000080008081n,
|
||||
0x8000000000008009n,
|
||||
0x000000000000008an,
|
||||
0x0000000000000088n,
|
||||
0x0000000080008009n,
|
||||
0x000000008000000an,
|
||||
0x000000008000808bn,
|
||||
0x800000000000008bn,
|
||||
0x8000000000008089n,
|
||||
0x8000000000008003n,
|
||||
0x8000000000008002n,
|
||||
0x8000000000000080n,
|
||||
0x000000000000800an,
|
||||
0x800000008000000an,
|
||||
0x8000000080008081n,
|
||||
0x8000000000008080n,
|
||||
0x0000000080000001n,
|
||||
0x8000000080008008n,
|
||||
];
|
||||
|
||||
// Rotation offsets r[x+5y] (rho step).
|
||||
const ROT: number[] = [
|
||||
0, 1, 62, 28, 27, 36, 44, 6, 55, 20, 3, 10, 43, 25, 39, 41, 45, 15, 21, 8, 18, 2, 61, 56, 14,
|
||||
];
|
||||
|
||||
function rotl64(x: bigint, n: number): bigint {
|
||||
if (n === 0) return x;
|
||||
const bn = BigInt(n);
|
||||
return ((x << bn) | (x >> (64n - bn))) & MASK;
|
||||
}
|
||||
|
||||
function keccakF1600(s: bigint[]): void {
|
||||
const C = new Array<bigint>(5);
|
||||
const D = new Array<bigint>(5);
|
||||
const B = new Array<bigint>(25);
|
||||
for (let round = 0; round < 24; round++) {
|
||||
// θ
|
||||
for (let x = 0; x < 5; x++) C[x] = s[x] ^ s[x + 5] ^ s[x + 10] ^ s[x + 15] ^ s[x + 20];
|
||||
for (let x = 0; x < 5; x++) D[x] = C[(x + 4) % 5] ^ rotl64(C[(x + 1) % 5], 1);
|
||||
for (let x = 0; x < 5; x++) for (let y = 0; y < 5; y++) s[x + 5 * y] ^= D[x];
|
||||
// ρ + π
|
||||
for (let x = 0; x < 5; x++) {
|
||||
for (let y = 0; y < 5; y++) {
|
||||
B[y + 5 * ((2 * x + 3 * y) % 5)] = rotl64(s[x + 5 * y], ROT[x + 5 * y]);
|
||||
}
|
||||
}
|
||||
// χ
|
||||
for (let x = 0; x < 5; x++) {
|
||||
for (let y = 0; y < 5; y++) {
|
||||
s[x + 5 * y] = B[x + 5 * y] ^ (~B[((x + 1) % 5) + 5 * y] & MASK & B[((x + 2) % 5) + 5 * y]);
|
||||
}
|
||||
}
|
||||
// ι
|
||||
s[0] ^= RC[round];
|
||||
}
|
||||
}
|
||||
|
||||
// SHA3-512: rate r = 576 bits (72 bytes / 9 lanes), capacity 1024, output 64 bytes.
|
||||
const RATE_BYTES = 72;
|
||||
|
||||
function sha3_512Bytes(msg: Uint8Array): Uint8Array {
|
||||
const s: bigint[] = new Array<bigint>(25).fill(0n);
|
||||
|
||||
// FIPS-202 pad10*1 with SHA-3 domain separation (first pad byte 0x06, last |= 0x80).
|
||||
const padLen = RATE_BYTES - (msg.length % RATE_BYTES);
|
||||
const padded = new Uint8Array(msg.length + padLen);
|
||||
padded.set(msg);
|
||||
padded[msg.length] = 0x06;
|
||||
padded[padded.length - 1] |= 0x80;
|
||||
|
||||
// Absorb (little-endian lanes).
|
||||
for (let off = 0; off < padded.length; off += RATE_BYTES) {
|
||||
for (let i = 0; i < RATE_BYTES / 8; i++) {
|
||||
let lane = 0n;
|
||||
for (let b = 0; b < 8; b++) lane |= BigInt(padded[off + i * 8 + b]) << BigInt(8 * b);
|
||||
s[i] ^= lane;
|
||||
}
|
||||
keccakF1600(s);
|
||||
}
|
||||
|
||||
// Squeeze 64 bytes (8 lanes — fits in one rate block).
|
||||
const out = new Uint8Array(64);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const lane = s[i];
|
||||
for (let b = 0; b < 8; b++) out[i * 8 + b] = Number((lane >> BigInt(8 * b)) & 0xffn);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function toBytes(input: string | Uint8Array): Uint8Array {
|
||||
return typeof input === "string" ? new Uint8Array(Buffer.from(input, "utf8")) : input;
|
||||
}
|
||||
|
||||
/** Pure-JS SHA3-512 hex digest. Always runs the vendored Keccak (used as the fallback). */
|
||||
export function sha3_512HexJs(input: string | Uint8Array): string {
|
||||
return Buffer.from(sha3_512Bytes(toBytes(input))).toString("hex");
|
||||
}
|
||||
|
||||
type NativeHasher = (data: Uint8Array) => string;
|
||||
|
||||
// undefined = not yet probed; null = native SHA-3 unavailable (BoringSSL/Electron).
|
||||
let nativeHasher: NativeHasher | null | undefined;
|
||||
|
||||
function detectNative(): NativeHasher | null {
|
||||
try {
|
||||
// Construct + digest once — Electron/BoringSSL throws here, not lazily.
|
||||
createHash("sha3-512").update(Buffer.alloc(0)).digest("hex");
|
||||
return (data) => createHash("sha3-512").update(data).digest("hex");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA3-512 hex digest that works on every runtime: native (OpenSSL) where
|
||||
* available, pure-JS Keccak fallback where the crypto backend lacks SHA-3
|
||||
* (Electron/BoringSSL — #5531). Capability is probed once and cached.
|
||||
*/
|
||||
export function sha3_512Hex(input: string | Uint8Array): string {
|
||||
const data = toBytes(input);
|
||||
if (nativeHasher === undefined) nativeHasher = detectNative();
|
||||
if (nativeHasher) {
|
||||
try {
|
||||
return nativeHasher(data);
|
||||
} catch {
|
||||
nativeHasher = null; // became unavailable mid-flight — defensive
|
||||
}
|
||||
}
|
||||
return sha3_512HexJs(data);
|
||||
}
|
||||
|
||||
/** Test seam: force the native capability state (null = simulate BoringSSL, undefined = re-probe). */
|
||||
export function __setSha3NativeForTesting(state: NativeHasher | null | undefined): void {
|
||||
nativeHasher = state;
|
||||
}
|
||||
@@ -153,7 +153,7 @@ function isPendingRequestClearedError(error: unknown): boolean {
|
||||
* chunk into the now-closed response stream, as a "Controller is already closed"
|
||||
* TypeError. Treating any of these as an upstream error wrongly cools down the
|
||||
* account/connection, so the stream error path uses this to skip the provider
|
||||
* failover/cooldown (the chatgpt-web / codex / antigravity executors already
|
||||
* failover/cooldown (the Codex / Antigravity executors already
|
||||
* guard client aborts the same way).
|
||||
*/
|
||||
export function isClientDisconnectError(error: unknown): boolean {
|
||||
@@ -599,7 +599,7 @@ function resolveSilentCloseOutcome(input: {
|
||||
// #10443: every known path that produces OpenAI chat chunks emits a
|
||||
// terminal — the response translators (gemini/claude/kiro/cursor-to-openai)
|
||||
// all emit a finish_reason chunk, the non-standard executors (kiro, cursor,
|
||||
// nlpcloud, poe-web, copilot-m365-web, chatgpt-web, chipotle, gitlab)
|
||||
// nlpcloud, poe-web, copilot-m365-web, chipotle, gitlab)
|
||||
// enqueue `data: [DONE]` themselves, and standard OpenAI-compatible
|
||||
// upstreams end with finish_reason + [DONE] per spec. So a close that
|
||||
// forwarded content but no terminal marker is an upstream drop, not a
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* even when it does run, silently no-ops on a rate-limited/failed GitHub API
|
||||
* call instead of raising — so `node_modules/tls-client-node/bin/` can end
|
||||
* up empty with no visible signal until the first live request throws
|
||||
* TlsClientUnavailableError (chatgpt-web/claude-web/grok-web/lmarena/
|
||||
* TlsClientUnavailableError (claude-web/grok-web/lmarena/
|
||||
* perplexity-web all share this transport).
|
||||
*
|
||||
* This module:
|
||||
@@ -123,7 +123,7 @@ export async function fixTlsClientNodeBinary({
|
||||
"(GitHub API rate-limited or unreachable after retries)."
|
||||
);
|
||||
console.warn(
|
||||
" chatgpt-web/claude-web/grok-web/lmarena/perplexity-web will raise a clear " +
|
||||
" claude-web/grok-web/lmarena/perplexity-web will raise a clear " +
|
||||
"TlsClientUnavailableError on first use until this is resolved."
|
||||
);
|
||||
console.warn(
|
||||
|
||||
@@ -140,7 +140,7 @@ export const PACK_ARTIFACT_ROOT_ALLOWED_EXACT_PATHS: string[] = [
|
||||
"scripts/build/postinstallSupport.mjs",
|
||||
"scripts/build/colocateOptionals.mjs",
|
||||
// #7802: imported by scripts/build/postinstall.mjs to repair tls-client-node's
|
||||
// native binary (chatgpt-web/claude-web/grok-web/lmarena/perplexity-web transport).
|
||||
// native binary (claude-web/grok-web/lmarena/perplexity-web transport).
|
||||
"scripts/build/fixTlsClientNodeBinary.mjs",
|
||||
// #8859: imported by scripts/build/postinstall.mjs to repair playwright-core's
|
||||
// browser resolution on Termux/Android (no glibc, no bundled browsers).
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* Modules repaired:
|
||||
* - better-sqlite3 (SQLite bindings)
|
||||
* - wreq-js (TLS client for OAuth providers)
|
||||
* - tls-client-node (TLS client for chatgpt-web/claude-web/grok-web/lmarena/perplexity-web)
|
||||
* - tls-client-node (TLS client for claude-web/grok-web/lmarena/perplexity-web)
|
||||
* - sql.js (WASM SQLite fallback runtime)
|
||||
* - node-machine-id (local CLI machine-token server runtime)
|
||||
*
|
||||
|
||||
@@ -57,7 +57,7 @@ export const OPTIONAL_PACKS = [
|
||||
name: "browser-runtime",
|
||||
packVersion: 1,
|
||||
description:
|
||||
"Browser automation closure: Claude Turnstile solver and ChatGPT/Gemini web executors",
|
||||
"Browser automation closure: Claude Turnstile solver and ChatGPT Web (Codex)/Gemini web executors",
|
||||
packages: [{ name: "playwright" }, { name: "playwright-core" }],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
decodeChatGptWebCodexSecrets,
|
||||
encodeChatGptWebCodexSecrets,
|
||||
} from "@omniroute/open-sse/services/chatgptWebCodexAdmin.ts";
|
||||
import { rejectRetiredCommonChatGptWebProvider } from "@/lib/providers/chatgptWebRetirementResponse";
|
||||
|
||||
function normalizeCodexLimitPolicy(
|
||||
incoming: unknown,
|
||||
@@ -129,10 +130,7 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
|
||||
...validation.error.details.map((d) => d.field).filter(Boolean),
|
||||
...validation.error.details.flatMap((d) => d.keys ?? []),
|
||||
];
|
||||
return NextResponse.json(
|
||||
{ error: { ...validation.error, rejected } },
|
||||
{ status: 400 }
|
||||
);
|
||||
return NextResponse.json({ error: { ...validation.error, rejected } }, { status: 400 });
|
||||
}
|
||||
const body = validation.data;
|
||||
const {
|
||||
@@ -166,6 +164,8 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: "Connection not found" }, { status: 404 });
|
||||
}
|
||||
const retirementResponse = rejectRetiredCommonChatGptWebProvider(existing.provider);
|
||||
if (retirementResponse) return retirementResponse;
|
||||
|
||||
const updateData: Record<string, any> = {};
|
||||
if (name !== undefined) updateData.name = name;
|
||||
|
||||
@@ -36,9 +36,9 @@ import { classifyAmbiguousOrAuthError, type ClassifyFailureArgs } from "./mistra
|
||||
import { buildApiKeyConnectionTestResult } from "./apiKeyTestResult";
|
||||
import { classifyOAuthProbeInconclusive, OAUTH_TEST_CONFIG } from "./oauthTestConfig";
|
||||
import { isGeoBlockedError } from "@omniroute/open-sse/services/errorClassifier.ts";
|
||||
import * as retirement from "@/lib/providers/chatgptWebRetirementResponse";
|
||||
|
||||
// Bound the OAuth probe so a hung upstream can't block the connection-test queue
|
||||
// forever (#1449). Mirrors the 30s timeout the API-key path uses via validateProviderApiKey.
|
||||
// Match the API-key path's 30s timeout so a hung OAuth upstream cannot block the test queue.
|
||||
const OAUTH_TEST_TIMEOUT_MS = 30_000;
|
||||
|
||||
import { CLI_RUNTIME_PROVIDER_MAP } from "./cliRuntimeProviderMap";
|
||||
@@ -999,8 +999,8 @@ export async function testSingleConnection(connectionId: string, validationModel
|
||||
latencyMs: 0,
|
||||
};
|
||||
}
|
||||
retirement.assertProviderAvailable(provider);
|
||||
|
||||
// Resolve proxy for this connection (key → combo → provider → global → direct)
|
||||
let proxyInfo: any = null;
|
||||
try {
|
||||
proxyInfo = await resolveProxyForConnection(connectionId);
|
||||
@@ -1078,7 +1078,6 @@ export async function testSingleConnection(connectionId: string, validationModel
|
||||
};
|
||||
}
|
||||
|
||||
// Build update data
|
||||
const now = new Date().toISOString();
|
||||
const diagnosis =
|
||||
result.diagnosis ||
|
||||
@@ -1111,6 +1110,7 @@ export async function testSingleConnection(connectionId: string, validationModel
|
||||
connection as { rateLimitedUntil?: string | null },
|
||||
result.valid
|
||||
);
|
||||
const lastErrorType = result.valid ? connection.lastErrorType : diagnosis.type;
|
||||
|
||||
const updateData: Record<string, any> = {
|
||||
testStatus: clearErrorState ? "active" : result.valid ? connection.testStatus : "error",
|
||||
@@ -1125,11 +1125,7 @@ export async function testSingleConnection(connectionId: string, validationModel
|
||||
lastError: clearErrorState ? null : result.valid ? connection.lastError : result.error,
|
||||
lastErrorAt: clearErrorState ? null : result.valid ? connection.lastErrorAt : now,
|
||||
lastTested: now,
|
||||
lastErrorType: clearErrorState
|
||||
? null
|
||||
: result.valid
|
||||
? connection.lastErrorType
|
||||
: diagnosis.type,
|
||||
lastErrorType: clearErrorState ? null : lastErrorType,
|
||||
lastErrorSource: clearErrorState
|
||||
? null
|
||||
: result.valid
|
||||
@@ -1158,7 +1154,6 @@ export async function testSingleConnection(connectionId: string, validationModel
|
||||
if (recovered) updateData.providerSpecificData = recovered;
|
||||
}
|
||||
|
||||
// If token was refreshed, update tokens in DB
|
||||
if (result.refreshed && result.newTokens) {
|
||||
updateData.accessToken = result.newTokens.accessToken;
|
||||
if (result.newTokens.refreshToken) {
|
||||
@@ -1227,7 +1222,6 @@ export async function testSingleConnection(connectionId: string, validationModel
|
||||
};
|
||||
}
|
||||
|
||||
// POST /api/providers/[id]/test - Test connection
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
@@ -1252,6 +1246,8 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
|
||||
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
const retired = retirement.responseForError(error);
|
||||
if (retired) return retired;
|
||||
console.log("Error testing connection:", error);
|
||||
return NextResponse.json({ error: "Test failed" }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
hasUsableWebSessionCredential,
|
||||
resolveWebSessionImportApiKey,
|
||||
} from "@/shared/providers/webSessionCredentials";
|
||||
import { rejectRetiredCommonChatGptWebProvider } from "@/lib/providers/chatgptWebRetirementResponse";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const authError = await requireManagementAuth(request);
|
||||
@@ -39,6 +40,9 @@ export async function POST(request: Request) {
|
||||
|
||||
const { provider, entries, priority, globalPriority } = validation.data;
|
||||
|
||||
const retirementResponse = rejectRetiredCommonChatGptWebProvider(provider);
|
||||
if (retirementResponse) return retirementResponse;
|
||||
|
||||
if (!requiresWebSessionCredential(provider)) {
|
||||
return NextResponse.json(
|
||||
{ error: `Provider '${provider}' does not require web-session credentials` },
|
||||
|
||||
@@ -30,6 +30,7 @@ import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
import { validateProviderApiKey } from "@/lib/providers/validation";
|
||||
import { getProxyForLevel, resolveProxyForProvider } from "@/lib/localDb";
|
||||
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
|
||||
import { rejectRetiredCommonChatGptWebProvider } from "@/lib/providers/chatgptWebRetirementResponse";
|
||||
|
||||
// POST /api/providers/bulk — create multiple API-key connections for a single provider.
|
||||
// Partial-failure semantics: each entry succeeds or fails independently; the
|
||||
@@ -62,6 +63,9 @@ export async function POST(request: Request) {
|
||||
validateKeys,
|
||||
} = validation.data;
|
||||
|
||||
const retirementResponse = rejectRetiredCommonChatGptWebProvider(provider);
|
||||
if (retirementResponse) return retirementResponse;
|
||||
|
||||
const isManagedOrCompatible =
|
||||
isManagedProviderConnectionId(provider) ||
|
||||
isOpenAICompatibleProvider(provider) ||
|
||||
|
||||
@@ -10,7 +10,10 @@ import {
|
||||
getProviderNodeById,
|
||||
isCloudEnabled,
|
||||
} from "@/models";
|
||||
import { isAnthropicCompatibleProvider, isOpenAICompatibleProvider } from "@/shared/constants/providers";
|
||||
import {
|
||||
isAnthropicCompatibleProvider,
|
||||
isOpenAICompatibleProvider,
|
||||
} from "@/shared/constants/providers";
|
||||
import { isManagedProviderConnectionId } from "@/lib/providers/catalog";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { resolveBulkNameCollisions } from "@/shared/utils/bulkApiKeyParser";
|
||||
@@ -26,6 +29,7 @@ import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
import { validateProviderApiKey } from "@/lib/providers/validation";
|
||||
import { getProxyForLevel, resolveProxyForProvider } from "@/lib/localDb";
|
||||
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
|
||||
import { rejectRetiredCommonChatGptWebProvider } from "@/lib/providers/chatgptWebRetirementResponse";
|
||||
|
||||
type ImportEntry = {
|
||||
provider: string;
|
||||
@@ -164,7 +168,10 @@ async function resolveImportNameCollisions(entries: ImportEntry[]): Promise<Impo
|
||||
const resolvedProviderEntries = resolveBulkNameCollisions(providerEntries, existingNames);
|
||||
|
||||
indices.forEach((originalIndex, i) => {
|
||||
resolved[originalIndex] = { ...entries[originalIndex], name: resolvedProviderEntries[i].name };
|
||||
resolved[originalIndex] = {
|
||||
...entries[originalIndex],
|
||||
name: resolvedProviderEntries[i].name,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -205,6 +212,10 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
const { entries, validateKeys } = validation.data;
|
||||
for (const entry of entries) {
|
||||
const retirementResponse = rejectRetiredCommonChatGptWebProvider(entry.provider);
|
||||
if (retirementResponse) return retirementResponse;
|
||||
}
|
||||
const resolvedEntries = await resolveImportNameCollisions(entries);
|
||||
|
||||
const created: Array<Record<string, unknown>> = [];
|
||||
@@ -215,7 +226,12 @@ export async function POST(request: Request) {
|
||||
try {
|
||||
const result = await importOneEntry(entry, !!validateKeys);
|
||||
if ("error" in result) {
|
||||
errors.push({ index: i, name: entry.name, provider: entry.provider, message: result.error });
|
||||
errors.push({
|
||||
index: i,
|
||||
name: entry.name,
|
||||
provider: entry.provider,
|
||||
message: result.error,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
created.push(result.created);
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
import { finalizeValidatedChatGptWebCodexSecrets } from "@omniroute/open-sse/services/chatgptWebCodexAdmin.ts";
|
||||
import { isAutoFetchModelsEnabled } from "@/lib/providerModels/modelDiscovery";
|
||||
import { testSingleConnection } from "./[id]/test/route";
|
||||
import { rejectRetiredCommonChatGptWebProvider } from "@/lib/providers/chatgptWebRetirementResponse";
|
||||
|
||||
function projectCodexAccountPoolWithRoutingQuota(
|
||||
connection: Parameters<typeof projectCodexAccountPool>[0],
|
||||
@@ -175,6 +176,10 @@ export async function POST(request: Request) {
|
||||
providerSpecificData: incomingPsd,
|
||||
} = validation.data;
|
||||
const provider = resolveProviderId(requestedProvider);
|
||||
const retirementResponse =
|
||||
rejectRetiredCommonChatGptWebProvider(requestedProvider) ??
|
||||
rejectRetiredCommonChatGptWebProvider(provider);
|
||||
if (retirementResponse) return retirementResponse;
|
||||
|
||||
// Business validation
|
||||
const isValidProvider =
|
||||
@@ -420,6 +425,17 @@ export async function PATCH(request: Request) {
|
||||
const { ids, isActive } = validation.data;
|
||||
|
||||
try {
|
||||
if (isActive) {
|
||||
const requestedIds = new Set(ids);
|
||||
const requestedConnections = (
|
||||
await getProviderConnections({}, undefined, undefined, ["id", "provider"])
|
||||
).filter((connection) => requestedIds.has(connection.id));
|
||||
for (const connection of requestedConnections) {
|
||||
const retirementResponse = rejectRetiredCommonChatGptWebProvider(connection.provider);
|
||||
if (retirementResponse) return retirementResponse;
|
||||
}
|
||||
}
|
||||
|
||||
// Partial-failure semantics: report unknown IDs instead of failing the whole batch
|
||||
const updatedIds: string[] = [];
|
||||
const notFoundIds: string[] = [];
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getProxyForLevel, resolveProxyForProvider } from "@/lib/localDb";
|
||||
import { validateProviderApiKeySchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { runWithProxyContextOrDirect } from "@omniroute/open-sse/utils/proxyFetch.ts";
|
||||
import { rejectRetiredCommonChatGptWebProvider } from "@/lib/providers/chatgptWebRetirementResponse";
|
||||
|
||||
function sanitizeAuditUrl(url: string | null | undefined) {
|
||||
if (!url) return null;
|
||||
@@ -64,6 +65,9 @@ export async function POST(request) {
|
||||
connectorName,
|
||||
} = validation.data;
|
||||
|
||||
const retirementResponse = rejectRetiredCommonChatGptWebProvider(provider);
|
||||
if (retirementResponse) return retirementResponse;
|
||||
|
||||
let providerSpecificData: any = { validationModelId };
|
||||
if (customUserAgent) {
|
||||
providerSpecificData.customUserAgent = customUserAgent;
|
||||
|
||||
@@ -27,6 +27,10 @@ import {
|
||||
withCompressionHeaderEcho,
|
||||
} from "@/shared/utils/compressionHeaderEcho";
|
||||
import { resolveModelAliasWithSeedFallbackOnBody } from "@/lib/modelAliasResolver";
|
||||
import {
|
||||
assertCommonChatGptWebModelAvailable,
|
||||
isCommonChatGptWebRetirementError,
|
||||
} from "@/shared/constants/chatgptWebRetirement";
|
||||
|
||||
let initPromise = null;
|
||||
|
||||
@@ -147,6 +151,20 @@ export async function POST(request) {
|
||||
errorResponse(400, `${field}: ${issue?.message ?? "Invalid request"}`)
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
assertCommonChatGptWebModelAvailable(parsedBody.model);
|
||||
} catch (error) {
|
||||
if (isCommonChatGptWebRetirementError(error)) {
|
||||
return finishAdmission(
|
||||
errorResponse(error.status, error.message, {
|
||||
type: "provider_error",
|
||||
code: error.code,
|
||||
})
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const structuralAdmission = await admitChatStructure(parsedBody, admission.lease, {
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors";
|
||||
import { getChatGptImage } from "@omniroute/open-sse/services/chatgptImageCache.ts";
|
||||
|
||||
export async function OPTIONS() {
|
||||
return handleCorsOptions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve a cached ChatGPT-generated image by its opaque cache id.
|
||||
*
|
||||
* Auth: intentionally unauthenticated. The id is a 128-bit random UUID and
|
||||
* the entry has a short TTL, so the URL is unguessable for the lifetime of
|
||||
* the chat turn. We need it open because it's loaded by the user's BROWSER
|
||||
* (via an `<img>` tag rendered from markdown) — that fetch doesn't carry
|
||||
* the OmniRoute API key. Rate limiting / abuse protection sit at the
|
||||
* network layer the same way they do for any other static asset.
|
||||
*/
|
||||
export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const entry = getChatGptImage(id);
|
||||
if (!entry) {
|
||||
return new Response(JSON.stringify({ error: "Image not found or expired" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json", ...CORS_HEADERS },
|
||||
});
|
||||
}
|
||||
// entry.bytes is a Buffer (subclass of Uint8Array); pass it directly.
|
||||
// Wrapping in `new Uint8Array(...)` would copy the entire payload — up to
|
||||
// 8 MB per image — for no benefit.
|
||||
return new Response(entry.bytes, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": entry.mime,
|
||||
// Allow short browser caching — the id is unique-per-image, so a
|
||||
// cache hit is fine and saves a round-trip if the user re-renders
|
||||
// the chat. Beyond the in-memory TTL the URL 404s anyway.
|
||||
"Cache-Control": "private, max-age=1800",
|
||||
"Content-Length": String(entry.bytes.length),
|
||||
...CORS_HEADERS,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
handleAdobeFireflyImageGeneration,
|
||||
handleCodexImageEdit,
|
||||
handleImageEdit,
|
||||
handleOpenAIImageEdit,
|
||||
handleOpenRouterImageEdit,
|
||||
} from "@omniroute/open-sse/handlers/imageGeneration.ts";
|
||||
@@ -39,6 +38,10 @@ import {
|
||||
RequestBodyTooLargeError,
|
||||
} from "@/shared/middleware/bodySizeGuard";
|
||||
import { getCachedSettings } from "@/lib/db/readCache";
|
||||
import {
|
||||
CHATGPT_WEB_RETIRED_ERROR_CODE,
|
||||
isCommonChatGptWebRetirementError,
|
||||
} from "@/shared/constants/chatgptWebRetirement";
|
||||
import { z } from "zod";
|
||||
|
||||
// JSON edit body (Open WebUI / OpenAI-style). All fields optional — the prompt
|
||||
@@ -59,11 +62,7 @@ const ImageEditJsonSchema = z
|
||||
/**
|
||||
* /v1/images/edits — OpenAI-compatible image-edit endpoint.
|
||||
*
|
||||
* Two upstream shapes are supported:
|
||||
* - **chatgpt-web**: an "edit" only makes sense if the uploaded image was originally
|
||||
* generated through OmniRoute — we then have its `{conversationId, parentMessageId}`
|
||||
* cached and can continue the saved chatgpt.com conversation node (the only way to
|
||||
* actually edit the image instead of generating an unrelated one).
|
||||
* Supported upstream shapes include:
|
||||
* - **custom OpenAI-compatible providers** (#3214/#3215): forward a multipart edit to
|
||||
* the node's `{base_url}/images/edits`, mirroring how generations forwards.
|
||||
*
|
||||
@@ -85,17 +84,6 @@ export async function OPTIONS() {
|
||||
});
|
||||
}
|
||||
|
||||
const PUBLIC_BASE_URL_HEADER_KEYS = ["host", "x-forwarded-host", "x-forwarded-proto"] as const;
|
||||
|
||||
function publicBaseUrlHeaders(headers: Headers): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
for (const key of PUBLIC_BASE_URL_HEADER_KEYS) {
|
||||
const value = headers.get(key);
|
||||
if (value !== null) out[key] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
interface EditInput {
|
||||
prompt: string;
|
||||
model: string | null;
|
||||
@@ -350,8 +338,27 @@ async function postHandler(request: Request, _context?: unknown) {
|
||||
if (!imageBytes || imageBytes.length === 0) {
|
||||
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing required field: image");
|
||||
}
|
||||
if (!model) {
|
||||
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing required field: model");
|
||||
}
|
||||
|
||||
const fullModel = model || "cgpt-web/gpt-5.5";
|
||||
const fullModel = model;
|
||||
|
||||
// Resolve combo/alias, custom-provider prefix, and built-in ids consistently with
|
||||
// /v1/images/generations (#3215). Retirement is resolved before API-key policy
|
||||
// so the same explicit provider request always receives the deterministic 410.
|
||||
let resolvedModel: string;
|
||||
try {
|
||||
resolvedModel = await resolveImageRouteModel(fullModel);
|
||||
} catch (error) {
|
||||
if (isCommonChatGptWebRetirementError(error)) {
|
||||
return errorResponse(error.status, error.message, {
|
||||
type: "provider_error",
|
||||
code: CHATGPT_WEB_RETIRED_ERROR_CODE,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const policy = await enforceApiKeyPolicy(request, fullModel);
|
||||
if (policy.rejection) return policy.rejection;
|
||||
@@ -361,9 +368,6 @@ async function postHandler(request: Request, _context?: unknown) {
|
||||
? policy.apiKeyInfo.allowedConnections
|
||||
: null;
|
||||
|
||||
// Resolve combo/alias, custom-provider prefix, and built-in ids consistently with
|
||||
// /v1/images/generations (#3215).
|
||||
const resolvedModel = await resolveImageRouteModel(fullModel);
|
||||
const parsed = parseImageModel(resolvedModel);
|
||||
const providerConfig = parsed.provider ? getImageProvider(parsed.provider) : null;
|
||||
// Firefly nano/gpt-image accept multiple reference blobs; other non-Codex stay at 1.
|
||||
@@ -383,56 +387,6 @@ async function postHandler(request: Request, _context?: unknown) {
|
||||
: "This image edit provider currently supports only one reference image"
|
||||
);
|
||||
}
|
||||
// chatgpt-web keeps its conversation-continuation edit flow unchanged.
|
||||
if (providerConfig?.format === "chatgpt-web") {
|
||||
const credentials = await getProviderCredentialsWithQuotaPreflight(
|
||||
parsed.provider,
|
||||
null,
|
||||
allowedConnections,
|
||||
resolvedModel
|
||||
);
|
||||
if (!credentials) {
|
||||
return errorResponse(
|
||||
HTTP_STATUS.UNAUTHORIZED,
|
||||
`No credentials for provider: ${parsed.provider}`
|
||||
);
|
||||
}
|
||||
if (credentials.allRateLimited) {
|
||||
return unavailableResponse(
|
||||
HTTP_STATUS.RATE_LIMITED,
|
||||
`[${parsed.provider}] All accounts rate limited`,
|
||||
credentials.retryAfter,
|
||||
credentials.retryAfterHuman
|
||||
);
|
||||
}
|
||||
|
||||
const result = await handleImageEdit({
|
||||
provider: parsed.provider,
|
||||
model: parsed.model,
|
||||
body: {
|
||||
prompt,
|
||||
size: size ?? undefined,
|
||||
response_format: responseFormat ?? undefined,
|
||||
n: 1,
|
||||
},
|
||||
imageBytes,
|
||||
imageMime,
|
||||
credentials,
|
||||
log,
|
||||
signal: request.signal,
|
||||
clientHeaders: publicBaseUrlHeaders(request.headers),
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
await clearRecoveredProviderState(credentials);
|
||||
return jsonResponse((result as any).data);
|
||||
}
|
||||
return jsonResponse(
|
||||
toJsonErrorPayload((result as any).error, "Image edit provider error"),
|
||||
(result as any).status
|
||||
);
|
||||
}
|
||||
|
||||
// Built-in Codex uses its native Responses hosted tool for stateless reference-image edits.
|
||||
if (providerConfig?.format === "codex-responses") {
|
||||
const modelEntry = getImageModelEntry(resolvedModel);
|
||||
@@ -640,7 +594,7 @@ async function postHandler(request: Request, _context?: unknown) {
|
||||
return errorResponse(
|
||||
HTTP_STATUS.BAD_REQUEST,
|
||||
`Image edit is not supported for built-in provider "${parsed.provider}". ` +
|
||||
`Use adobe-firefly, chatgpt-web, codex, or a custom OpenAI-compatible image provider.`
|
||||
`Use adobe-firefly, codex, or a custom OpenAI-compatible image provider.`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,11 @@ import { enforceClientApiRouteAuth } from "@/shared/utils/clientApiRouteAuth";
|
||||
import { runWithCallLogApiKeyContext } from "@/lib/usage/callLogApiKeyContext";
|
||||
import { executeImageWithCredentialFallback } from "@/sse/services/imageCredentialRetry";
|
||||
import { AUTHZ_HEADER_PEER_LOCALITY } from "@/server/authz/headers";
|
||||
import {
|
||||
assertCommonChatGptWebModelAvailable,
|
||||
CHATGPT_WEB_RETIRED_ERROR_CODE,
|
||||
isCommonChatGptWebRetirementError,
|
||||
} from "@/shared/constants/chatgptWebRetirement";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -80,7 +85,7 @@ function hasImageGenerationInput(body: Record<string, unknown>) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Forward only the host-shaped headers the chatgpt-web image handler needs
|
||||
// Forward only the host-shaped headers the Gemini Web image handler needs
|
||||
// to derive the browser-facing public base URL. Avoid copying the full
|
||||
// request header set: it's wider than the handler needs (auth tokens,
|
||||
// content-type, etc.) and `Headers.forEach` collapses repeated values, which
|
||||
@@ -118,6 +123,19 @@ async function postHandler(request, context) {
|
||||
const authRejection = await enforceClientApiRouteAuth(request);
|
||||
if (authRejection) return authRejection;
|
||||
|
||||
// Fail closed on the raw wire id before combo/alias/custom-node remapping can erase it.
|
||||
try {
|
||||
assertCommonChatGptWebModelAvailable(body.model);
|
||||
} catch (error) {
|
||||
if (isCommonChatGptWebRetirementError(error)) {
|
||||
return errorResponse(error.status, error.message, {
|
||||
type: "provider_error",
|
||||
code: CHATGPT_WEB_RETIRED_ERROR_CODE,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Enforce API key policies (model restrictions + budget limits)
|
||||
const policy = await enforceApiKeyPolicy(request, body.model);
|
||||
if (policy.rejection) return policy.rejection;
|
||||
@@ -127,16 +145,8 @@ async function postHandler(request, context) {
|
||||
if (body.model && typeof body.model === "string" && !body.model.includes("/")) {
|
||||
const combo = await getComboByName(body.model as string);
|
||||
if (combo) {
|
||||
const { executeImageCombo } = await import(
|
||||
"@omniroute/open-sse/services/imageCombo"
|
||||
);
|
||||
return executeImageCombo(
|
||||
body.model as string,
|
||||
body,
|
||||
{ request, policy },
|
||||
startTime,
|
||||
log
|
||||
);
|
||||
const { executeImageCombo } = await import("@omniroute/open-sse/services/imageCombo");
|
||||
return executeImageCombo(body.model as string, body, { request, policy }, startTime, log);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +154,17 @@ async function postHandler(request, context) {
|
||||
// model (`myImg/gpt-image-2`) to its internal `<nodeId>/<model>` form so the
|
||||
// custom-model lookup and handler's resolvedProvider extraction resolve correctly.
|
||||
// Built-in and already-internal ids pass through unchanged. Shared with /images/edits.
|
||||
body.model = await resolveImageRouteModel(body.model);
|
||||
try {
|
||||
body.model = await resolveImageRouteModel(body.model);
|
||||
} catch (error) {
|
||||
if (isCommonChatGptWebRetirementError(error)) {
|
||||
return errorResponse(error.status, error.message, {
|
||||
type: "provider_error",
|
||||
code: CHATGPT_WEB_RETIRED_ERROR_CODE,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Parse model to get provider
|
||||
let { provider, model: requestedModel } = parseImageModel(body.model);
|
||||
@@ -246,7 +266,8 @@ async function postHandler(request, context) {
|
||||
provider,
|
||||
null,
|
||||
syncedEndpointRoute?.connectionIds ?? null,
|
||||
requestedModel );
|
||||
requestedModel
|
||||
);
|
||||
if (!credentials) {
|
||||
return errorResponse(
|
||||
HTTP_STATUS.BAD_REQUEST,
|
||||
@@ -346,7 +367,10 @@ async function postHandler(request, context) {
|
||||
});
|
||||
}
|
||||
|
||||
const errorPayload = toJsonErrorPayload((result as any).error, "Image generation provider error") as {
|
||||
const errorPayload = toJsonErrorPayload(
|
||||
(result as any).error,
|
||||
"Image generation provider error"
|
||||
) as {
|
||||
error?: { message?: string };
|
||||
};
|
||||
const message =
|
||||
|
||||
@@ -3,7 +3,9 @@ import { v1CountTokensSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { countTextTokens, type TokenizerContext } from "@/shared/utils/tiktokenCounter";
|
||||
import { getExecutor } from "@omniroute/open-sse/executors/index.ts";
|
||||
import { buildErrorBody } from "@omniroute/open-sse/utils/error.ts";
|
||||
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
|
||||
import { isCommonChatGptWebRetirementError } from "@/shared/constants/chatgptWebRetirement";
|
||||
import { getModelInfo } from "@/sse/services/model";
|
||||
import { extractApiKey, getProviderCredentials, isValidApiKey } from "@/sse/services/auth";
|
||||
import { safeResolveProxy } from "@/sse/handlers/chatHelpers";
|
||||
@@ -103,6 +105,20 @@ export async function POST(request) {
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
if (isCommonChatGptWebRetirementError(error)) {
|
||||
return new Response(
|
||||
JSON.stringify(
|
||||
buildErrorBody(error.status, error.message, null, {
|
||||
type: "provider_error",
|
||||
code: error.code,
|
||||
})
|
||||
),
|
||||
{
|
||||
status: error.status,
|
||||
headers: { "Content-Type": "application/json", ...CORS_HEADERS },
|
||||
}
|
||||
);
|
||||
}
|
||||
log.debug(
|
||||
"COUNT_TOKENS",
|
||||
`Falling back to estimate for ${requestedModel}: ${error instanceof Error ? error.message : String(error)}`
|
||||
|
||||
@@ -25,12 +25,9 @@ export async function HEAD() {
|
||||
* GET /v1/models/{model} — OpenAI-compatible single-model retrieval (#4674).
|
||||
*
|
||||
* Catch-all (`[...model]`) so provider-prefixed ids that contain a slash
|
||||
* (e.g. `cgpt-web/gpt-5.5`, `claude/claude-sonnet-4-6`) are captured intact.
|
||||
* (e.g. `openai/gpt-5.4`, `claude/claude-sonnet-4-6`) are captured intact.
|
||||
*/
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ model: string[] }> }
|
||||
) {
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ model: string[] }> }) {
|
||||
const { model } = await params;
|
||||
const segments = Array.isArray(model) ? model : [model];
|
||||
const requestedId = decodeURIComponent(segments.join("/"));
|
||||
|
||||
@@ -5,6 +5,7 @@ import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
|
||||
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
|
||||
import { getRegistryEntry } from "@omniroute/open-sse/config/providerRegistry.ts";
|
||||
import { withChatAdmission } from "@/shared/middleware/withChatAdmission";
|
||||
import { rejectRetiredCommonChatGptWebProvider } from "@/lib/providers/chatgptWebRetirementResponse";
|
||||
|
||||
let initialized = false;
|
||||
|
||||
@@ -34,6 +35,8 @@ export async function OPTIONS() {
|
||||
*/
|
||||
async function postHandler(request, { params }) {
|
||||
const { provider: rawProvider } = await params;
|
||||
const retirementResponse = rejectRetiredCommonChatGptWebProvider(rawProvider);
|
||||
if (retirementResponse) return retirementResponse;
|
||||
|
||||
const providerEntry = getRegistryEntry(rawProvider);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { enforceClientApiRouteAuth } from "@/shared/utils/clientApiRouteAuth";
|
||||
import { runWithCallLogApiKeyContext } from "@/lib/usage/callLogApiKeyContext";
|
||||
import { executeImageWithCredentialFallback } from "@/sse/services/imageCredentialRetry";
|
||||
import { rejectRetiredCommonChatGptWebProvider } from "@/lib/providers/chatgptWebRetirementResponse";
|
||||
|
||||
/**
|
||||
* Handle CORS preflight
|
||||
@@ -32,6 +33,8 @@ export async function OPTIONS() {
|
||||
*/
|
||||
export async function POST(request, { params }) {
|
||||
const { provider: rawProvider } = await params;
|
||||
const retirementResponse = rejectRetiredCommonChatGptWebProvider(rawProvider);
|
||||
if (retirementResponse) return retirementResponse;
|
||||
|
||||
// Verify this is a valid image provider
|
||||
const imageProvider = getImageProvider(rawProvider);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { isCommonChatGptWebRetirementError } from "@/shared/constants/chatgptWebRetirement";
|
||||
import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy";
|
||||
import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors";
|
||||
import {
|
||||
@@ -55,8 +56,7 @@ export const OPTIONS = async (): Promise<Response> => handleCorsOptions();
|
||||
export async function POST(request: Request): Promise<Response> {
|
||||
const apiKey = extractApiKey(request);
|
||||
if (!apiKey) return error(401, "LEASE_AUTHENTICATION_REQUIRED", "Authentication required");
|
||||
if (!(await isValidApiKey(apiKey)))
|
||||
return error(401, "LEASE_API_KEY_INVALID", "Invalid API key");
|
||||
if (!(await isValidApiKey(apiKey))) return error(401, "LEASE_API_KEY_INVALID", "Invalid API key");
|
||||
const contentType = request.headers.get("content-type")?.toLowerCase().split(";", 1)[0].trim();
|
||||
if (contentType !== "application/json") {
|
||||
return error(415, "LEASE_CONTENT_TYPE_REQUIRED", "Content-Type must be application/json");
|
||||
@@ -65,6 +65,18 @@ export async function POST(request: Request): Promise<Response> {
|
||||
const parsed = actionSchema.safeParse(await request.json().catch(() => null));
|
||||
if (!parsed.success) return error(400, "LEASE_ACTION_INVALID", "Invalid lease lifecycle action");
|
||||
|
||||
let acquisitionModelInfo: Awaited<ReturnType<typeof getModelInfo>> | null = null;
|
||||
if (parsed.data.action === "acquire") {
|
||||
try {
|
||||
acquisitionModelInfo = await getModelInfo(parsed.data.model);
|
||||
} catch (cause) {
|
||||
if (isCommonChatGptWebRetirementError(cause)) {
|
||||
return error(cause.status, cause.code, cause.message);
|
||||
}
|
||||
return error(503, "LEASE_SERVICE_UNAVAILABLE", "Lease service unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
const policy = await enforceApiKeyPolicy(
|
||||
request,
|
||||
parsed.data.action === "acquire" ? parsed.data.model : null
|
||||
@@ -91,7 +103,7 @@ export async function POST(request: Request): Promise<Response> {
|
||||
: error(409, "LEASE_FENCE_STALE", "The lease generation is stale");
|
||||
}
|
||||
|
||||
const modelInfo = await getModelInfo(parsed.data.model);
|
||||
const modelInfo = acquisitionModelInfo!;
|
||||
if (!modelInfo.provider) return error(400, "LEASE_MODEL_INVALID", "The model is unavailable");
|
||||
const selection = await getProviderCredentialsWithQuotaPreflight(
|
||||
modelInfo.provider,
|
||||
@@ -133,6 +145,9 @@ export async function POST(request: Request): Promise<Response> {
|
||||
const result = selection as ExclusiveLeaseSelectionResult;
|
||||
return json(200, lifecycle(result.exclusiveLease));
|
||||
} catch (cause) {
|
||||
if (isCommonChatGptWebRetirementError(cause)) {
|
||||
return error(cause.status, cause.code, cause.message);
|
||||
}
|
||||
if (cause instanceof LeaseContextError) return error(cause.status, cause.code, cause.message);
|
||||
return error(503, "LEASE_SERVICE_UNAVAILABLE", "Lease service unavailable");
|
||||
}
|
||||
|
||||
195
src/lib/db/migrations/163_retire_chatgpt_web.sql
Normal file
195
src/lib/db/migrations/163_retire_chatgpt_web.sql
Normal file
@@ -0,0 +1,195 @@
|
||||
-- Retire the common ChatGPT Web integration because its inherited provenance cannot be cleared.
|
||||
--
|
||||
-- Match the complete ECMAScript trim whitespace set so database tombstones and
|
||||
-- the TypeScript runtime agree even for restored provider ids wrapped in Unicode
|
||||
-- spaces (NBSP, OGHAM, U+2000..U+200A, line/paragraph separators and BOM).
|
||||
--
|
||||
-- Keep connection rows and historical records for auditability. Disabling the
|
||||
-- connections is deliberately fail-closed: API-key allowed_connections entries
|
||||
-- continue to reference the same connection ids instead of becoming an empty
|
||||
-- allowlist, which would mean unrestricted access in the policy layer.
|
||||
|
||||
UPDATE exclusive_connection_leases
|
||||
SET state = 'INVALIDATED',
|
||||
ended_at = datetime('now'),
|
||||
end_reason = 'CONNECTION_INELIGIBLE'
|
||||
WHERE state = 'ACTIVE'
|
||||
AND (
|
||||
lower(trim(provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
|
||||
IN ('chatgpt-web', 'cgpt-web')
|
||||
OR connection_id IN (
|
||||
SELECT id
|
||||
FROM provider_connections
|
||||
WHERE lower(trim(provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
|
||||
IN ('chatgpt-web', 'cgpt-web')
|
||||
)
|
||||
);
|
||||
|
||||
UPDATE provider_connections
|
||||
SET is_active = 0,
|
||||
test_status = 'unavailable',
|
||||
error_code = 'PROVIDER_REMOVED',
|
||||
last_error = 'Provider integration retired from OmniRoute v3.8.50',
|
||||
last_error_type = 'provider_removed',
|
||||
last_error_source = 'migration:retire-chatgpt-web',
|
||||
last_error_at = datetime('now'),
|
||||
updated_at = datetime('now')
|
||||
WHERE lower(trim(provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
|
||||
IN ('chatgpt-web', 'cgpt-web')
|
||||
AND (
|
||||
is_active IS NOT 0
|
||||
OR test_status IS NOT 'unavailable'
|
||||
OR error_code IS NOT 'PROVIDER_REMOVED'
|
||||
OR last_error IS NOT 'Provider integration retired from OmniRoute v3.8.50'
|
||||
OR last_error_type IS NOT 'provider_removed'
|
||||
OR last_error_source IS NOT 'migration:retire-chatgpt-web'
|
||||
OR last_error_at IS NULL
|
||||
);
|
||||
|
||||
-- Migrations run before settings imports. Keep the tombstone durable when an
|
||||
-- old db.json snapshot or an admin PATCH later attempts to reactivate either
|
||||
-- retired id. The WHEN predicates are null-safe and prevent timestamp churn
|
||||
-- when an already-normalized row is written again.
|
||||
CREATE TRIGGER IF NOT EXISTS provider_connections_retire_chatgpt_web_insert
|
||||
AFTER INSERT ON provider_connections
|
||||
WHEN lower(trim(NEW.provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
|
||||
IN ('chatgpt-web', 'cgpt-web')
|
||||
BEGIN
|
||||
UPDATE provider_connections
|
||||
SET is_active = 0,
|
||||
test_status = 'unavailable',
|
||||
error_code = 'PROVIDER_REMOVED',
|
||||
last_error = 'Provider integration retired from OmniRoute v3.8.50',
|
||||
last_error_type = 'provider_removed',
|
||||
last_error_source = 'migration:retire-chatgpt-web',
|
||||
last_error_at = datetime('now'),
|
||||
updated_at = datetime('now')
|
||||
WHERE id = NEW.id
|
||||
AND (
|
||||
is_active IS NOT 0
|
||||
OR test_status IS NOT 'unavailable'
|
||||
OR error_code IS NOT 'PROVIDER_REMOVED'
|
||||
OR last_error IS NOT 'Provider integration retired from OmniRoute v3.8.50'
|
||||
OR last_error_type IS NOT 'provider_removed'
|
||||
OR last_error_source IS NOT 'migration:retire-chatgpt-web'
|
||||
OR last_error_at IS NULL
|
||||
);
|
||||
|
||||
UPDATE exclusive_connection_leases
|
||||
SET state = 'INVALIDATED',
|
||||
ended_at = datetime('now'),
|
||||
end_reason = 'CONNECTION_INELIGIBLE'
|
||||
WHERE state = 'ACTIVE'
|
||||
AND connection_id = NEW.id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS provider_connections_retire_chatgpt_web_update
|
||||
AFTER UPDATE OF provider, is_active, test_status, error_code, last_error,
|
||||
last_error_type, last_error_source, last_error_at ON provider_connections
|
||||
WHEN lower(trim(NEW.provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
|
||||
IN ('chatgpt-web', 'cgpt-web')
|
||||
BEGIN
|
||||
UPDATE provider_connections
|
||||
SET is_active = 0,
|
||||
test_status = 'unavailable',
|
||||
error_code = 'PROVIDER_REMOVED',
|
||||
last_error = 'Provider integration retired from OmniRoute v3.8.50',
|
||||
last_error_type = 'provider_removed',
|
||||
last_error_source = 'migration:retire-chatgpt-web',
|
||||
last_error_at = datetime('now'),
|
||||
updated_at = datetime('now')
|
||||
WHERE id = NEW.id
|
||||
AND (
|
||||
is_active IS NOT 0
|
||||
OR test_status IS NOT 'unavailable'
|
||||
OR error_code IS NOT 'PROVIDER_REMOVED'
|
||||
OR last_error IS NOT 'Provider integration retired from OmniRoute v3.8.50'
|
||||
OR last_error_type IS NOT 'provider_removed'
|
||||
OR last_error_source IS NOT 'migration:retire-chatgpt-web'
|
||||
OR last_error_at IS NULL
|
||||
);
|
||||
|
||||
UPDATE exclusive_connection_leases
|
||||
SET state = 'INVALIDATED',
|
||||
ended_at = datetime('now'),
|
||||
end_reason = 'CONNECTION_INELIGIBLE'
|
||||
WHERE state = 'ACTIVE'
|
||||
AND connection_id = NEW.id;
|
||||
END;
|
||||
|
||||
-- Once a connection id belongs to a retired provider, imports and internal
|
||||
-- writers must not repurpose that same audited identity as another provider.
|
||||
-- Retired-to-retired normalization remains allowed and is re-tombstoned by the
|
||||
-- AFTER UPDATE trigger above.
|
||||
CREATE TRIGGER IF NOT EXISTS provider_connections_preserve_chatgpt_web_identity_insert
|
||||
BEFORE INSERT ON provider_connections
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM provider_connections
|
||||
WHERE id = NEW.id
|
||||
AND lower(trim(provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
|
||||
IN ('chatgpt-web', 'cgpt-web')
|
||||
)
|
||||
AND lower(trim(NEW.provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
|
||||
NOT IN ('chatgpt-web', 'cgpt-web')
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'Retired provider connection identity cannot be changed');
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS provider_connections_preserve_chatgpt_web_identity_update
|
||||
BEFORE UPDATE OF provider ON provider_connections
|
||||
WHEN lower(trim(OLD.provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
|
||||
IN ('chatgpt-web', 'cgpt-web')
|
||||
AND lower(trim(NEW.provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
|
||||
NOT IN ('chatgpt-web', 'cgpt-web')
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'Retired provider connection identity cannot be changed');
|
||||
END;
|
||||
|
||||
-- A restore can also insert lease rows after migrations have completed. Keep
|
||||
-- lease state fail-closed independently of request-time auth selection.
|
||||
CREATE TRIGGER IF NOT EXISTS exclusive_connection_leases_retire_chatgpt_web_insert
|
||||
AFTER INSERT ON exclusive_connection_leases
|
||||
WHEN NEW.state = 'ACTIVE'
|
||||
AND (
|
||||
lower(trim(NEW.provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
|
||||
IN ('chatgpt-web', 'cgpt-web')
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM provider_connections
|
||||
WHERE id = NEW.connection_id
|
||||
AND lower(trim(provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
|
||||
IN ('chatgpt-web', 'cgpt-web')
|
||||
)
|
||||
)
|
||||
BEGIN
|
||||
UPDATE exclusive_connection_leases
|
||||
SET state = 'INVALIDATED',
|
||||
ended_at = datetime('now'),
|
||||
end_reason = 'CONNECTION_INELIGIBLE'
|
||||
WHERE id = NEW.id
|
||||
AND state = 'ACTIVE';
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS exclusive_connection_leases_retire_chatgpt_web_update
|
||||
AFTER UPDATE OF provider, connection_id, state ON exclusive_connection_leases
|
||||
WHEN NEW.state = 'ACTIVE'
|
||||
AND (
|
||||
lower(trim(NEW.provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
|
||||
IN ('chatgpt-web', 'cgpt-web')
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM provider_connections
|
||||
WHERE id = NEW.connection_id
|
||||
AND lower(trim(provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
|
||||
IN ('chatgpt-web', 'cgpt-web')
|
||||
)
|
||||
)
|
||||
BEGIN
|
||||
UPDATE exclusive_connection_leases
|
||||
SET state = 'INVALIDATED',
|
||||
ended_at = datetime('now'),
|
||||
end_reason = 'CONNECTION_INELIGIBLE'
|
||||
WHERE id = NEW.id
|
||||
AND state = 'ACTIVE';
|
||||
END;
|
||||
@@ -3,6 +3,8 @@
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { isCommonChatGptWebRetiredProviderId } from "@/shared/constants/chatgptWebRetirement";
|
||||
import { getDbInstance, rowToCamel, cleanNulls } from "./core";
|
||||
import { backupDbFile } from "./backup";
|
||||
import {
|
||||
@@ -595,13 +597,19 @@ export async function createProviderConnection(data: JsonRecord) {
|
||||
_updateConnectionRow(db, existingId, encryptConnectionFields(persistence));
|
||||
})();
|
||||
backupDbFile("pre-write");
|
||||
return withNullableRateLimitOverrides(
|
||||
const returnedConnection = withNullableRateLimitOverrides(
|
||||
withNullableQuotaWindowThresholds(
|
||||
withNullableMaxConcurrent(cleanNulls(merged), merged),
|
||||
merged
|
||||
),
|
||||
merged
|
||||
);
|
||||
|
||||
if (isCommonChatGptWebRetiredProviderId(merged.provider)) {
|
||||
return (await getProviderConnectionById(existingId)) ?? returnedConnection;
|
||||
}
|
||||
|
||||
return returnedConnection;
|
||||
}
|
||||
|
||||
// Generate name: prefer explicit name, then email, then a stable short-ID label.
|
||||
@@ -722,13 +730,19 @@ export async function createProviderConnection(data: JsonRecord) {
|
||||
backupDbFile("pre-write");
|
||||
invalidateDbCache("connections"); // Bust connections read cache
|
||||
|
||||
return withNullableRateLimitOverrides(
|
||||
const returnedConnection = withNullableRateLimitOverrides(
|
||||
withNullableQuotaWindowThresholds(
|
||||
withNullableMaxConcurrent(cleanNulls(connection), connection),
|
||||
connection
|
||||
),
|
||||
connection
|
||||
);
|
||||
|
||||
if (isCommonChatGptWebRetiredProviderId(providerId)) {
|
||||
return (await getProviderConnectionById(String(connection.id))) ?? returnedConnection;
|
||||
}
|
||||
|
||||
return returnedConnection;
|
||||
}
|
||||
|
||||
function _insertConnectionRow(db: DbLike, conn: JsonRecord) {
|
||||
@@ -967,13 +981,19 @@ export async function updateProviderConnection(id: string, data: JsonRecord) {
|
||||
reorderConnections(db, providerId);
|
||||
}
|
||||
|
||||
return withNullableRateLimitOverrides(
|
||||
const returnedConnection = withNullableRateLimitOverrides(
|
||||
withNullableQuotaWindowThresholds(
|
||||
withNullableMaxConcurrent(cleanNulls(merged), merged),
|
||||
merged
|
||||
),
|
||||
merged
|
||||
);
|
||||
|
||||
if (isCommonChatGptWebRetiredProviderId(merged.provider)) {
|
||||
return (await getProviderConnectionById(id)) ?? returnedConnection;
|
||||
}
|
||||
|
||||
return returnedConnection;
|
||||
}
|
||||
|
||||
export {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* 1. Bare combo / alias name with no slash (`image`) — resolved to the combo's single
|
||||
* image target, then that target is itself prefix-resolved. Bare combos intentionally
|
||||
* override built-in image aliases with the same name.
|
||||
* 2. Built-in image model id / alias (`cgpt-web/...`, `gpt-image-1`, …) — untouched.
|
||||
* 2. Built-in image model id / alias (`openai/gpt-image-2`, `gpt-image-1`, etc.) — untouched.
|
||||
* 3. Custom provider *prefix* form (`myImg/gpt-image-2`) — rewritten to the internal
|
||||
* `<nodeId>/<model>` id (#3205 did this inline in the generations route only).
|
||||
*
|
||||
@@ -19,6 +19,7 @@ import { resolveComboTargets } from "@omniroute/open-sse/services/combo.ts";
|
||||
|
||||
import { getComboByName, getCombos } from "@/lib/db/combos";
|
||||
import { getCachedProviderNodes } from "@/lib/localDb";
|
||||
import { assertCommonChatGptWebModelAvailable } from "@/shared/constants/chatgptWebRetirement";
|
||||
|
||||
/**
|
||||
* Rewrite a `prefix/model` custom image model to its internal `<nodeId>/<model>` form.
|
||||
@@ -28,6 +29,7 @@ import { getCachedProviderNodes } from "@/lib/localDb";
|
||||
*/
|
||||
export async function resolveImageModelPrefix(modelStr: string): Promise<string> {
|
||||
if (typeof modelStr !== "string") return modelStr;
|
||||
assertCommonChatGptWebModelAvailable(modelStr);
|
||||
const slash = modelStr.indexOf("/");
|
||||
if (slash <= 0) return modelStr;
|
||||
|
||||
@@ -75,6 +77,7 @@ export async function resolveSingleImageComboTarget(name: string): Promise<strin
|
||||
*/
|
||||
export async function resolveImageRouteModel(modelStr: string): Promise<string> {
|
||||
if (typeof modelStr !== "string" || !modelStr.trim()) return modelStr;
|
||||
assertCommonChatGptWebModelAvailable(modelStr);
|
||||
const parsedModel = parseImageModel(modelStr);
|
||||
const hasSlash = modelStr.includes("/");
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ const SENSITIVE_KEYS = new Set([
|
||||
"secret",
|
||||
"token",
|
||||
// secret-leak hardening: session cookies + browser-storage credentials that
|
||||
// some web-impersonation providers (Meta AI ecto_1_sess, chatgpt-web
|
||||
// some web-impersonation providers (Meta AI ecto_1_sess, Perplexity Web
|
||||
// storageState / runtimeKey) can surface into a request/response BODY field
|
||||
// rather than a header. Header-borne values are already masked by
|
||||
// maskSensitiveHeaders; this covers the body path into the on-disk call-log
|
||||
|
||||
@@ -35,8 +35,11 @@
|
||||
* Built-in/no-compatible catalog entries are always eligible.
|
||||
*/
|
||||
|
||||
import { REGISTRY } from "@omniroute/open-sse/config/providerRegistry.ts";
|
||||
import { getProviderNodes } from "@/lib/db/providers/nodes";
|
||||
import {
|
||||
getReservedProviderPrefixes,
|
||||
isReservedProviderPrefix,
|
||||
} from "@/shared/constants/reservedProviderPrefixes";
|
||||
|
||||
export type ProviderPrefixStatus = "unique" | "ambiguous" | "reserved";
|
||||
|
||||
@@ -66,12 +69,7 @@ export interface ProviderPrefixIndex {
|
||||
* prefixes can never shadow a built-in provider.
|
||||
*/
|
||||
export function buildReservedPrefixes(): Set<string> {
|
||||
const reserved = new Set<string>();
|
||||
for (const entry of Object.values(REGISTRY)) {
|
||||
if (entry?.id) reserved.add(entry.id);
|
||||
if (entry?.alias) reserved.add(entry.alias);
|
||||
}
|
||||
return reserved;
|
||||
return new Set(getReservedProviderPrefixes());
|
||||
}
|
||||
|
||||
export interface CompatibleNodeLike {
|
||||
@@ -97,7 +95,6 @@ export function selectCompatibleNodeForPrefix(
|
||||
}
|
||||
|
||||
export async function getProviderPrefixIndex(): Promise<ProviderPrefixIndex> {
|
||||
const reserved = buildReservedPrefixes();
|
||||
const nodes = (await getProviderNodes()) as CompatibleNodeLike[];
|
||||
const compatible = nodes.filter(
|
||||
(n) => n.type === "openai-compatible" || n.type === "anthropic-compatible"
|
||||
@@ -123,8 +120,9 @@ export async function getProviderPrefixIndex(): Promise<ProviderPrefixIndex> {
|
||||
const eligibleNodeIds = new Set<string>();
|
||||
|
||||
for (const [prefix, prefixNodes] of byPrefix) {
|
||||
if (reserved.has(prefix)) {
|
||||
// Built-in registry id/alias — never a compatible public target.
|
||||
if (isReservedProviderPrefix(prefix)) {
|
||||
// Built-in registry id/alias or case-insensitive retired id — never a
|
||||
// compatible public target.
|
||||
entries.set(prefix, { prefix, status: "reserved" });
|
||||
continue;
|
||||
}
|
||||
|
||||
30
src/lib/providers/chatgptWebRetirementResponse.ts
Normal file
30
src/lib/providers/chatgptWebRetirementResponse.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
|
||||
|
||||
import {
|
||||
assertCommonChatGptWebProviderAvailable,
|
||||
CHATGPT_WEB_RETIRED_ERROR_CODE,
|
||||
CHATGPT_WEB_RETIRED_MESSAGE,
|
||||
isCommonChatGptWebRetiredProviderId,
|
||||
isCommonChatGptWebRetirementError,
|
||||
} from "@/shared/constants/chatgptWebRetirement";
|
||||
|
||||
export function commonChatGptWebRetirementResponse(): Response {
|
||||
return errorResponse(410, CHATGPT_WEB_RETIRED_MESSAGE, {
|
||||
type: "provider_error",
|
||||
code: CHATGPT_WEB_RETIRED_ERROR_CODE,
|
||||
});
|
||||
}
|
||||
|
||||
export function rejectRetiredCommonChatGptWebProvider(providerId: unknown): Response | null {
|
||||
return isCommonChatGptWebRetiredProviderId(providerId)
|
||||
? commonChatGptWebRetirementResponse()
|
||||
: null;
|
||||
}
|
||||
|
||||
export function assertProviderAvailable(providerId: unknown): void {
|
||||
assertCommonChatGptWebProviderAvailable(providerId);
|
||||
}
|
||||
|
||||
export function responseForError(error: unknown): Response | null {
|
||||
return isCommonChatGptWebRetirementError(error) ? commonChatGptWebRetirementResponse() : null;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ const TOOL_ONLY_SERVICE_KINDS = new Set<string>(["webSearch", "webFetch"]);
|
||||
* are intentionally NOT curated: their model list is discovered live from the
|
||||
* console API (see volcenginePlanModelDiscovery.ts) and merged into the synced
|
||||
* catalog, so the static registry only acts as a capability-seed fallback. */
|
||||
const CURATED_MODEL_ONLY_PROVIDERS = new Set<string>(["chatgpt-web", "kimi-web", "zai-web"]);
|
||||
const CURATED_MODEL_ONLY_PROVIDERS = new Set<string>(["kimi-web", "zai-web"]);
|
||||
|
||||
export function providerUsesCuratedModelsOnly(providerId: string): boolean {
|
||||
return CURATED_MODEL_ONLY_PROVIDERS.has(providerId.trim().toLowerCase());
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
validateDeepSeekWebProvider,
|
||||
validateQwenWebProvider,
|
||||
validateGrokWebProvider,
|
||||
validateChatGptWebProvider,
|
||||
validatePerplexityWebProvider,
|
||||
validateBlackboxWebProvider,
|
||||
validateKimiWebProvider,
|
||||
@@ -166,7 +165,11 @@ export async function validateFreebuffProvider({ apiKey }: { apiKey: string }) {
|
||||
return { valid: false, error: "Invalid or expired Freebuff Auth Token", unsupported: false };
|
||||
}
|
||||
const errText = await res.text().catch(() => "");
|
||||
return { valid: false, error: `Freebuff validation returned ${res.status}: ${errText.slice(0, 100)}`, unsupported: false };
|
||||
return {
|
||||
valid: false,
|
||||
error: `Freebuff validation returned ${res.status}: ${errText.slice(0, 100)}`,
|
||||
unsupported: false,
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
return { valid: false, error: `Freebuff validation network error: ${msg}`, unsupported: false };
|
||||
@@ -307,7 +310,6 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi
|
||||
"grok-web": validateGrokWebProvider,
|
||||
"qwen-web": validateQwenWebProvider,
|
||||
"kimi-web": validateKimiWebProvider,
|
||||
"chatgpt-web": validateChatGptWebProvider,
|
||||
"chatgpt-web-codex": validateChatGptWebCodexProvider,
|
||||
"perplexity-web": validatePerplexityWebProvider,
|
||||
"blackbox-web": validateBlackboxWebProvider,
|
||||
@@ -376,7 +378,7 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi
|
||||
|
||||
// Web-cookie providers WITHOUT a dedicated specialty validator above fall back to the generic
|
||||
// session-ping check (AUTH_007 SESSION_EXPIRED on 401/403). Providers that DO have a rich
|
||||
// per-provider validator (grok-web, chatgpt-web, claude-web, …) are handled by
|
||||
// per-provider validator (grok-web, perplexity-web, claude-web, etc.) are handled by
|
||||
// SPECIALTY_VALIDATORS first and must not be shadowed by this generic probe (issue: the
|
||||
// #4023 dispatch was placed too early and intercepted every web-cookie provider).
|
||||
const canonicalProvider = resolveProviderId(provider);
|
||||
|
||||
@@ -120,7 +120,6 @@ const WEB_COOKIE_PROVIDERS_WITH_UNRELIABLE_MODELS_PROBE = new Set(["lmarena"]);
|
||||
// WEB_COOKIE_PROVIDERS_WITH_UNRELIABLE_MODELS_PROBE/REDIRECT_BLOCKED path above (#7542).
|
||||
export const WEB_COOKIE_PROVIDERS_WITHOUT_MODELS_API = new Set([
|
||||
"huggingchat",
|
||||
"chatgpt-web",
|
||||
"grok-web",
|
||||
"notion-web",
|
||||
"t3-web",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Web-cookie provider key validators (part A): deepseek-web, qwen-web, grok-web, chatgpt-web,
|
||||
// Web-cookie provider key validators (part A): deepseek-web, qwen-web, grok-web,
|
||||
// perplexity-web, blackbox-web. Extracted from validation.ts (god-file decomposition) — top-level
|
||||
// functions with no dispatcher-state captures; behavior is byte-identical to the original inline defs.
|
||||
import { addModelsSuffix } from "./urlHelpers";
|
||||
@@ -488,114 +488,6 @@ export async function validateGrokWebProvider({ apiKey, providerSpecificData = {
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateChatGptWebProvider({ apiKey, providerSpecificData = {} }: any) {
|
||||
try {
|
||||
// Accept bare value, unchunked cookie, chunked (.0/.1) cookies, or full
|
||||
// "Cookie: ..." DevTools line. Pass through verbatim once recognised.
|
||||
let cookieHeader = String(apiKey || "").trim();
|
||||
if (/^cookie\s*:\s*/i.test(cookieHeader)) {
|
||||
cookieHeader = cookieHeader.replace(/^cookie\s*:\s*/i, "");
|
||||
}
|
||||
if (!/__Secure-next-auth\.session-token(?:\.\d+)?\s*=/.test(cookieHeader)) {
|
||||
cookieHeader = `__Secure-next-auth.session-token=${cookieHeader}`;
|
||||
}
|
||||
|
||||
// Use the TLS-impersonating client — Cloudflare on chatgpt.com pins
|
||||
// cf_clearance to JA3/JA4 + HTTP/2 SETTINGS, so plain Node fetch always
|
||||
// gets cf-mitigated: challenge regardless of cookies.
|
||||
const { tlsFetchChatGpt, TlsClientUnavailableError } =
|
||||
await import("@omniroute/open-sse/services/chatgptTlsClient.ts");
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await tlsFetchChatGpt("https://chatgpt.com/api/auth/session", {
|
||||
method: "GET",
|
||||
headers: applyCustomUserAgent(
|
||||
{
|
||||
Accept: "application/json",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Cache-Control": "no-cache",
|
||||
Cookie: cookieHeader,
|
||||
Origin: "https://chatgpt.com",
|
||||
Pragma: "no-cache",
|
||||
Referer: "https://chatgpt.com/",
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Site": "same-origin",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:152.0) Gecko/20100101 Firefox/152.0",
|
||||
},
|
||||
providerSpecificData
|
||||
),
|
||||
timeoutMs: 30_000,
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (err instanceof TlsClientUnavailableError) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `${err.message} (chatgpt-web requires this — without it, Cloudflare blocks every request)`,
|
||||
};
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
const cfRay = response.headers.get("cf-ray");
|
||||
const cfMitigated = response.headers.get("cf-mitigated");
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
const bodyText = response.text || "";
|
||||
if (cfMitigated || /just a moment|cloudflare|cf-chl|attention required/i.test(bodyText)) {
|
||||
return {
|
||||
valid: false,
|
||||
error:
|
||||
"Cloudflare blocked the validator — open chatgpt.com in your browser, then copy the FULL Cookie line from DevTools (Network → request → Cookie) including cf_clearance, __cf_bm, _cfuvid, and the session-token chunks.",
|
||||
};
|
||||
}
|
||||
return {
|
||||
valid: false,
|
||||
error:
|
||||
"Invalid ChatGPT session cookie — re-paste __Secure-next-auth.session-token from chatgpt.com DevTools → Cookies",
|
||||
};
|
||||
}
|
||||
|
||||
if (response.status >= 500) {
|
||||
return { valid: false, error: `ChatGPT unavailable (${response.status})` };
|
||||
}
|
||||
|
||||
if (response.status >= 400) {
|
||||
return { valid: false, error: `Validation failed: ${response.status}` };
|
||||
}
|
||||
|
||||
if (!contentType.includes("json")) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `ChatGPT returned non-JSON (${contentType || "no content-type"}${cfRay ? `, cf-ray=${cfRay}` : ""}) — paste the FULL Cookie line including cf_clearance, __cf_bm, _cfuvid alongside the session-token chunks.`,
|
||||
};
|
||||
}
|
||||
|
||||
let data: any = {};
|
||||
try {
|
||||
data = JSON.parse(response.text || "{}");
|
||||
} catch {
|
||||
return {
|
||||
valid: false,
|
||||
error:
|
||||
"ChatGPT session response was not JSON — paste the FULL Cookie line including cf_clearance and __cf_bm.",
|
||||
};
|
||||
}
|
||||
if (!data?.accessToken) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "ChatGPT session expired — log into chatgpt.com and copy a fresh cookie",
|
||||
};
|
||||
}
|
||||
return { valid: true, error: null };
|
||||
} catch (error: any) {
|
||||
return toValidationErrorResult(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function validatePerplexityWebProvider({ apiKey, providerSpecificData = {} }: any) {
|
||||
try {
|
||||
let sessionToken = apiKey;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Flat-rate (subscription / cookie-web) provider classification — issue #5552.
|
||||
*
|
||||
* Some providers are billed at a flat rate (a subscription or a coding plan),
|
||||
* not per token: cookie/web sessions (ChatGPT Web, grok-web, …) are backed by a
|
||||
* not per token: cookie/web sessions (ChatGPT Web (Codex), grok-web, …) are backed by a
|
||||
* consumer subscription, and several "Coding Plan" providers (Codex, MiniMax
|
||||
* Coding, Kimi Coding, GLM Coding, …) bill a fixed monthly fee. These providers
|
||||
* still carry per-token pricing rows (used for pre-flight estimates), so cost
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user