diff --git a/.env.example b/.env.example
index 032dabfd57..9f2869678c 100644
--- a/.env.example
+++ b/.env.example
@@ -1806,6 +1806,15 @@ APP_LOG_TO_FILE=true
# Skip writing to the hosts file when adding/removing DNS entries (e.g. sandboxed
# or read-only test environments). Used by: src/mitm/dns/dnsConfig.ts.
# OMNIROUTE_SKIP_DNS_WRITE=1
+# Opt in to the root-CA + per-host-leaf cert model for the MITM proxy (#6684).
+# Fresh installs and installs with this set to "true" get a persisted root CA that
+# signs per-host leaves; installs with a pre-existing trusted legacy leaf keep the
+# legacy fixed-SAN cert unless opted in. Used by: src/mitm/manager.ts.
+# MITM_ROOT_CA_ENABLED=true
+# Set BY the MITM manager for the spawned proxy process ("root-ca" | "legacy") —
+# reflects the migration decision above; not meant to be set manually.
+# Read by: src/mitm/server.cjs.
+# MITM_CERT_MODE=legacy
# ── Test/CI-only guards (never needed in production) ──
# Set automatically by tests/_setup/isolateDataDir.ts and the CI workflows: the
@@ -1825,6 +1834,14 @@ APP_LOG_TO_FILE=true
# ONEPROXY_MAX_PROXIES=500
# ONEPROXY_MIN_QUALITY_THRESHOLD=50
+# ── Free Proxy Pool (auto-sync scheduler) ──
+# Background refresh of the free-proxy pool. Opt-in, OFF by default (parallels
+# Hard Rule #20's default-off posture for data-mutating background features).
+# Used by: src/lib/freeProxyProviders/scheduler.ts
+# FREE_PROXY_AUTO_SYNC_ENABLED=true
+# Sync interval in ms (default: 1800000 = 30 min).
+# FREE_PROXY_AUTO_SYNC_INTERVAL_MS=1800000
+
# ── Free Proxy Pool (1proxy source) ──
# Used by: src/lib/freeProxyProviders/oneproxy.ts
# Set FREE_PROXY_1PROXY_ENABLED=false to disable this source.
diff --git a/README.md b/README.md
index 3ebc463ab4..e53aadc503 100644
--- a/README.md
+++ b/README.md
@@ -18,9 +18,11 @@
> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute aggregates the **documented** free tiers of **39 provider pools / 460+ models** into one honest number and shows it live on the dashboard (`/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)**.
+>
+> These figures are re-audited every two weeks against the live catalog and **move both ways** — a provider ends a free tier and the number drops; a new one lands and it climbs. We publish what the catalog actually computes, never a rounded-up best case. A CI gate (`check:docs-counts`) fails the build if this headline drifts from the code.
diff --git a/config/quality/dashboard-typecheck-baseline.json b/config/quality/dashboard-typecheck-baseline.json
index 4f7857d6ce..20617731af 100644
--- a/config/quality/dashboard-typecheck-baseline.json
+++ b/config/quality/dashboard-typecheck-baseline.json
@@ -63,9 +63,6 @@
"src/app/(dashboard)/dashboard/compression/studio/EncoderComparisonTable.tsx": {
"TS2322": 1
},
- "src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx": {
- "TS2304": 1
- },
"src/app/(dashboard)/dashboard/costs/quota-share/QuotaSharePageClient.tsx": {
"TS2551": 7,
"TS2322": 2,
@@ -184,12 +181,6 @@
"src/app/(dashboard)/dashboard/settings/components/RedisLauncherPanel.tsx": {
"TS2345": 11
},
- "src/app/(dashboard)/dashboard/settings/components/SidebarTab.tsx": {
- "TS2322": 1
- },
- "src/app/(dashboard)/dashboard/settings/components/proxy/FreePoolTab.tsx": {
- "TS2304": 1
- },
"src/app/(dashboard)/dashboard/tools/traffic-inspector/components/CustomHostsManager.tsx": {
"TS2339": 1
},
diff --git a/docs/diagrams/README.md b/docs/diagrams/README.md
index dee34594f5..0fa9f35bca 100644
--- a/docs/diagrams/README.md
+++ b/docs/diagrams/README.md
@@ -16,7 +16,7 @@ Mermaid sources (`.mmd`) and exported SVGs for OmniRoute v3.8.0 architecture flo
| [auto-combo-12factor.mmd](./auto-combo-12factor.mmd) | [SVG](./exported/auto-combo-12factor.svg) | docs/routing/AUTO-COMBO.md |
| [resilience-3layers.mmd](./resilience-3layers.mmd) | [SVG](./exported/resilience-3layers.svg) | docs/architecture/RESILIENCE_GUIDE.md, CLAUDE.md |
| [i18n-flow.mmd](./i18n-flow.mmd) | [SVG](./exported/i18n-flow.svg) | docs/guides/I18N.md |
-| [mcp-tools-94.mmd](./mcp-tools-94.mmd) | [SVG](./exported/mcp-tools-94.svg) | docs/frameworks/MCP-SERVER.md |
+| [mcp-tools-104.mmd](./mcp-tools-104.mmd) | [SVG](./exported/mcp-tools-104.svg) | docs/frameworks/MCP-SERVER.md |
| [cloud-agent-flow.mmd](./cloud-agent-flow.mmd) | [SVG](./exported/cloud-agent-flow.svg) | docs/frameworks/CLOUD_AGENT.md |
| [authz-pipeline.mmd](./authz-pipeline.mmd) | [SVG](./exported/authz-pipeline.svg) | docs/architecture/AUTHZ_GUIDE.md |
| [db-schema-overview.mmd](./db-schema-overview.mmd) | [SVG](./exported/db-schema-overview.svg) | docs/architecture/CODEBASE_DOCUMENTATION.md |
@@ -34,7 +34,7 @@ inside GitHub's `
` sandbox:
| [combo-always-on.svg](./combo-always-on.svg) | style reference | Animated priority-combo fallback (4 layers, 16s loop). Edit the SVG directly — there is no `.mmd` source. |
| [cli-terminal.svg](./cli-terminal.svg) | README.md (root) | Compact half-height animated terminal (1200×350): 3 real CLI commands cycling with typewriter + scrolling subcommand ticker; first frame = completed providers screen. Edit the SVG directly — there is no `.mmd` source. |
| [compression-pipeline.svg](./compression-pipeline.svg) | README.md (root) | Animated 10-engine compression funnel (8s loop). Edit the SVG directly — there is no `.mmd` source. |
-| [free-tier-budget.svg](./free-tier-budget.svg) | README.md (root) | Animated free-tier budget card (~1.6B/mo headline, 21-pool budget bar, per-model grid, signup credits, 10s loop). Edit the SVG directly — there is no `.mmd` source. |
+| [free-tier-budget.svg](./free-tier-budget.svg) | README.md (root) | Animated free-tier budget card (~1.4B/mo headline, 19-pool budget bar, per-model grid, signup credits, 10s loop). Edit the SVG directly — there is no `.mmd` source. |
| [readme-hero.svg](./readme-hero.svg) | README.md (root) | Animated hero card (tagline, 268-provider/90+ free headline, full-width compression bar demo, 6 stat chips). Edit the SVG directly — there is no `.mmd` source. |
| [promise-pillars.svg](./promise-pillars.svg) | README.md (root) | Animated "The Promise" 6-pillar card (12s border-highlight sweep). Edit the SVG directly — there is no `.mmd` source. |
| [why-pain-fix.svg](./why-pain-fix.svg) | README.md (root) | Animated "Why OmniRoute" 10-row pain-vs-fix ledger (15s green row sweep). Edit the SVG directly — there is no `.mmd` source. |
diff --git a/docs/diagrams/compression-pipeline.svg b/docs/diagrams/compression-pipeline.svg
index e03d5293a5..72a12994e4 100644
--- a/docs/diagrams/compression-pipeline.svg
+++ b/docs/diagrams/compression-pipeline.svg
@@ -1,4 +1,4 @@
-
+
@@ -30,7 +30,7 @@
- OMNIROUTE COMPRESSION — 10 ENGINES
+ OMNIROUTE COMPRESSION — 11 ENGINES
@@ -53,10 +53,10 @@
-
+
-
+
@@ -77,105 +77,114 @@
-
+
-
-
-
+
+
+
- 1
- Session
- Dedup
+ 1
+ Session
+ Dedup
-
-
-
+
+
+
- 2
- CCR
+ 2
+ CCR
-
-
-
+
+
+
- 3
- RTK
+ 3
+ RTK
-
-
-
+
+
+
- 4
- Headroom
+ 4
+ Headroom
-
-
-
+
+
+
- 5
- Relevance
+ 5
+ Relevance
-
-
-
+
+
+
- 6
- Caveman
+ 6
+ Caveman
-
-
-
+
+
+
- 7
- LLM
- Lingua-2
+ 7
+ LLM
+ Lingua-2
-
-
-
-
+
+
+
+
- 8
- Lite
+ 8
+ Omniglyph
-
-
-
-
+
+
+
+
- 9
- Aggressive
+ 9
+ Lite
-
-
-
-
+
+
+
+
- 10
- Ultra
+ 10
+ Aggressive
+
+
+
+
+
+
+
+ 11
+ Ultra
-
- default stack: RTK → Caveman
+
+ default stack: RTK → Caveman
{ } code · URLs · JSON — always preserved byte-perfect
diff --git a/docs/diagrams/exported/mcp-tools-104.svg b/docs/diagrams/exported/mcp-tools-104.svg
new file mode 100644
index 0000000000..3f750ae1fd
--- /dev/null
+++ b/docs/diagrams/exported/mcp-tools-104.svg
@@ -0,0 +1 @@
+
MCP Server 104 tools total
Core (42) routing, cache, compression, quota, proxy, tunnels, search, web_fetch
Memory (3) memory_search, memory_save, memory_delete
Skills (4) skill_invoke, skill_list, skill_diagnose, skill_uninstall + agent-skill cli-registry discovery
GitHub-Skills (3) marketplace search, install, update
Pool (6) pool_status, pool_sessions, pool_reset, pool_warm, pool_health, browser_pool_status
Notion (6) search, get_page, query_database, get_database, list_block_children, append_blocks
Obsidian (22) vault read/write, search, frontmatter, daily notes, …
Compression Studios (+2) beyond the 11 in Core
\ No newline at end of file
diff --git a/docs/diagrams/exported/mcp-tools-94.svg b/docs/diagrams/exported/mcp-tools-94.svg
deleted file mode 100644
index e4edb1d769..0000000000
--- a/docs/diagrams/exported/mcp-tools-94.svg
+++ /dev/null
@@ -1 +0,0 @@
-
Core (34) routing, cache, compression, quota, proxy, tunnels, search, web_fetch
Memory (3) memory_search, memory_save, memory_delete
Skills (4) skill_invoke, skill_list, skill_diagnose, skill_uninstall
Agent-Skills (3) cli-registry discovery
Pool (6) pool_status, pool_sessions, pool_reset, pool_warm, pool_health, browser_pool_status
Notion (6) search, get_page, query_database, get_database, list_block_children, append_blocks
Obsidian (22) vault read/write, search, frontmatter, daily notes, …
\ No newline at end of file
diff --git a/docs/diagrams/exported/request-pipeline.svg b/docs/diagrams/exported/request-pipeline.svg
index f64c7903fe..9de7fb82a0 100644
--- a/docs/diagrams/exported/request-pipeline.svg
+++ b/docs/diagrams/exported/request-pipeline.svg
@@ -1 +1 @@
-
Next.js Route /v1/chat/completions
Zod validation (request body)
AuthZ pipeline (extractApiKey + isValidApiKey)
API key policy (allowlist + scopes)
Prompt-injection guardrail
Rate limit (per-key, per-IP)
resolveComboTargets (14 strategies)
handleSingleModel (per target)
translateRequest (OpenAI↔Claude↔Gemini)
getExecutor (31 executors)
Upstream Provider (177 providers)
responsesTransformer (Responses↔Chat)
\ No newline at end of file
+
Next.js Route /v1/chat/completions
Zod validation (request body)
AuthZ pipeline (extractApiKey + isValidApiKey)
API key policy (allowlist + scopes)
Prompt-injection guardrail
Rate limit (per-key, per-IP)
resolveComboTargets (18 strategies)
handleSingleModel (per target)
translateRequest (OpenAI↔Claude↔Gemini)
getExecutor (31 executors)
Upstream Provider (177 providers)
responsesTransformer (Responses↔Chat)
\ No newline at end of file
diff --git a/docs/diagrams/free-tier-budget.svg b/docs/diagrams/free-tier-budget.svg
index 570a9abc75..fece1c76f6 100644
--- a/docs/diagrams/free-tier-budget.svg
+++ b/docs/diagrams/free-tier-budget.svg
@@ -1,4 +1,4 @@
-
+
@@ -27,9 +27,9 @@
-
-
-
+
+
+
FREE-TIER BUDGET · LIVE ON /dashboard/free-tiers
@@ -79,32 +79,30 @@
counted once ✓
15 providers ToS-flagged — we flag it · you decide
-
- WHERE IT COMES FROM · 21 COUNTABLE FREE POOLS
+
+ WHERE IT COMES FROM · 19 COUNTABLE FREE POOLS
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -113,86 +111,88 @@
each segment = one free pool · widths floored so every provider shows · honest numbers below
-
+
Mistral Large 3 1.00B
GPT-4o mini 150M
- LongCat-2.0-Preview 150M
- Gemini 2.5 Flash 60M
- GLM 4.7 30M
- Llama 3.3 70B 30M
- Grok-3 24M
- DeepSeek V4 Pro 20M
- GPT-4.1 18M
- Llama 4 Scout 15M
- Inclusion Model 15M
- GPT-4o 7M
- MiniMax-M2.7 6M
- Arcee Trinity Large Prev 5M
- Auto Free 4M
- Auto 1M
- Command A Reasoning 800K
- Llama 3.3 70B 500K
- morph-v3-large 400K
- Llama 3.1 8B 200K
- Auto 25K
+ Gemini 2.5 Flash 60M
+ GLM 4.7 30M
+ Llama 3.3 70B 30M
+ Grok-3 24M
+ DeepSeek V4 Pro 20M
+ GPT-4.1 18M
+ Llama 4 Scout 15M
+ GPT-4o 7M
+ MiniMax-M2.7 6M
+ Arcee Trinity Large Prev 5M
+ Auto Free 4M
+ Auto 1M
+ Command A Reasoning 800K
+ ERNIE 4.5 VL 424B 500K
+ morph-v3-large 400K
+ Llama 3.1 8B 200K
+ Claude Sonnet 4.5 25K
- + FIRST MONTH · ONE-TIME SIGNUP CREDITS ~616M
+ + FIRST MONTH · ONE-TIME SIGNUP CREDITS ~626M
-
- vertex 300M
-
- agentrouter 200M
-
- predibase 25M
-
- together 25M
-
- glm-cn 20M
-
- doubao 15M
-
- ai21 10M
-
- deepseek 5M
-
- hyperbolic 5M
+
+ vertex 300M
+
+ agentrouter 200M
+
+ predibase 25M
+
+ together 25M
+
+ glm-cn 20M
+
+ doubao 15M
+
+ ai21 10M
+
+ deepseek 5M
+
+ hyperbolic 5M
+
+ longcat 10M
+
+ …
- ∞
- PLUS THE UN-COUNTABLE — PERMANENTLY FREE · NO TOKEN CAP
+ ∞
+ PLUS THE UN-COUNTABLE — PERMANENTLY FREE · NO TOKEN CAP
-
- SiliconFlow
-
- Z.AI GLM-Flash
-
- Kilo
-
- OpenCode Zen
-
- baidu
-
- …
+
+ SiliconFlow
+
+ Z.AI GLM-Flash
+
+ Kilo
+
+ OpenCode Zen
+
+ baidu
+
+ …
- $10 OpenRouter top-up → +24M/mo
- surfaced separately — never inflates the headline
+ $10 OpenRouter top-up → +24M/mo
+ surfaced separately — never inflates the headline
-
- CURRENT MONTH
-
-
+
+ CURRENT MONTH
+
+
-
+
-
+
- LIVE
- used / remaining · per-model breakdown · transparent terms flag per provider
+ LIVE
+ used / remaining · per-model breakdown · transparent terms flag per provider
diff --git a/docs/diagrams/mcp-tools-94.mmd b/docs/diagrams/mcp-tools-104.mmd
similarity index 52%
rename from docs/diagrams/mcp-tools-94.mmd
rename to docs/diagrams/mcp-tools-104.mmd
index bb629d4050..27967da552 100644
--- a/docs/diagrams/mcp-tools-94.mmd
+++ b/docs/diagrams/mcp-tools-104.mmd
@@ -1,16 +1,20 @@
%% MCP Server tool inventory by category
-%% Reflects: open-sse/mcp-server/ — TOTAL_MCP_TOOL_COUNT in server.ts
-%% (MCP_TOOLS 34 + memory 3 + skill 4 + agentSkill 3 + pool 6 + gamification 8 + plugin 8 + notion 6 + obsidian 22 = 94)
+%% Reflects: open-sse/mcp-server/ — TOTAL_MCP_TOOL_COUNT in server.ts (countUniqueMcpTools)
+%% Unique contributions: MCP_TOOLS 42 + memory 3 + skill 4 + githubSkill 3 + pool 6
+%% + gamification 8 + plugin 8 + notion 6 + obsidian 22 + compression 2 = 104
+%% (agentSkillTools fully overlaps the skill registry; 11 of the 13 compression
+%% tools live in the base registry — duplicates counted once)
%% Regenerate the SVG with: npm run docs:render-diagrams
-%% v3.8.40
+%% v3.8.49
flowchart LR
- MCP["MCP Server 94 tools total"]
- MCP --> Core["Core (34) routing, cache, compression, quota, proxy, tunnels, search, web_fetch"]
+ MCP["MCP Server 104 tools total"]
+ MCP --> Core["Core (42) routing, cache, compression, quota, proxy, tunnels, search, web_fetch"]
MCP --> Mem["Memory (3) memory_search, memory_save, memory_delete"]
- MCP --> Skl["Skills (4) skill_invoke, skill_list, skill_diagnose, skill_uninstall"]
- MCP --> ASkl["Agent-Skills (3) cli-registry discovery"]
+ MCP --> Skl["Skills (4) skill_invoke, skill_list, skill_diagnose, skill_uninstall + agent-skill cli-registry discovery"]
+ MCP --> GSkl["GitHub-Skills (3) marketplace search, install, update"]
MCP --> Pool["Pool (6) pool_status, pool_sessions, pool_reset, pool_warm, pool_health, browser_pool_status"]
MCP --> Gam["Gamification (8)"]
MCP --> Plg["Plugins (8)"]
MCP --> Notion["Notion (6) search, get_page, query_database, get_database, list_block_children, append_blocks"]
MCP --> Obs["Obsidian (22) vault read/write, search, frontmatter, daily notes, …"]
+ MCP --> Cmp["Compression Studios (+2) beyond the 11 in Core"]
diff --git a/docs/diagrams/promise-pillars.svg b/docs/diagrams/promise-pillars.svg
index 23d0e59143..e687bfb652 100644
--- a/docs/diagrams/promise-pillars.svg
+++ b/docs/diagrams/promise-pillars.svg
@@ -1,4 +1,4 @@
-
+
Animated promise card: six pillar tiles fade in in reading order, then a soft colored border highlight sweeps from tile to tile in a continuous cycle.
@@ -91,7 +91,7 @@
Every tool works
- 26 coding agents — Claude Code, Codex,
+ 33 coding agents — Claude Code, Codex,
Cursor, Cline, Copilot, Antigravity —
through one config.
diff --git a/docs/diagrams/request-pipeline.mmd b/docs/diagrams/request-pipeline.mmd
index 88261da1b5..2933c645c3 100644
--- a/docs/diagrams/request-pipeline.mmd
+++ b/docs/diagrams/request-pipeline.mmd
@@ -13,7 +13,7 @@ flowchart LR
Cache -->|yes| Return["Return cached"]
Cache -->|no| Rate["Rate limit (per-key, per-IP)"]
Rate --> Combo{"Combo target?"}
- Combo -->|combo| Resolve["resolveComboTargets (17 strategies)"]
+ Combo -->|combo| Resolve["resolveComboTargets (18 strategies)"]
Resolve --> Single["handleSingleModel (per target)"]
Combo -->|single| Single
Single --> Translate["translateRequest (OpenAI↔Claude↔Gemini)"]
diff --git a/docs/i18n/zh-CN/docs/frameworks/MCP-SERVER.md b/docs/i18n/zh-CN/docs/frameworks/MCP-SERVER.md
index 9508bf8304..ef7f577df8 100644
--- a/docs/i18n/zh-CN/docs/frameworks/MCP-SERVER.md
+++ b/docs/i18n/zh-CN/docs/frameworks/MCP-SERVER.md
@@ -10,9 +10,9 @@ lastUpdated: 2026-06-28
>
> 数据来源:`open-sse/mcp-server/schemas/tools.ts`(34 个基础工具)+ `memoryTools.ts`(3)+ `skillTools.ts`(4)+ `agentSkillTools.ts`(3)+ `poolTools.ts`(6)+ `gamificationTools.ts`(8)+ `pluginTools.ts`(8)+ `notionTools.ts`(6)+ `obsidianTools.ts`(22)= **94**(`TOTAL_MCP_TOOL_COUNT`)。工具注册和权限域绑定逻辑见 `open-sse/mcp-server/server.ts`。
-
+
-> 来源:[diagrams/mcp-tools-94.mmd](../diagrams/mcp-tools-94.mmd)(通过 `npm run docs:render-diagrams` 重新生成)。
+> 来源:[diagrams/mcp-tools-104.mmd](../diagrams/mcp-tools-104.mmd)(通过 `npm run docs:render-diagrams` 重新生成)。
## 安装
diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md
index 57f471a554..5967da22b0 100644
--- a/docs/reference/ENVIRONMENT.md
+++ b/docs/reference/ENVIRONMENT.md
@@ -1029,6 +1029,8 @@ Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy),
| `MITM_DISABLE_TLS_VERIFY` | `0` | `src/mitm/server.cjs` | Set `1` to disable upstream TLS verification (development only). |
| `MITM_IDLE_TIMEOUT_MS` | `60000` | `src/mitm/socketTimeouts.ts`, `src/mitm/server.cjs` | Idle socket timeout (ms) for proxied connections; idle sockets past this are torn down to avoid leaking half-open tunnels. |
| `MITM_VERBOSE` | `1` | `src/mitm/server.cjs`, `src/mitm/_internal/bypass.cjs` | Routing-decision log verbosity: `0` silences, higher values log more bypass/route decisions. |
+| `MITM_ROOT_CA_ENABLED` | `false` | `src/mitm/manager.ts` | Set `true` to opt in to the root-CA + per-host-leaf cert model (#6684). Fresh installs get it automatically; installs with a pre-existing trusted legacy leaf keep the legacy fixed-SAN cert unless opted in. |
+| `MITM_CERT_MODE` | `legacy` | `src/mitm/manager.ts`, `src/mitm/server.cjs` | Set BY the MITM manager for the spawned proxy process (`root-ca` \| `legacy`) — reflects the cert-migration decision; not meant to be set manually. |
| `OMNIROUTE_NO_SUDO` | `0` | `src/mitm/systemCommands.ts` | Set `1` (truthy) to strip the leading `sudo` from MITM cert-trust commands — for root-less / user-namespaced deployments where the operator trusts the CA manually (e.g. via Node's extra-CA-certs mechanism). |
| `SKIP_ANTIGRAVITY_DNS` | _(unset)_ | `src/mitm/dns/provision.ts` | Set `true` to skip provisioning `/etc/hosts` DNS entries for the Antigravity proxy hostnames entirely — for containers with no sudo/root available. |
| `OMNIROUTE_SKIP_DNS_WRITE` | _(unset)_ | `src/mitm/dns/dnsConfig.ts` | Set `1` to skip writing to the hosts file when adding/removing DNS entries — for sandboxed or read-only test environments. |
@@ -1039,6 +1041,8 @@ Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy),
| `ONEPROXY_API_URL` | `https://1proxy-api.aitradepulse.com` | `src/lib/oneproxySync.ts` | 1Proxy service API URL override. |
| `ONEPROXY_MAX_PROXIES` | `500` | `src/lib/oneproxySync.ts` | Maximum proxies imported per sync. |
| `ONEPROXY_MIN_QUALITY_THRESHOLD` | `50` | `src/lib/oneproxySync.ts` | Minimum quality score for imported proxies. |
+| `FREE_PROXY_AUTO_SYNC_ENABLED` | `false` | `src/lib/freeProxyProviders/scheduler.ts` | Set `true` to enable the background free-proxy pool auto-sync scheduler. Opt-in, off by default. |
+| `FREE_PROXY_AUTO_SYNC_INTERVAL_MS` | `1800000` | `src/lib/freeProxyProviders/scheduler.ts` | Auto-sync interval in milliseconds (default 30 min). |
| `FREE_PROXY_1PROXY_ENABLED` | `true` | `src/lib/freeProxyProviders/oneproxy.ts` | Enable the 1proxy free proxy source. Set to `false` to disable. |
| `FREE_PROXY_1PROXY_API_URL` | _(see oneproxy.ts)_ | `src/lib/freeProxyProviders/oneproxy.ts` | 1proxy API URL override. |
| `FREE_PROXY_1PROXY_MAX` | `500` | `src/lib/freeProxyProviders/oneproxy.ts` | Maximum proxies fetched per sync from 1proxy. |
diff --git a/docs/reference/FREE_TIERS.md b/docs/reference/FREE_TIERS.md
index 936af9e78c..bac9c0f725 100644
--- a/docs/reference/FREE_TIERS.md
+++ b/docs/reference/FREE_TIERS.md
@@ -15,15 +15,17 @@ lastUpdated: 2026-06-28
| Metric | Tokens / month | Meaning |
| ------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| **Documented recurring grant (steady)** | **~1.54B** | Free-tier **pools** (per-model catalog), each shared pool counted **once**. The live source behind `/api/free-tier/summary` and the dashboard's Free-Tier Budget page. **Use this number.** |
-| **+ first month with signup credits** | **~2.15B** | Steady + one-time signup credits (Together $25, Z.AI 20M, DeepSeek 5M, …), deduped per account. **First month only** — does not recur. |
+| **Documented recurring grant (steady)** | **~1.37B** | Free-tier **pools** (per-model catalog), each shared pool counted **once**. The live source behind `/api/free-tier/summary` and the dashboard's Free-Tier Budget page. **Use this number.** |
+| **+ first month with signup credits** | **~2.00B** | Steady + one-time signup credits (Together $25, Z.AI 20M, DeepSeek 5M, …), deduped per account. **First month only** — does not recur. |
| **+ permanently free, no published cap** | _un-quantifiable_ | `siliconflow`, `glm-cn` (GLM-4-Flash), `tencent`, `baidu`, `kilo-gateway`, `opencode-zen` — real recurring access, rate/concurrency-limited, **no token cap to count**. Listed, never summed (counting them at `RPM×24/7` is the inflation we reject). |
| **+ deposit-unlock boost** | **+~24M** | A one-time **$10** OpenRouter top-up raises its free pool from 50 → 1000 req/day. Reported separately so it never inflates the steady number. |
| Theoretical ceiling (all rate limits, 24/7) | ~10B | Sum of every provider rate limit extrapolated to non-stop use. **Not a guarantee** — do not headline this. |
-**Honest headline:** _OmniRoute aggregates **~1.6B documented free tokens per month** (up to ~2.1B in your first month with signup credits) across 40+ free-tier pools — plus a long tail of permanently-free, no-cap providers — and RTK + Caveman compression (15–95% token savings) stretches that further._
+**Honest headline:** _OmniRoute aggregates **~1.37B documented free tokens per month** (up to ~2.0B in your first month with signup credits) across 39 free-tier pools — plus a long tail of permanently-free, no-cap providers — and RTK + Caveman compression (15–95% token savings) stretches that further._
> **Why this dropped from the previous ~1.94B.** The 2026-06-17 refresh is an honesty correction, not a loss: `gemini` is now pool-deduped (was inflated by counting each Flash variant separately, 462M → 60M), `cloudflare-ai` corrected to its real 10k-Neurons/day (122M → 30M), `doubao` reclassified as a one-time signup credit (not recurring), and shut-down tiers removed (`github-models` closed to new signups, `chutes`/`phind`/`kluster` discontinued). Partly offset by `llm7` (correct 5M/day → 150M) and new free providers (Kilo, OpenCode Zen, Z.AI GLM-Flash).
+>
+> **Further corrected to ~1.37B in v3.8.42:** `longcat` was reclassified from a 150M/mo recurring grant to a one-time 10M signup credit after its free preview ended. Same honesty rule — no provider was dropped by mistake.
Biggest **documented** contributors: `mistral` 1.00B, `llm7` 150M, `groq` 117M, `gemini` 60M, `cerebras` 30M, `cloudflare-ai` 30M, `sambanova` 30M. (`longcat` is excluded — its 10M LongCat-2.0 grant is a one-time, KYC-gated signup credit, not a recurring monthly budget.)
diff --git a/scripts/check/check-docs-counts-sync.mjs b/scripts/check/check-docs-counts-sync.mjs
index 242a5d9b84..024cdb1cb0 100644
--- a/scripts/check/check-docs-counts-sync.mjs
+++ b/scripts/check/check-docs-counts-sync.mjs
@@ -23,6 +23,8 @@
// not catch it — regenerate with `npm run gen:provider-reference` before relying on it.
import fs from "node:fs";
+import { spawnSync } from "node:child_process";
+import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
@@ -102,6 +104,16 @@ export function tallyDrift(checks, getContent) {
}
for (const f of c.files) {
const content = getContent(f);
+ if (c.validate) {
+ if (content == null) continue;
+ const v = c.validate(content);
+ lines.push(` ${v.ok ? "✓" : c.strict ? "✗" : "⚠"} ${f} — ${v.detail}`);
+ if (!v.ok) {
+ if (c.strict) strict++;
+ else soft++;
+ }
+ continue;
+ }
const found = content != null && content.includes(String(c.actual));
if (found) {
lines.push(` ✓ ${f} mentions "${c.actual}"`);
@@ -115,6 +127,131 @@ export function tallyDrift(checks, getContent) {
return { strict, soft, lines };
}
+// Reads every code-derived fact in ONE tsx subprocess — the same functions the app
+// serves at runtime, never a hardcoded copy. DATA_DIR is redirected to a throwaway dir
+// so importing the MCP tool modules cannot touch the operator's real SQLite file.
+// Returns null when tsx is unavailable so the gate degrades to a skip, not a false red.
+function readCodeFacts() {
+ const script = [
+ 'import {computeFreeModelTotals} from "./open-sse/config/freeModelCatalog.ts";',
+ 'import {ENGINE_IDS} from "./open-sse/services/compression/engineCatalog.ts";',
+ 'import {CLI_TOOLS} from "./src/shared/constants/cliTools.ts";',
+ 'import {countUniqueMcpTools} from "./open-sse/mcp-server/toolCount.ts";',
+ 'import {MCP_TOOLS} from "./open-sse/mcp-server/schemas/tools.ts";',
+ 'import {memoryTools} from "./open-sse/mcp-server/tools/memoryTools.ts";',
+ 'import {skillTools} from "./open-sse/mcp-server/tools/skillTools.ts";',
+ 'import {agentSkillTools} from "./open-sse/mcp-server/tools/agentSkillTools.ts";',
+ 'import {githubSkillTools} from "./open-sse/mcp-server/tools/githubSkillTools.ts";',
+ 'import {poolTools} from "./open-sse/mcp-server/tools/poolTools.ts";',
+ 'import {gamificationTools} from "./open-sse/mcp-server/tools/gamificationTools.ts";',
+ 'import {pluginTools} from "./open-sse/mcp-server/tools/pluginTools.ts";',
+ 'import {notionTools} from "./open-sse/mcp-server/tools/notionTools.ts";',
+ 'import {obsidianTools} from "./open-sse/mcp-server/tools/obsidianTools.ts";',
+ 'import {compressionTools} from "./open-sse/mcp-server/tools/compressionTools.ts";',
+ "const cols={MCP_TOOLS,memoryTools,skillTools,agentSkillTools,githubSkillTools,poolTools,",
+ "gamificationTools,pluginTools,notionTools,obsidianTools,compressionTools};",
+ "const sc=new Set();",
+ "for(const col of Object.values(cols))for(const t of Object.values(col))",
+ "for(const x of (t?.scopes||[]))sc.add(x);",
+ "const t=computeFreeModelTotals();const cli=Object.values(CLI_TOOLS);",
+ "const by=(c)=>cli.filter(x=>x.category===c).length;",
+ 'console.log("@@"+JSON.stringify({freeSteady:t.steadyRecurringTokens,',
+ "freeFirst:t.firstMonthRealisticTokens,freePools:t.poolCount,engines:ENGINE_IDS.length,",
+ "cliTotal:cli.length,cliCode:by('code'),cliAgent:by('agent'),",
+ "mcpTools:countUniqueMcpTools(cols),mcpScopes:sc.size}));",
+ ].join("");
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "docs-counts-"));
+ try {
+ const r = spawnSync(process.execPath, ["--import", "tsx/esm", "-e", script], {
+ cwd: ROOT,
+ encoding: "utf8",
+ timeout: 180000,
+ env: { ...process.env, DATA_DIR: tmp, APP_LOG_LEVEL: "silent" },
+ });
+ if (r.status !== 0 || !r.stdout) return null;
+ const line = r.stdout.split("\n").find((l) => l.startsWith("@@"));
+ return line ? JSON.parse(line.slice(2)) : null;
+ } catch {
+ return null;
+ } finally {
+ fs.rmSync(tmp, { recursive: true, force: true });
+ }
+}
+
+// The docs publish a rounded aggregate headline ("~1.4B"). Accept a claim that rounds to
+// the live value at one decimal place; reject a stale one. Tolerance is tight on purpose:
+// this gate exists so the headline cannot drift upward unnoticed.
+//
+// Only the AGGREGATE headline is validated, via an explicit whitelist. These files also
+// carry figures that are legitimately not the headline and must never trip the gate:
+// the theoretical ceiling ("would read ~10B; not published"), the historical "previous
+// ~1.94B", and per-model rows ("mistral … ~1.00B"). A whitelist keeps those safe without
+// having to enumerate every contrastive phrasing.
+const HEADLINE_AFTER = /^\s*(?:documented\s+)?free tokens|^\s*in (?:your|the) first month/i;
+const HEADLINE_BEFORE = /(recurring grant[^|]*\|\s*\**|signup credits[^|]*\|\s*\**|up to\s*)$/i;
+
+export function extractHeadlineClaims(content) {
+ const claims = [];
+ for (const m of content.matchAll(/~(\d+(?:\.\d+)?)B/g)) {
+ const after = content.slice(m.index + m[0].length, m.index + m[0].length + 80);
+ const before = content.slice(Math.max(0, m.index - 80), m.index);
+ if (!HEADLINE_AFTER.test(after) && !HEADLINE_BEFORE.test(before)) continue;
+ claims.push({ value: Number(m[1]), text: m[0] });
+ }
+ return claims;
+}
+
+export function checkFreeTierHeadline(content, totals) {
+ const claims = extractHeadlineClaims(content);
+ if (!claims.length) return { ok: true, detail: "no aggregate free-tier headline in this file" };
+ const steady = totals.s / 1e9;
+ const first = totals.m / 1e9;
+ const stale = claims.filter(
+ (c) => Math.abs(c.value - steady) >= 0.05 && Math.abs(c.value - first) >= 0.05
+ );
+ if (!stale.length)
+ return { ok: true, detail: `${claims.length} headline claim(s) match the live catalog` };
+ return {
+ ok: false,
+ detail:
+ `stale headline ${[...new Set(stale.map((c) => c.text))].join(", ")} — live catalog ` +
+ `computes ~${steady.toFixed(2)}B steady / ~${first.toFixed(2)}B first month`,
+ };
+}
+
+// --- Generic numeric-claim gate ---------------------------------------------
+// Same principle as the free-tier headline: docs legitimately carry numbers that are
+// NOT the aggregate being gated (per-module tool counts like "Memory tool definitions
+// (3 tools)", the CLI catalog's "33 tools (25 CLI Code's ...)" next to the MCP total).
+// So every check declares what to skip rather than assuming any "N tools" is the claim.
+export function extractNumberClaims(content, { pattern, skipBefore, skipAfter }) {
+ const claims = [];
+ for (const m of content.matchAll(pattern)) {
+ const before = content.slice(Math.max(0, m.index - 40), m.index);
+ const after = content.slice(m.index + m[0].length, m.index + m[0].length + 40);
+ if (skipBefore && skipBefore.test(before)) continue;
+ if (skipAfter && skipAfter.test(after)) continue;
+ claims.push({ value: Number(m[1]), text: m[0].trim() });
+ }
+ return claims;
+}
+
+export function makeNumberClaimValidator(expected, opts) {
+ return (content) => {
+ const claims = extractNumberClaims(content, opts);
+ if (!claims.length) return { ok: true, detail: `no ${opts.what} claim in this file` };
+ const stale = claims.filter((c) => c.value !== expected);
+ if (!stale.length)
+ return { ok: true, detail: `${claims.length} ${opts.what} claim(s) match the code` };
+ return {
+ ok: false,
+ detail:
+ `stale ${opts.what}: ${[...new Set(stale.map((c) => `"${c.text}"`))].join(", ")} — ` +
+ `code has ${expected}`,
+ };
+ };
+}
+
export function buildChecks() {
return [
{
@@ -131,6 +268,70 @@ export function buildChecks() {
strict: true,
files: ["docs/README.md", "docs/guides/I18N.md", "AGENTS.md"],
},
+ ...(() => {
+ const f = readCodeFacts();
+ if (!f)
+ return [
+ {
+ label: "Code-derived counts",
+ actual: 0,
+ docKey: "code facts",
+ strict: false,
+ files: [],
+ },
+ ];
+ const claim = (expected, what, opts, files) => ({
+ label: `${what} (live code)`,
+ actual: expected,
+ docKey: what,
+ strict: true,
+ files,
+ validate: makeNumberClaimValidator(expected, { what, ...opts }),
+ });
+ return [
+ {
+ label: "Free-tier headline (live catalog)",
+ actual: `~${(f.freeSteady / 1e9).toFixed(2)}B steady / ${f.freePools} pools`,
+ docKey: "free-tier headline",
+ strict: true,
+ files: ["README.md", "docs/reference/FREE_TIERS.md"],
+ validate: (content) =>
+ checkFreeTierHeadline(content, { s: f.freeSteady, m: f.freeFirst }),
+ },
+ claim(
+ f.engines,
+ "compression engines",
+ { pattern: /(\d+)[-\s](?:engine stack|composable engines|stacked engines)/gi },
+ ["README.md"]
+ ),
+ claim(
+ f.mcpTools,
+ "MCP tools",
+ {
+ pattern: /(\d+) tools/gi,
+ // per-module rows ("Memory tool definitions (3 tools)") and the CLI catalog
+ // total ("33 tools (25 CLI Code's …)") are not the MCP aggregate
+ // per-module rows read "… tool definitions (N tools" / "… management tools
+ // (N tools" — the word tool(s)/definitions sits right before the paren. The
+ // aggregate ("MCP Server (104 tools", "all 104 tools") never does.
+ skipBefore: /(tools?|definitions?)\s*\(\s*$/i,
+ skipAfter: /^\s*\(\d+ CLI/,
+ },
+ ["README.md", "CLAUDE.md", "AGENTS.md", "docs/frameworks/MCP-SERVER.md"]
+ ),
+ claim(f.mcpScopes, "MCP scopes", { pattern: /(\d+) scopes/gi }, [
+ "README.md",
+ "CLAUDE.md",
+ "AGENTS.md",
+ ]),
+ claim(
+ f.cliTotal,
+ "CLI tools",
+ { pattern: /(\d+) tools(?=\s*\(\d+ CLI)/gi },
+ ["README.md"]
+ ),
+ ];
+ })(),
{
label: "Executors count",
actual: countFiles("open-sse/executors"),
diff --git a/src/shared/components/OAuthModal.tsx b/src/shared/components/OAuthModal.tsx
index 7d079724b2..9daefd84e9 100644
--- a/src/shared/components/OAuthModal.tsx
+++ b/src/shared/components/OAuthModal.tsx
@@ -705,7 +705,7 @@ export default function OAuthModal({
// (`{user, accessToken, expires}`), not just the bare token (#6636).
if (provider === "codex" && looksLikeCodexSessionJson(callbackUrl)) {
const result = parseCodexSessionJson(JSON.parse(callbackUrl.trim()));
- if (!result.ok) {
+ if (result.ok === false) {
setError(result.error);
return;
}
diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json
index 6d401fc86d..0656be2a47 100644
--- a/tests/snapshots/provider/translate-path.json
+++ b/tests/snapshots/provider/translate-path.json
@@ -3212,8 +3212,8 @@
}
},
"url": {
- "nonStream": "https://www.notion.so/api/v3/runInferenceTranscript",
- "stream": "https://www.notion.so/api/v3/runInferenceTranscript"
+ "nonStream": "https://app.notion.com/api/v3/runInferenceTranscript",
+ "stream": "https://app.notion.com/api/v3/runInferenceTranscript"
}
},
"nous-research": {
diff --git a/tests/unit/check-docs-counts-sync.test.ts b/tests/unit/check-docs-counts-sync.test.ts
index 3c7256f6aa..040876f438 100644
--- a/tests/unit/check-docs-counts-sync.test.ts
+++ b/tests/unit/check-docs-counts-sync.test.ts
@@ -98,3 +98,101 @@ test("the gate exits 0 against the current (synced) repo state", () => {
// Throws if exit code is non-zero; current docs are synced so this must pass.
assert.doesNotThrow(() => execFileSync("node", [GATE], { encoding: "utf8", stdio: "pipe" }));
});
+
+// --- Free-tier headline gate ------------------------------------------------
+// Regression guard for the drift found in the v3.8.49 README audit: the README
+// headlined ~1.6B for seven releases after the catalog had already been corrected
+// down to 1.37B, because no gate watched that number.
+import {
+ checkFreeTierHeadline,
+ extractHeadlineClaims,
+} from "../../scripts/check/check-docs-counts-sync.mjs";
+
+const checkHeadline = checkFreeTierHeadline as (
+ content: string,
+ totals: { s: number; m: number; p: number }
+) => { ok: boolean; detail: string };
+const extractClaims = extractHeadlineClaims as (
+ content: string
+) => { value: number; text: string }[];
+
+const TOTALS = { s: 1_371_725_000, m: 1_998_225_000, p: 39 };
+
+test("free-tier gate accepts a headline that rounds to the live catalog", () => {
+ assert.equal(checkHeadline("~1.4B free tokens per month", TOTALS).ok, true);
+ assert.equal(checkHeadline("up to ~2.0B in the first month", TOTALS).ok, true);
+});
+
+test("free-tier gate rejects the stale headlines this audit found", () => {
+ for (const stale of ["~1.6B free tokens/mo", "~1.54B free tokens per month"]) {
+ const r = checkHeadline(stale, TOTALS);
+ assert.equal(r.ok, false, `expected ${stale} to be rejected`);
+ assert.match(r.detail, /live catalog computes/);
+ }
+ assert.equal(checkHeadline("up to ~2.1B in the first month", TOTALS).ok, false);
+});
+
+test("free-tier gate ignores non-headline figures", () => {
+ // The theoretical ceiling, the historical value and per-model rows are legitimate
+ // and must never trip the gate — that is why the extractor is a whitelist.
+ const noise =
+ "counting every rate limit 24/7 would read ~10B; not published. " +
+ "Why this dropped from the previous ~1.94B. | `mistral` | recurring | ~1.00B |";
+ assert.deepEqual(extractClaims(noise), []);
+ assert.equal(checkHeadline(noise, TOTALS).ok, true);
+});
+
+test("free-tier gate passes when a file carries no headline at all", () => {
+ assert.equal(checkHeadline("no figures here", TOTALS).ok, true);
+});
+
+// --- Generic numeric-claim gate (engines / MCP tools / scopes / CLI) --------
+// Extends the same drift guard to the counts that silently drifted in v3.8.49:
+// 10→11 engines, 94→104 MCP tools, 30→31 scopes, 26→33 CLI tools.
+import { makeNumberClaimValidator } from "../../scripts/check/check-docs-counts-sync.mjs";
+
+const makeValidator = makeNumberClaimValidator as (
+ expected: number,
+ opts: { what: string; pattern: RegExp; skipBefore?: RegExp; skipAfter?: RegExp }
+) => (content: string) => { ok: boolean; detail: string };
+
+test("MCP-tools gate accepts the aggregate and rejects a stale one", () => {
+ const v = makeValidator(104, {
+ what: "MCP tools",
+ pattern: /(\d+) tools/gi,
+ skipBefore: /(tools?|definitions?)\s*\(\s*$/i,
+ skipAfter: /^\s*\(\d+ CLI/,
+ });
+ assert.equal(v("MCP Server (104 tools)").ok, true);
+ assert.equal(v("with 104 tools total").ok, true);
+ assert.equal(v("MCP Server (94 tools)").ok, false);
+});
+
+test("MCP-tools gate ignores per-module counts and the CLI catalog total", () => {
+ const v = makeValidator(104, {
+ what: "MCP tools",
+ pattern: /(\d+) tools/gi,
+ skipBefore: /(tools?|definitions?)\s*\(\s*$/i,
+ skipAfter: /^\s*\(\d+ CLI/,
+ });
+ // "Memory tool definitions (3 tools)" and "33 tools (25 CLI Code's)" are not the MCP total
+ assert.equal(v("Memory tool definitions (3 tools)").ok, true);
+ assert.equal(v("management tools (8 tools)").ok, true);
+ assert.equal(v("all 33 tools (25 CLI Code's + 8 CLI Agents)").ok, true);
+});
+
+test("compression-engines and CLI-tools gates catch their v3.8.49 drift", () => {
+ const eng = makeValidator(11, {
+ what: "compression engines",
+ pattern: /(\d+)[-\s](?:engine stack|composable engines|stacked engines)/gi,
+ });
+ assert.equal(eng("11-engine stack").ok, true);
+ assert.equal(eng("10-engine stack").ok, false);
+
+ const cli = makeValidator(33, {
+ what: "CLI tools",
+ pattern: /(\d+) tools(?=\s*\(\d+ CLI)/gi,
+ });
+ assert.equal(cli("all 33 tools (25 CLI Code's)").ok, true);
+ assert.equal(cli("all 26 tools (25 CLI Code's)").ok, false);
+});
diff --git a/tests/unit/web-session-credentials.test.ts b/tests/unit/web-session-credentials.test.ts
index 250ac5ea39..55d6fe11a9 100644
--- a/tests/unit/web-session-credentials.test.ts
+++ b/tests/unit/web-session-credentials.test.ts
@@ -15,14 +15,19 @@ test("web session credential metadata covers every web-cookie provider", () => {
});
test("web session credential metadata identifies cookie, token, and no-auth providers", () => {
- // Grok needs BOTH sso and sso-rw cookies (#3180)
- assert.deepEqual(webSessionCredentials.getWebSessionCredentialRequirement("grok-web"), {
- kind: "cookie",
- credentialName: "sso + sso-rw",
- placeholder: "sso=...; sso-rw=...",
- acceptsFullCookieHeader: true,
- storageKeys: ["cookie", "sso", "sso-rw"],
- });
+ // Grok needs BOTH sso and sso-rw cookies (#3180). #7567 added the proactive
+ // cf_clearance/User-Agent hint — assert its intent, don't freeze operator copy.
+ {
+ const req = webSessionCredentials.getWebSessionCredentialRequirement("grok-web");
+ assert.ok(req && req.kind === "cookie");
+ assert.equal(req.credentialName, "sso + sso-rw");
+ assert.equal(req.placeholder, "sso=...; sso-rw=...");
+ assert.equal(req.acceptsFullCookieHeader, true);
+ assert.deepEqual(req.storageKeys, ["cookie", "sso", "sso-rw"]);
+ assert.equal(req.hintKey, "grokWebCookieHint");
+ assert.ok(typeof req.hintFallback === "string" && /cf_clearance/.test(req.hintFallback));
+ assert.ok(/User-Agent/.test(req.hintFallback));
+ }
assert.deepEqual(webSessionCredentials.getWebSessionCredentialRequirement("copilot-web"), {
kind: "token",
credentialName: "access_token",