mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-09 00:32:13 +03:00
Compare commits
15 Commits
feat/9544-
...
fix/releas
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c351cc6b9 | ||
|
|
9d353217f1 | ||
|
|
16904141f7 | ||
|
|
18717aa6b8 | ||
|
|
744ab48f1c | ||
|
|
db7c066f87 | ||
|
|
eba6fb42d4 | ||
|
|
43f78b712d | ||
|
|
54922fa1a6 | ||
|
|
5d25d34700 | ||
|
|
db2c3a4753 | ||
|
|
b80987e4d4 | ||
|
|
e8936f53f4 | ||
|
|
e4f55c7d92 | ||
|
|
5dfc96ae54 |
1
.eslintcache-probe
Normal file
1
.eslintcache-probe
Normal file
File diff suppressed because one or more lines are too long
@@ -58,7 +58,7 @@ Repository map and Reference Documentation sections below.
|
||||
| Services | `open-sse/services/` | Combo routing, rate limits, caching, etc |
|
||||
| Database | `src/lib/db/` | SQLite domain modules (130 migrations) |
|
||||
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
|
||||
| MCP Server | `open-sse/mcp-server/` | 104 tools (42 base + memory/skill/agentSkill/pool/notion/obsidian/gamification/plugin modules), 3 transports (stdio / SSE / Streamable HTTP), 31 scopes |
|
||||
| MCP Server | `open-sse/mcp-server/` | 105 tools (42 base + memory/skill/agentSkill/pool/notion/obsidian/gamification/plugin modules), 3 transports (stdio / SSE / Streamable HTTP), 31 scopes |
|
||||
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
|
||||
| Skills | `src/lib/skills/` | Extensible skill framework |
|
||||
| Memory | `src/lib/memory/` | Persistent conversational memory |
|
||||
|
||||
@@ -188,7 +188,7 @@ curl http://localhost:20128/v1/chat/completions \
|
||||
|
||||
</div>
|
||||
|
||||
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint. 291 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 291 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 15–95%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 40+ free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 104 tools, A2A, memory, guardrails, evals — 25,000+ tests)."/>
|
||||
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint. 291 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 291 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 15–95%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 40+ free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 105 tools, A2A, memory, guardrails, evals — 25,000+ tests)."/>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
@@ -439,7 +439,7 @@ All **19** strategies — mix & match per combo step:
|
||||
|
||||
</div>
|
||||
|
||||
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — comparison table vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 291 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, built-in MCP server with 104 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA, 43 i18n UI locales, 100% MIT self-hosted. OmniRoute is the only one with the full set; competitors show a mix of checks, partials and crosses. Verified from each project's docs."/>
|
||||
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — comparison table vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 291 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, built-in MCP server with 105 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA, 43 i18n UI locales, 100% MIT self-hosted. OmniRoute is the only one with the full set; competitors show a mix of checks, partials and crosses. Verified from each project's docs."/>
|
||||
|
||||
<sub>📊 Full methodology & per-feature detail vs 9router, OpenRouter, CLIProxyAPI & LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md)</sub>
|
||||
|
||||
@@ -723,7 +723,7 @@ Expose OmniRoute over **MCP**, **A2A**, a **REST API**, **webhooks** or a **remo
|
||||
<table>
|
||||
<tr><th align="left">Interface</th><th align="left">Endpoint / command</th><th align="left">Use it for</th></tr>
|
||||
<tr><td align="left" nowrap>🧰 <b>MCP (stdio)</b></td><td align="left" nowrap><code>omniroute --mcp</code></td><td align="left">Plug into Claude Desktop, Cursor, any MCP client</td></tr>
|
||||
<tr><td align="left" nowrap>🌊 <b>MCP (HTTP)</b></td><td align="left" nowrap><code>/api/mcp/stream</code></td><td align="left">Remote MCP — <b>104 tools</b>, 31 scopes, full audit trail</td></tr>
|
||||
<tr><td align="left" nowrap>🌊 <b>MCP (HTTP)</b></td><td align="left" nowrap><code>/api/mcp/stream</code></td><td align="left">Remote MCP — <b>105 tools</b>, 31 scopes, full audit trail</td></tr>
|
||||
<tr><td align="left" nowrap>📡 <b>MCP (SSE)</b></td><td align="left" nowrap><code>/api/mcp/sse</code></td><td align="left">Streaming MCP transport</td></tr>
|
||||
<tr><td align="left" nowrap>🤝 <b>A2A</b></td><td align="left" nowrap><code>/.well-known/agent.json</code></td><td align="left">Agent-to-agent, <b>JSON-RPC 2.0</b> + SSE, 6 skills</td></tr>
|
||||
<tr><td align="left" nowrap>🌐 <b>REST API</b></td><td align="left" nowrap><code>/v1/*</code></td><td align="left">OpenAI-compatible — chat, embeddings, images, audio, OCR</td></tr>
|
||||
|
||||
@@ -1,6 +1 @@
|
||||
---
|
||||
kind: feature
|
||||
ref: "#9415"
|
||||
---
|
||||
|
||||
New-API / One-API / Sub2API aggregator balance detection for compatible nodes. When a compatible provider node has the "Aggregator Gateway" toggle enabled, OmniRoute will query the aggregator's `/api/user/self` endpoint to detect the account balance. The dashboard shows the balance badge and quota-preflight routing skips exhausted accounts. The feature is gated by the `NEWAPI_AGGREGATOR_BALANCE` feature flag (default: off). A custom `quotaPerUnit` override is supported for aggregators that use a different rate than the default 500000 units/$1.
|
||||
- **sse:** New-API / One-API / Sub2API aggregator balance detection for compatible nodes — with the "Aggregator Gateway" toggle on, OmniRoute queries the aggregator's `/api/user/self` to read the account balance, shows it as a dashboard badge and lets quota-preflight routing skip exhausted accounts. Gated by the `NEWAPI_AGGREGATOR_BALANCE` feature flag (default off), with a `quotaPerUnit` override for aggregators that do not use the default 500000 units/$1 rate ([#9415](https://github.com/diegosouzapw/OmniRoute/issues/9415))
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"_comment": "Congelamento da divida ESLint da migracao TypeScript 7 (release/v3.8.50, 2026-08-05; regenerado 2026-08-06 apos prune de entradas orfas). Gerado pelo modo nativo `eslint --suppress-all --suppressions-location config/quality/eslint-suppressions.json` (NODE_OPTIONS=--max-old-space-size=12288). Politica: violacao PRE-EXISTENTE fica suprimida aqui; violacao NOVA (fora deste arquivo) e vermelho imediato e deve ser corrigida, nunca adicionada. Entradas que deixarem de ocorrer sao podadas com `eslint --prune-suppressions` (o job 'No new ESLint warnings' falha com supressoes orfas). A baseline eslintWarnings em config/quality/quality-baseline.json e 0 — o valor real medido com estas supressoes aplicadas.",
|
||||
"open-sse/executors/blackbox-web.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
@@ -2080,11 +2079,6 @@
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"tests/unit/combo-cache-invalidation.test.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"tests/unit/combo-context-length.test.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 2
|
||||
@@ -3328,11 +3322,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"tests/unit/vertex-functioncall-id-3440.test.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"tests/unit/vertex-media.test.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 3
|
||||
@@ -3363,4 +3352,4 @@
|
||||
"count": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -371,7 +371,7 @@
|
||||
"tests/unit/model-sync-route.test.ts": 1016,
|
||||
"tests/unit/models-catalog-route.test.ts": 1636,
|
||||
"tests/unit/perplexity-web.test.ts": 1355,
|
||||
"tests/unit/provider-models-route.test.ts": 1784,
|
||||
"tests/unit/provider-models-route.test.ts": 1787,
|
||||
"tests/unit/provider-validation-specialty.test.ts": 2985,
|
||||
"tests/unit/providers-page-utils.test.ts": 1106,
|
||||
"tests/unit/response-sanitizer.test.ts": 1063,
|
||||
@@ -381,7 +381,7 @@
|
||||
"tests/unit/stream-utils.test.ts": 2445,
|
||||
"tests/unit/token-refresh-service.test.ts": 1378,
|
||||
"tests/unit/translator-openai-responses-req.test.ts": 1194,
|
||||
"tests/unit/translator-openai-to-gemini.test.ts": 1616,
|
||||
"tests/unit/translator-openai-to-gemini.test.ts": 1619,
|
||||
"tests/unit/translator-openai-to-kiro.test.ts": 1275,
|
||||
"tests/unit/translator-resp-gemini-to-openai.test.ts": 1234,
|
||||
"tests/unit/usage-service-hardening.test.ts": 1483,
|
||||
@@ -607,5 +607,7 @@
|
||||
"_rebaseline_2026_08_05_9323_agentrouter_waf_retry": "PR #9323 (fix(agentrouter): retry on 400 content-blocked + burst guard) own growth: open-sse/executors/base.ts 1578->1623 (check-file-size.mjs conta via split(\"\\n\").length; wc -l ve 1622). As +45 linhas sao o WAF_RETRY_CONFIG + o burst guard via gateOutboundRequest() para o WAF do agentrouter.org, com comentarios explicando o porque de cada mitigacao e cobertos por tests/unit/base-executor-waf-retry.test.ts e tests/unit/wafRateLimit.test.ts. Crescimento funcional legitimo, nao inchaco.",
|
||||
"_rebaseline_2026_08_05_9529_own_growth": "PR #9529 own growth (base release/v3.8.50 medida EXATAMENTE nos frozen antigos, entao o modo base-relative #8522 nao cobre): open-sse/services/rateLimitManager.ts 1060->1105 (+45: helper applyLimiterSettings() que re-arma o heartbeat do reservoir apos updateSettings — fix do bug Bottleneck 2.19.5 que congelava a fila weighted; TDD em tests/unit/ratelimit-reservoir-refresh.test.ts); tests/integration/chat-pipeline.test.ts 1592->1598 (+6: User-Agent do codex derivado de getCodexClientVersion() em vez de literal pinado — teste-irmao alinhado ao contrato); tests/unit/provider-validation-specialty.test.ts 2980->2985 (+5: cobertura NOVA claude-web 429 -> valid:false, alinhamento #9406); open-sse/translator/response/openai-responses.ts 1174->1204 (+30: buildResponsesReasoningSummaryDelta MOVIDA do leaf pureHelpers.ts para o host — a funcao do #9500 muta stream state e violava o contrato do leaf puro; o LOC total do par host+leaf nao cresceu, o pureHelpers encolheu o mesmo tanto). Crescimento por fix de producao + cobertura adicional + realocacao arquitetural, nao inchaco.",
|
||||
"_rebaseline_2026_08_06_v3850_inherited_drift_reconcile": "Reconciliacao 2026-08-06 do drift ACUMULADO da release/v3.8.50 apos o lote de merges de 08-05/06: 13 arquivos acima do frozen no tip puro 8180b49ce1 (medidos pelo proprio gate). O modo PR base-relative (#8522) deixa PRs inocentes passarem, e os rebaselines individuais dos PRs se perderam nas resolucoes sucessivas de conflito deste hot-file — o drift so aparece no modo absoluto (nightly/local). Crescimentos funcionais dos PRs mergeados: #9024 topology click-nav src/app/(dashboard)/dashboard/HomePageClient.tsx; #9324 OpenRouter enrich src/app/(dashboard)/dashboard/providers/page.tsx; #9329 quota card ordering src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx; #9193 context-window suffixes src/sse/handlers/chat.ts; #9332 nested Claude server tool ids open-sse/executors/base.ts; #9228 strip orphaned tool outputs open-sse/executors/codex.ts; #9236 nvidia tool-name normalize open-sse/executors/default.ts; #9314 nested tool_call validation open-sse/executors/kiro.ts; #9260 caller identity REST hops open-sse/mcp-server/server.ts; #8934 cache breakpoints tests tests/unit/chatcore-translation-paths.test.ts; #9193 suffix tests tests/unit/combo-routing-engine.test.ts; #9196 reasoning-on-tool-finish tests tests/unit/sse-auth.test.ts; #9163 GPT-5.6 Max reasoning tests tests/unit/translator-openai-to-kiro.test.ts. default.ts e kiro.ts entram no frozen (estavam sem entrada, acima do cap 1000). Atualizacao pos-medicao (a base avancou durante o ciclo do PR): src/sse/handlers/chat.ts 1857->1877 (#9184 affinity EOF evict) e open-sse/executors/default.ts 1027->1042 (#9005 Kimi K3 tool-name backfill).",
|
||||
"_rebaseline_2026_08_06b_v3850_sweepreds_drift": "Segunda reconciliacao de 2026-08-06 (/sweep-reds sobre o tip puro 2ddbbc61a6): 3 arquivos voltaram a passar do frozen apos os merges do mesmo dia, com atribuicao 1:1 por commit. (1) src/app/(dashboard)/dashboard/providers/page.tsx 1928->1944 e (2) open-sse/executors/base.ts 1635->1640, ambos do #9515 (feat(radar): flag-gated signed free-model catalog overlay, commit e7f6b1d130) — o overlay do Radar entra por wiring nos chokepoints ja existentes (a resolucao/verificacao do catalogo assinado mora fora destes dois arquivos); +16 e +5 linhas liquidas nao sao extraiveis sem inventar um leaf por callsite. (3) open-sse/services/accountFallback.ts 1966->1972 do #8704 (commit c4527f97bd), +6 linhas de dados em CREDITS_EXHAUSTED_SIGNALS ('has been exhausted', fixes #8631). src/sse/handlers/chat.ts 1880>1877 tambem estava violando e NAO entra aqui de proposito: e drenado por encolhimento na PR #9598, sem rebaseline. Crescimento proprio DESTA PR: src/lib/db/migrationRunner.ts 1077->1084 (+7) — o guard retroativo em isSchemaAlreadyApplied para os arquivos renumerados 137/138, exigido pela propria mensagem de erro de colisao do runner (ambas as migracoes sao ALTER TABLE ADD COLUMN puro, nao idempotente). Dois `case` + dois `return hasColumn(...)` + 3 linhas de comentario dentro do switch existente; nao extraivel."
|
||||
"_rebaseline_2026_08_06b_v3850_sweepreds_drift": "Segunda reconciliacao de 2026-08-06 (/sweep-reds sobre o tip puro 2ddbbc61a6): 3 arquivos voltaram a passar do frozen apos os merges do mesmo dia, com atribuicao 1:1 por commit. (1) src/app/(dashboard)/dashboard/providers/page.tsx 1928->1944 e (2) open-sse/executors/base.ts 1635->1640, ambos do #9515 (feat(radar): flag-gated signed free-model catalog overlay, commit e7f6b1d130) — o overlay do Radar entra por wiring nos chokepoints ja existentes (a resolucao/verificacao do catalogo assinado mora fora destes dois arquivos); +16 e +5 linhas liquidas nao sao extraiveis sem inventar um leaf por callsite. (3) open-sse/services/accountFallback.ts 1966->1972 do #8704 (commit c4527f97bd), +6 linhas de dados em CREDITS_EXHAUSTED_SIGNALS ('has been exhausted', fixes #8631). src/sse/handlers/chat.ts 1880>1877 tambem estava violando e NAO entra aqui de proposito: e drenado por encolhimento na PR #9598, sem rebaseline. Crescimento proprio DESTA PR: src/lib/db/migrationRunner.ts 1077->1084 (+7) — o guard retroativo em isSchemaAlreadyApplied para os arquivos renumerados 137/138, exigido pela propria mensagem de erro de colisao do runner (ambas as migracoes sao ALTER TABLE ADD COLUMN puro, nao idempotente). Dois `case` + dois `return hasColumn(...)` + 3 linhas de comentario dentro do switch existente; nao extraivel.",
|
||||
"_rebaseline_2026_08_06c_v3850_sweepreds_pr2": "Segunda PR do /sweep-reds (fix/release-v3.8.50-basereds-0806b): tests/unit/provider-models-route.test.ts 1784->1787 (medido pelo gate, que conta split(\"\\n\").length) (+2 apos compressao de comentarios) — alinhamento de contrato forcado por dois merges do dia: #9106 tornou gemini-3.1-pro-high user-callable (a entry do alias entra na lista esperada do teste de discovery-retry, +1 linha de dado + 1 de comentario) e ff012ff420 adicionou onboardUser como bootstrap hop (exclusao no mock, ja comprimida a 1 linha). Nao ha o que encolher sem apagar o comentario que explica o porque.",
|
||||
"_rebaseline_2026_08_07_v3850_sweepreds_pr2_toolnamemap": "tests/unit/translator-openai-to-gemini.test.ts 1616->1619 (+3). O frozen estava EXATAMENTE no tamanho da base, entao qualquer linha nova viola. #9568 (c9a3361e5a) fez buildChangedToolNameMap emitir entradas IDENTIDADE (o Gemini minusculiza nomes de tool nas respostas, entao o tradutor de resposta precisa da chave para mapear de volta), o que passou a incluir `_toolNameMap` no envelope Antigravity de qualquer request com tools. As 3 linhas sao: a chave nova na lista esperada de Object.keys, 1 comentario explicando POR QUE ela aparece (sem ele o proximo leitor tenta remove-la de novo) e 1 assert do CONTEUDO do map — presenca de chave sozinha nao provaria a entrada identidade, que e justamente o comportamento novo. Nao ha o que extrair: e alinhamento de contrato dentro de um teste existente."
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ lastUpdated: 2026-06-28
|
||||
|
||||
# OmniRoute MCP Server Documentation
|
||||
|
||||
> Model Context Protocol server with 104 tools across routing, cache, compression, memory, skills, proxy, pool, and context source operations.
|
||||
> Model Context Protocol server with 105 tools across routing, cache, compression, memory, skills, proxy, pool, and context source operations.
|
||||
>
|
||||
> Source of truth: `open-sse/mcp-server/server.ts` computes **104 unique tools** with `countUniqueMcpTools()`: 42 canonical definitions (including the six CCR lifecycle tools and the agent-skills trio), plus memory (3), skills (4), GitHub skills (3), pool (6), gamification (8), plugins (8), Notion (6), Obsidian (22), and two RTK-only compression tools.
|
||||
|
||||
@@ -369,7 +369,7 @@ MCP tool, prompt, and resource registries can compress descriptions at registrat
|
||||
|
||||
Description compression shrinks each tool's metadata; **tool-cardinality reduction** goes one step further by reducing _how many_ tools are announced at all. Advertising fewer tools in the `tools/list` manifest cuts the per-request token cost the client's model pays for the tool catalog ("layer 5" compression). The implementation is a pure, stateless filter in `open-sse/mcp-server/toolCardinality.ts` (`reduceToolManifest`), wired into the registration loop in `createMcpServer()` (`open-sse/mcp-server/server.ts`).
|
||||
|
||||
**Opt-in, off by default.** The filter only runs when at least one of two environment variables is set; with neither set, all 104 tools are announced unchanged.
|
||||
**Opt-in, off by default.** The filter only runs when at least one of two environment variables is set; with neither set, all 105 tools are announced unchanged.
|
||||
|
||||
| Variable | Mode |
|
||||
| :--------------- | :-------------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -645,7 +645,7 @@ for (const s of statuses) {
|
||||
}
|
||||
|
||||
// Force re-check a specific proxy
|
||||
invalidateProxyHealth("http://user:pass@1.2.3.4:8080");
|
||||
invalidateProxyHealth("http://user:pass@203.0.113.7:8080");
|
||||
```
|
||||
|
||||
Flaga `stale` jest `true`, gdy wpis cache przekroczył `HEALTH_CACHE_TTL_MS` i następne żądanie wywoła świeży check.
|
||||
@@ -807,7 +807,7 @@ Gdy proxy systematycznie pada, oznacz je ręcznie, by rotator je pomijał:
|
||||
```ts
|
||||
import { failOneproxyProxy } from "omniroute/oneproxyRotator";
|
||||
|
||||
const removed = await failOneproxyProxy("1.2.3.4", 8080);
|
||||
const removed = await failOneproxyProxy("203.0.113.7", 8080);
|
||||
if (removed) {
|
||||
console.log("Proxy marked as failed; rotator will skip it");
|
||||
}
|
||||
|
||||
@@ -645,7 +645,7 @@ for (const s of statuses) {
|
||||
}
|
||||
|
||||
// Force re-check a specific proxy
|
||||
invalidateProxyHealth("http://user:pass@1.2.3.4:8080");
|
||||
invalidateProxyHealth("http://user:pass@203.0.113.7:8080");
|
||||
```
|
||||
|
||||
The `stale` flag is `true` when the cache entry has exceeded `HEALTH_CACHE_TTL_MS` and the next request will trigger a fresh check.
|
||||
@@ -807,7 +807,7 @@ When a proxy consistently fails, mark it manually so the rotator will skip it:
|
||||
```ts
|
||||
import { failOneproxyProxy } from "omniroute/oneproxyRotator";
|
||||
|
||||
const removed = await failOneproxyProxy("1.2.3.4", 8080);
|
||||
const removed = await failOneproxyProxy("203.0.113.7", 8080);
|
||||
if (removed) {
|
||||
console.log("Proxy marked as failed; rotator will skip it");
|
||||
}
|
||||
|
||||
@@ -429,6 +429,7 @@ For deployments on small VPS instances (1 GB RAM or less):
|
||||
|
||||
- **Disable background services** — set `OMNIROUTE_DISABLE_BACKGROUND_SERVICES=1` to skip scheduler, MCP server, and periodic maintenance tasks. See `docs/reference/ENVIRONMENT.md`.
|
||||
- **Use SQLite WAL mode** — enabled by default, reduces peak memory during concurrent reads.
|
||||
- **Limit connection concurrency** — reduce `OMNIROUTE_MAX_POOL_SIZE` and `OMNIROUTE_DB_POOL_SIZE` in your environment.
|
||||
- **Cap the V8 heap** — set `OMNIROUTE_MEMORY_MB` (e.g. `512`) so the runtime does not calibrate a ceiling larger than the VM. See `docs/reference/ENVIRONMENT.md`.
|
||||
- **Limit concurrent heavy requests** — lower `OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT` (default `1`); excess requests get a retryable `503` with `Retry-After` instead of competing for memory.
|
||||
- **Avoid `next build` on the VPS** — build locally and deploy the standalone output (`.next/standalone/`).
|
||||
- **Monitor with `top` / `free -m`** — OmniRoute typically uses 200-400 MB RSS at idle on a 1 GB VM.
|
||||
|
||||
@@ -22,9 +22,10 @@ describe("MCP Essential Tools", () => {
|
||||
});
|
||||
|
||||
describe("Tool schema validation", () => {
|
||||
it("should have exactly 11 essential tools (includes web_search + web_fetch + tool_search)", () => {
|
||||
it("should have exactly 12 essential tools (includes web_search + web_fetch + tool_search)", () => {
|
||||
// 11 -> 12: #8925 shipped omniroute_create_combo as a phase-1 tool.
|
||||
const schemas = MCP_ESSENTIAL_TOOLS;
|
||||
expect(schemas).toHaveLength(11);
|
||||
expect(schemas).toHaveLength(12);
|
||||
});
|
||||
|
||||
it("all tools should have omniroute_ prefix", () => {
|
||||
|
||||
@@ -54,8 +54,21 @@ export function buildAccountSemaphoreKey({
|
||||
return `${String(provider)}:${String(accountKey)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective positive cap, or null when the semaphore is bypassed (unset/<=0).
|
||||
*
|
||||
* Narrowing companion of {@link isBypassed}: that one returns a plain boolean, so
|
||||
* TypeScript cannot narrow `number | null` to `number` in its else-branch (a
|
||||
* `x is null | undefined` predicate would be unsound — 0 bypasses too). Callers
|
||||
* that need the VALUE after the guard go through here instead of casting.
|
||||
*/
|
||||
function resolveActiveCap(maxConcurrency?: number | null): number | null {
|
||||
if (maxConcurrency == null || maxConcurrency <= 0) return null;
|
||||
return maxConcurrency;
|
||||
}
|
||||
|
||||
function isBypassed(maxConcurrency?: number | null): boolean {
|
||||
return maxConcurrency == null || maxConcurrency <= 0;
|
||||
return resolveActiveCap(maxConcurrency) === null;
|
||||
}
|
||||
|
||||
function createNoopReleaseFn(): () => void {
|
||||
@@ -192,7 +205,8 @@ export function acquire(
|
||||
maxQueueSize = DEFAULT_MAX_QUEUE_SIZE,
|
||||
}: AcquireAccountSemaphoreOptions = {}
|
||||
): Promise<() => void> {
|
||||
if (isBypassed(maxConcurrency)) {
|
||||
const activeCap = resolveActiveCap(maxConcurrency);
|
||||
if (activeCap === null) {
|
||||
return Promise.resolve(createNoopReleaseFn());
|
||||
}
|
||||
|
||||
@@ -200,7 +214,7 @@ export function acquire(
|
||||
return Promise.reject(makeAbortError(signal));
|
||||
}
|
||||
|
||||
const gate = ensureGate(semaphoreKey, maxConcurrency);
|
||||
const gate = ensureGate(semaphoreKey, activeCap);
|
||||
clearCleanupTimer(gate);
|
||||
|
||||
if (gate.running < gate.maxConcurrency && !isBlocked(gate)) {
|
||||
|
||||
@@ -22,6 +22,43 @@ import { updateProviderConnection } from "@/lib/db/providers";
|
||||
* Best-effort / non-fatal by design: a persistence failure must never block
|
||||
* the in-flight request, which already has the discovered id in hand.
|
||||
*/
|
||||
/**
|
||||
* Selection-side companion of the persistence write path (#8894): given a pool
|
||||
* of Antigravity/AGY connections, prefer the ones that already carry a stored
|
||||
* projectId — they can serve a request without the `loadCodeAssist` discovery
|
||||
* round-trip. "Prefer", not "require": when NO connection has a stored project
|
||||
* the pool is returned unchanged, so a fresh install never empties its
|
||||
* candidate list.
|
||||
*
|
||||
* Sync on purpose (called inside the quota-strategy connection expansion, which
|
||||
* builds candidate lists without awaiting per-connection work). Tolerates
|
||||
* `providerSpecificData` arriving either parsed or as the raw DB JSON string.
|
||||
*/
|
||||
export function preferAntigravityConnectionsWithStoredProject<T extends Record<string, unknown>>(
|
||||
connections: T[]
|
||||
): T[] {
|
||||
if (!Array.isArray(connections) || connections.length === 0) return connections;
|
||||
const hasStoredProject = (connection: T): boolean => {
|
||||
if (typeof connection.projectId === "string" && connection.projectId) return true;
|
||||
let psd = connection.providerSpecificData;
|
||||
if (typeof psd === "string") {
|
||||
try {
|
||||
psd = JSON.parse(psd);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return Boolean(
|
||||
psd &&
|
||||
typeof psd === "object" &&
|
||||
typeof (psd as Record<string, unknown>).projectId === "string" &&
|
||||
(psd as Record<string, unknown>).projectId
|
||||
);
|
||||
};
|
||||
const withStoredProject = connections.filter(hasStoredProject);
|
||||
return withStoredProject.length > 0 ? withStoredProject : connections;
|
||||
}
|
||||
|
||||
export async function persistDiscoveredAntigravityProjectId(
|
||||
connectionId: string | undefined | null,
|
||||
discoveredProjectId: string | undefined | null,
|
||||
|
||||
@@ -137,7 +137,9 @@ function normalizeRuntimeStep(
|
||||
: {}),
|
||||
weight,
|
||||
label,
|
||||
prompt: step.prompt || null,
|
||||
// `prompt` is a per-step pipeline input and only exists on a model step —
|
||||
// #8894 widened the union with ComboProviderWildcardStep, which has no prompt.
|
||||
prompt: (step.kind === "model" ? step.prompt : null) || null,
|
||||
} satisfies ResolvedComboTarget;
|
||||
}
|
||||
|
||||
|
||||
@@ -619,6 +619,7 @@ export async function tryRuntimeUnitDispatch(args: {
|
||||
nesting: nestingContext,
|
||||
baseOptions: buildBaseOptions(args),
|
||||
runCombo: args.runCombo,
|
||||
hiddenModelsByProvider: args.hiddenModelsByProvider,
|
||||
});
|
||||
recordRuntimeUnitStickySuccess({
|
||||
strategy,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* literal `auto/*` string panel member already behaves via the single-
|
||||
* dispatch safety net in src/sse/handlers/chat.ts.
|
||||
*/
|
||||
import { normalizeComboStep } from "../../../src/lib/combos/steps.ts";
|
||||
import { getComboModelString, normalizeComboStep } from "../../../src/lib/combos/steps.ts";
|
||||
import { executeComboRefUnit } from "./runtimeUnits.ts";
|
||||
import type {
|
||||
ComboCollectionLike,
|
||||
@@ -51,7 +51,11 @@ export function extractFusionPanelSpec(
|
||||
panel.push(step.comboName);
|
||||
return;
|
||||
}
|
||||
panel.push(step.model);
|
||||
// #8894 widened ComboStep with ComboProviderWildcardStep, which carries a
|
||||
// modelPattern instead of a model. getComboModelString() already resolves any
|
||||
// step shape (and returns null for the ones with no concrete model id).
|
||||
const modelStr = getComboModelString(step);
|
||||
if (modelStr) panel.push(modelStr);
|
||||
});
|
||||
return { panel, comboRefUnits };
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ import {
|
||||
type QuotaFetchCacheConfig,
|
||||
} from "./quotaScoring.ts";
|
||||
import { rankByHeadroom, type HeadroomSaturation } from "./headroomRanking.ts";
|
||||
import { preferAntigravityConnectionsWithStoredProject } from "../antigravityProjectPersistence.ts";
|
||||
import { preferAntigravityConnectionsWithStoredProject } from "../antigravityProjectPersist.ts";
|
||||
import { isQuotaExhaustedForRequest } from "../../../src/domain/quotaCache.ts";
|
||||
|
||||
const RESET_AWARE_CONNECTION_CACHE_TTL_MS = 30_000;
|
||||
|
||||
@@ -9,7 +9,12 @@
|
||||
import { isAccountSemaphoreFull } from "../accountSemaphore.ts";
|
||||
import { resolveComboTargets } from "./comboStructure.ts";
|
||||
import { lookupPositiveCap } from "./concurrencyCaps.ts";
|
||||
import type { ComboCollectionLike, ComboLike, ResolvedComboUnit } from "./types.ts";
|
||||
import type {
|
||||
ComboCollectionLike,
|
||||
ComboLike,
|
||||
HiddenModelsByProvider,
|
||||
ResolvedComboUnit,
|
||||
} from "./types.ts";
|
||||
|
||||
type CapLookup = (connectionId: string) => Promise<number | null>;
|
||||
|
||||
@@ -45,7 +50,12 @@ async function isConnectionAtConcurrencyCap(
|
||||
export async function isRuntimeUnitAtConcurrencyCap(
|
||||
unit: ResolvedComboUnit,
|
||||
allCombos: ComboCollectionLike,
|
||||
lookupCap: CapLookup = lookupPositiveCap
|
||||
lookupCap: CapLookup = lookupPositiveCap,
|
||||
// Threaded from the caller so the hidden-model snapshot resolved once per
|
||||
// request is reused. Without it resolveComboTargets falls back to its default
|
||||
// getHiddenModelsByProvider(), i.e. a fresh full key_value read per nested
|
||||
// combo-ref unit on EVERY request (#8878 threaded the other call sites).
|
||||
hiddenModelsByProvider?: HiddenModelsByProvider
|
||||
): Promise<boolean> {
|
||||
if (unit.kind === "model") {
|
||||
if (!unit.connectionId || !unit.provider) return false;
|
||||
@@ -55,7 +65,7 @@ export async function isRuntimeUnitAtConcurrencyCap(
|
||||
const childCombo = findComboByName(allCombos, unit.comboName);
|
||||
if (!childCombo) return false;
|
||||
|
||||
const targets = resolveComboTargets(childCombo, allCombos, 1);
|
||||
const targets = resolveComboTargets(childCombo, allCombos, 1, hiddenModelsByProvider);
|
||||
const byConnection = new Map<string, { provider: string; connectionId: string }>();
|
||||
for (const target of targets) {
|
||||
if (!target.connectionId || !target.provider) continue;
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
ComboNestingContext,
|
||||
HandleComboChatOptions,
|
||||
HandleSingleModel,
|
||||
HiddenModelsByProvider,
|
||||
IsModelAvailable,
|
||||
ResolvedComboRefTarget,
|
||||
ResolvedComboUnit,
|
||||
@@ -186,6 +187,7 @@ export async function executeRuntimeUnitCombo(args: {
|
||||
nesting: ComboNestingContext;
|
||||
baseOptions: HandleComboChatOptions;
|
||||
runCombo: RuntimeUnitRunner;
|
||||
hiddenModelsByProvider?: HiddenModelsByProvider;
|
||||
}): Promise<RuntimeUnitExecutionResult> {
|
||||
const maxRetries = Number(args.config.maxRetries ?? 1);
|
||||
const retryDelayMs = resolveDelayMs(args.config.retryDelayMs, 2000);
|
||||
@@ -197,7 +199,14 @@ export async function executeRuntimeUnitCombo(args: {
|
||||
let fallbackCount = 0;
|
||||
|
||||
for (const unit of orderedUnits) {
|
||||
if (await isRuntimeUnitAtConcurrencyCap(unit, args.allCombos)) {
|
||||
if (
|
||||
await isRuntimeUnitAtConcurrencyCap(
|
||||
unit,
|
||||
args.allCombos,
|
||||
undefined,
|
||||
args.hiddenModelsByProvider
|
||||
)
|
||||
) {
|
||||
args.log.info(
|
||||
"COMBO",
|
||||
`Skipping ${unit.kind} ${unitDisplayName(unit)} — concurrency cap reached`
|
||||
|
||||
@@ -292,7 +292,8 @@ function rehydrateEntry(hash: string, principalId: string, now: number): CcrEntr
|
||||
|
||||
// Re-admit through the same budgets a fresh store would face. If the block no longer
|
||||
// fits, it stays on disk and is served straight from the row instead of being cached.
|
||||
if (enforcePrincipalBudget(entry.principalId, entry.bytes) && enforceGlobalBudget(entry.bytes)) {
|
||||
const { principalId: owner, bytes } = entry;
|
||||
if (enforcePrincipalBudget(owner, bytes) && enforceGlobalBudget(owner, bytes)) {
|
||||
const key = buildStoreKey(hash, principalId === ANON ? undefined : principalId);
|
||||
ccrStore.set(key, entry);
|
||||
ccrTotalBytes += entry.bytes;
|
||||
|
||||
@@ -110,7 +110,8 @@ export function getFirecrawlBaseUrl(connection?: Record<string, unknown>): strin
|
||||
return envBase.replace(/\/+$/, "");
|
||||
}
|
||||
const providerData = toRecord(connection?.providerSpecificData);
|
||||
const connBase = typeof connection?.baseUrl === "string" ? connection.baseUrl : providerData?.baseUrl;
|
||||
const connBase =
|
||||
typeof connection?.baseUrl === "string" ? connection.baseUrl : providerData?.baseUrl;
|
||||
if (typeof connBase === "string" && connBase.trim() && !connBase.includes("api.firecrawl.dev")) {
|
||||
return connBase.trim().replace(/\/+$/, "");
|
||||
}
|
||||
@@ -120,7 +121,11 @@ export function getFirecrawlBaseUrl(connection?: Record<string, unknown>): strin
|
||||
export async function fetchFirecrawlQuota(
|
||||
connectionId: string,
|
||||
connection?: Record<string, unknown>
|
||||
): Promise<QuotaInfo | null> {
|
||||
// FirecrawlQuota, not the base QuotaInfo: every return here is a full credit
|
||||
// breakdown (remainingCredits / planCredits / extraCreditsInferred / overPlan),
|
||||
// and the narrower annotation made the custom-base literal below an excess-
|
||||
// property error. FirecrawlQuota extends QuotaInfo, so callers are unaffected.
|
||||
): Promise<FirecrawlQuota | null> {
|
||||
const cached = quotaCache.get(connectionId);
|
||||
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
|
||||
return cached.quota;
|
||||
|
||||
@@ -9,15 +9,23 @@
|
||||
|
||||
import { createHmac, createHash, randomUUID } from "node:crypto";
|
||||
|
||||
import { resolvePublicCred } from "../utils/publicCreds.ts";
|
||||
|
||||
export const RAYCAST_CHAT_URL = "https://backend.raycast.com/api/v1/ai/chat_completions";
|
||||
export const RAYCAST_MODELS_URL = "https://backend.raycast.com/api/v1/ai/models";
|
||||
export const RAYCAST_DEFAULT_USER_AGENT =
|
||||
"Raycast/1.104.20 (macOS Version 26.5.1 (Build 25F80))";
|
||||
export const RAYCAST_DEFAULT_USER_AGENT = "Raycast/1.104.20 (macOS Version 26.5.1 (Build 25F80))";
|
||||
export const RAYCAST_DEFAULT_EXPERIMENTAL = "chatBranching, mcpHTTPServer";
|
||||
|
||||
/** Community-extracted default; override via providerSpecificData.sigSecret or RAYCAST_SIG_SECRET. */
|
||||
export const RAYCAST_DEFAULT_SIG_SECRET =
|
||||
"6bc455473576ce2cd6f70426caff867aabbe3f7291c1a79681af5e8ce0ca1408";
|
||||
/**
|
||||
* Community-extracted default; override via providerSpecificData.sigSecret or
|
||||
* RAYCAST_SIG_SECRET. Embedded through resolvePublicCred() per Hard Rule #11 —
|
||||
* a public upstream credential must never be a string literal in the source
|
||||
* (see docs/security/PUBLIC_CREDS.md).
|
||||
*/
|
||||
export const RAYCAST_DEFAULT_SIG_SECRET = resolvePublicCred(
|
||||
"raycast_sig_secret",
|
||||
"RAYCAST_SIG_SECRET"
|
||||
);
|
||||
|
||||
export type RaycastCredentials = {
|
||||
accessToken?: string;
|
||||
@@ -69,9 +77,7 @@ export function raycastJwt(aid: string, secret: string): string {
|
||||
const iat = Date.now() / 1000;
|
||||
const header = base64UrlJson({ typ: "JWT", alg: "HS256" });
|
||||
const payload = base64UrlJson({ aid, exp: iat + 60, iat });
|
||||
const signature = createHmac("sha256", secret)
|
||||
.update(`${header}.${payload}`)
|
||||
.digest("base64url");
|
||||
const signature = createHmac("sha256", secret).update(`${header}.${payload}`).digest("base64url");
|
||||
return `${header}.${payload}.${signature}`;
|
||||
}
|
||||
|
||||
@@ -111,7 +117,10 @@ export function resolveRaycastSecrets(credentials: RaycastCredentials): {
|
||||
return { bearerToken, deviceId, aid, sigSecret };
|
||||
}
|
||||
|
||||
export function buildRaycastHeaders(payload: string, credentials: RaycastCredentials): Record<string, string> {
|
||||
export function buildRaycastHeaders(
|
||||
payload: string,
|
||||
credentials: RaycastCredentials
|
||||
): Record<string, string> {
|
||||
const { bearerToken, deviceId, aid, sigSecret } = resolveRaycastSecrets(credentials);
|
||||
const psd = credentials.providerSpecificData || {};
|
||||
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||||
@@ -137,7 +146,12 @@ export function contentToText(content: unknown): string {
|
||||
return content
|
||||
.map((part) => {
|
||||
if (typeof part === "string") return part;
|
||||
if (part && typeof part === "object" && "type" in part && (part as { type?: string }).type === "text") {
|
||||
if (
|
||||
part &&
|
||||
typeof part === "object" &&
|
||||
"type" in part &&
|
||||
(part as { type?: string }).type === "text"
|
||||
) {
|
||||
return String((part as { text?: string }).text || "");
|
||||
}
|
||||
return "";
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
* credits into the standard `{ plan, quotas }` response.
|
||||
*/
|
||||
|
||||
import { fetchFirecrawlQuota, getFirecrawlBaseUrl, type FirecrawlQuota } from "../firecrawlQuotaFetcher.ts";
|
||||
import {
|
||||
fetchFirecrawlQuota,
|
||||
getFirecrawlBaseUrl,
|
||||
type FirecrawlQuota,
|
||||
} from "../firecrawlQuotaFetcher.ts";
|
||||
import { createQuotaFromUsage, parseResetTime } from "./quota.ts";
|
||||
|
||||
function createFirecrawlPlanQuota(q: FirecrawlQuota) {
|
||||
@@ -29,7 +33,11 @@ function createFirecrawlPlanQuota(q: FirecrawlQuota) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function getFirecrawlUsage(connectionId: string, apiKey?: string, connection?: Record<string, unknown>) {
|
||||
export async function getFirecrawlUsage(
|
||||
connectionId: string,
|
||||
apiKey?: string,
|
||||
connection?: Record<string, unknown>
|
||||
) {
|
||||
if (!connectionId) {
|
||||
return { message: "Firecrawl: connection id unavailable." };
|
||||
}
|
||||
@@ -44,7 +52,12 @@ export async function getFirecrawlUsage(connectionId: string, apiKey?: string, c
|
||||
}
|
||||
|
||||
try {
|
||||
const live = await fetchFirecrawlQuota(connectionId, connection);
|
||||
// The explicit `apiKey` argument was silently dropped when #91bb6aa619 moved
|
||||
// this to fetchFirecrawlQuota(connectionId, connection): the fetcher reads the
|
||||
// key off the connection record, so a caller that passes the key directly —
|
||||
// without a connection carrying it — always got "API key not available".
|
||||
const resolvedConnection = apiKey ? { ...(connection || {}), apiKey } : connection;
|
||||
const live = await fetchFirecrawlQuota(connectionId, resolvedConnection);
|
||||
if (!live) {
|
||||
return { message: "Firecrawl API key not available or credit usage unavailable." };
|
||||
}
|
||||
|
||||
@@ -207,6 +207,15 @@ const EMBEDDED_DEFAULTS = {
|
||||
// Firefly credits balance endpoint public x-api-key (`SunbreakWebUI1`) from
|
||||
// GET firefly.adobe.io/v1/credits/balance browser traffic.
|
||||
adobe_firefly_balance_api_key: [60, 24, 0, 11, 0, 10, 20, 31, 50, 72, 18, 32, 43, 93],
|
||||
// Raycast Pro V2 request-signature secret (#8895). Community-extracted from the
|
||||
// public Raycast macOS client — the SAME value ships to every install, so it is
|
||||
// public by design, not a per-user credential. Overridable via RAYCAST_SIG_SECRET
|
||||
// or providerSpecificData.sigSecret.
|
||||
raycast_sig_secret: [
|
||||
89, 15, 13, 93, 71, 90, 65, 67, 86, 24, 71, 67, 1, 9, 91, 0, 73, 64, 87, 88, 93, 90, 91, 68, 12,
|
||||
20, 18, 3, 21, 70, 66, 3, 13, 11, 1, 72, 69, 87, 88, 95, 87, 88, 17, 94, 20, 67, 92, 27, 72, 68,
|
||||
3, 10, 92, 6, 21, 21, 84, 95, 14, 15, 88, 70, 95, 77,
|
||||
],
|
||||
} as const;
|
||||
|
||||
export type EmbeddedDefaultKey = keyof typeof EMBEDDED_DEFAULTS;
|
||||
|
||||
@@ -63,6 +63,7 @@ export const INTENTIONALLY_INTERNAL = new Set([
|
||||
"optimizationSettings", // db-internal: imported by db/core.ts for SQLite PRAGMA application helpers that require the live adapter
|
||||
"pluginMetrics", // DEAD? (production): write path não foi conectado ainda (documentado no cabeçalho do módulo); testado por tests/unit/plugins-metrics.test.ts
|
||||
"prompts", // DEAD? (production): zero callers de produção encontrados; domínio domain/prompts.ts é independente; testado por tests/integration/proxy-pipeline.test.ts
|
||||
"probeUtils", // db-internal: importado so por db/core.ts (retryProbeIfTransient no caminho da corruption-probe, #9541); testado por tests/unit/probe-9541-repro.test.ts
|
||||
"providerNodeSelect", // db-internal: importado só por db/providers.ts (selectProviderNodeForConnection — lógica pura de seleção de provider node split do providers.ts, #4421)
|
||||
"providerStats", // intentionally-internal: src/app/api/provider-stats/route.ts
|
||||
"proxyLatency", // intentionally-internal: imported directly by src/lib/db/proxies.ts (anti-barrel, #6798)
|
||||
|
||||
@@ -107,6 +107,11 @@ export const COLLECTORS = [
|
||||
glob: "open-sse/services/__tests__/antigravity-quota-family.test.ts",
|
||||
sources: ["vitest.mcp.config.ts"],
|
||||
},
|
||||
// #8890 landed this suite here without wiring a runner, so it had never run once.
|
||||
{
|
||||
glob: "open-sse/services/__tests__/fail-fast-concurrency-gate.test.ts",
|
||||
sources: ["vitest.mcp.config.ts"],
|
||||
},
|
||||
{ glob: "tests/unit/autoCombo/**/*.test.ts", sources: ["vitest.mcp.config.ts"] },
|
||||
{ glob: "src/lib/memory/__tests__/generic-backend.test.ts", sources: ["vitest.mcp.config.ts"] },
|
||||
{ glob: "tests/unit/encryption.spec.ts", sources: ["vitest.mcp.config.ts"] },
|
||||
|
||||
@@ -111,6 +111,14 @@ export const OAUTH_TEST_CONFIG = {
|
||||
// Validate using token presence/expiry as a lightweight auth check.
|
||||
checkExpiry: true,
|
||||
},
|
||||
raycast: {
|
||||
// #8895 — Raycast Pro is an `import_token` provider: the token is imported
|
||||
// from the local Raycast install, `refreshToken` is always null and the
|
||||
// stored `expiresIn` defaults to 30 days. There is nothing to refresh, so
|
||||
// the test is the expiry check on the imported token; without an entry here
|
||||
// Test Connection persists testStatus="error" on a healthy account (#8408).
|
||||
checkExpiry: true,
|
||||
},
|
||||
cline: CLINE_OAUTH_TEST_CONFIG,
|
||||
// ClinePass reuses the same WorkOS OAuth flow and token lifecycle as Cline.
|
||||
clinepass: CLINE_OAUTH_TEST_CONFIG,
|
||||
|
||||
@@ -5475,13 +5475,13 @@
|
||||
"newApiUserIdLabel": "ID de Usuário New-API",
|
||||
"newApiUserIdPlaceholder": "ex.: 12345",
|
||||
"newApiUserIdHint": "Valor do cabeçalho New-Api-User do AgentRouter, usado junto com a chave de API do console para consultar o saldo de cota.",
|
||||
"newApiAggregatorToggleLabel": "Gateway Agregador",
|
||||
"newApiAggregatorToggleHint": "Ativar detecção de saldo para nós agregadores New-API / One-API / Sub2API. O painel mostrará o badge de saldo e o roteamento de pré-voo de cota ignorará contas esgotadas.",
|
||||
"newApiAggregatorConsoleApiKeyHint": "Token de Acesso do Sistema para o endpoint /api/user/self do agregador. Não é a chave de API de roteamento.",
|
||||
"newApiAggregatorUserIdHint": "Valor do cabeçalho New-Api-User usado para consultar o saldo de cota do usuário do agregador.",
|
||||
"newApiAggregatorQuotaPerUnitLabel": "Cota por Unidade",
|
||||
"newApiAggregatorQuotaPerUnitHint": "Unidades de crédito New-API por $1 (padrão: 500000). Substitua se seu agregador usar uma taxa diferente.",
|
||||
"featureFlagNewApiAggregatorBalanceDescription": "Ativar detecção de saldo para nós compatíveis de agregadores New-API / One-API / Sub2API",
|
||||
"newApiAggregatorToggleLabel": "Gateway agregador",
|
||||
"newApiAggregatorToggleHint": "Ativa a detecção de saldo para nós agregadores New-API / One-API / Sub2API. O painel passa a mostrar o selo de saldo e o roteamento com quota-preflight pula contas esgotadas.",
|
||||
"newApiAggregatorConsoleApiKeyHint": "System Access Token para o endpoint /api/user/self do agregador. Não é a chave de API de roteamento.",
|
||||
"newApiAggregatorUserIdHint": "Valor do cabeçalho New-Api-User usado para buscar o saldo de quota do usuário do agregador.",
|
||||
"newApiAggregatorQuotaPerUnitLabel": "Quota por unidade",
|
||||
"newApiAggregatorQuotaPerUnitHint": "Unidades de crédito New-API por US$ 1 (padrão: 500000). Substitua se o seu agregador usar outra taxa.",
|
||||
"featureFlagNewApiAggregatorBalanceDescription": "Ativa a detecção de saldo para nós compatíveis New-API / One-API / Sub2API",
|
||||
"cpaModeDisabledTitle": "Habilitar backend CLIProxyAPI para emulação OAuth mais profunda do Claude Code",
|
||||
"cpaModeEnabledTitle": "Usando CLIProxyAPI para uma emulação mais profunda do Claude Code (uTLS, multi-conta, perfis de dispositivo)",
|
||||
"customUserAgentHint": "Override opcional enviado upstream como cabeçalho User-Agent desta conexão.",
|
||||
@@ -5597,7 +5597,7 @@
|
||||
"tagGroupPlaceholder": "ex.: personal, work, team-a",
|
||||
"testModel": "Test Model",
|
||||
"testingModel": "Testing Model",
|
||||
"modelTestQuotaTooltip": "Cota esgotada — reinicia amanhã ou precisa de recarga",
|
||||
"modelTestQuotaTooltip": "Quota esgotada — reseta amanhã ou precisa de recarga",
|
||||
"toggleOffShort": "OFF",
|
||||
"toggleOnShort": "ON",
|
||||
"tokenExpiredBadge": "Expirado",
|
||||
|
||||
@@ -6020,7 +6020,15 @@
|
||||
"ccAliasAddModelPlaceholder": "Id mô hình (ví dụ: gpt-4o)",
|
||||
"ccAliasAddModelButton": "Thêm ghi đè",
|
||||
"ccAliasLoadError": "Không tải được cài đặt bí danh khám phá: {error}",
|
||||
"ccAliasSaveError": "Không lưu được cài đặt bí danh khám phá: {error}"
|
||||
"ccAliasSaveError": "Không lưu được cài đặt bí danh khám phá: {error}",
|
||||
"newApiAggregatorToggleLabel": "Cổng tổng hợp",
|
||||
"newApiAggregatorToggleHint": "Bật phát hiện số dư cho các node tổng hợp New-API / One-API / Sub2API. Bảng điều khiển sẽ hiển thị huy hiệu số dư và định tuyến quota-preflight sẽ bỏ qua các tài khoản đã cạn.",
|
||||
"newApiAggregatorConsoleApiKeyHint": "System Access Token cho endpoint /api/user/self của bộ tổng hợp. Không phải khóa API định tuyến.",
|
||||
"newApiAggregatorUserIdHint": "Giá trị header New-Api-User dùng để lấy số dư quota của người dùng bộ tổng hợp.",
|
||||
"newApiAggregatorQuotaPerUnitLabel": "Quota mỗi đơn vị",
|
||||
"newApiAggregatorQuotaPerUnitHint": "Số đơn vị tín dụng New-API cho mỗi 1 USD (mặc định: 500000). Ghi đè nếu bộ tổng hợp của bạn dùng tỷ lệ khác.",
|
||||
"featureFlagNewApiAggregatorBalanceDescription": "Bật phát hiện số dư cho các node tương thích New-API / One-API / Sub2API",
|
||||
"modelTestQuotaTooltip": "Đã hết quota — sẽ đặt lại vào ngày mai hoặc cần nạp thêm"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Cài đặt",
|
||||
|
||||
@@ -254,40 +254,3 @@ export function deleteSessionModelHistory(sessionId: string, comboName: string):
|
||||
.run(sessionId, comboName);
|
||||
return result.changes ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get usage counts for ALL models in a session's history.
|
||||
* Returns a Map<{model}> -> {count} for least-used strategy.
|
||||
*
|
||||
* Queries the session_model_history table and aggregates by model_str.
|
||||
* Can optionally filter by connectionId if provided.
|
||||
*
|
||||
* @param connectionId - Optional connection ID to filter by. If not provided, returns all connections.
|
||||
* @returns Promise<Map<string, number>> of model strings to their usage count.
|
||||
*/
|
||||
export async function getSessionModelUsageCounts(
|
||||
connectionId?: string
|
||||
): Promise<Map<string, number>> {
|
||||
const db = getDbInstance() as any;
|
||||
|
||||
let sql = `SELECT model_str, COUNT(*) as count
|
||||
FROM session_model_history
|
||||
WHERE 1=1`;
|
||||
const params: unknown[] = [];
|
||||
|
||||
if (connectionId) {
|
||||
sql += ` AND connection_id = ?`;
|
||||
params.push(connectionId);
|
||||
}
|
||||
|
||||
sql += ` GROUP BY model_str ORDER BY count ASC`;
|
||||
|
||||
const rows = db.prepare(sql).all(...params) as Array<{ model_str: string; count: number }>;
|
||||
|
||||
const usageMap = new Map<string, number>();
|
||||
rows.forEach((row: { model_str: string; count: number }) => {
|
||||
usageMap.set(row.model_str, row.count);
|
||||
});
|
||||
|
||||
return usageMap;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { providerUsesAuthoritativeLiveCatalog } from "@omniroute/open-sse/config/providerRegistry";
|
||||
import { PROVIDER_ID_TO_ALIAS } from "@omniroute/open-sse/config/providerModels.ts";
|
||||
import { getSyncedAvailableModelsByConnection, type SyncedAvailableModel } from "../models";
|
||||
import {
|
||||
getSyncedAvailableModels,
|
||||
getSyncedAvailableModelsByConnection,
|
||||
type SyncedAvailableModel,
|
||||
} from "../models";
|
||||
import { getRawProviderConnections } from "../providers";
|
||||
|
||||
export type ActiveSyncedCatalog = {
|
||||
@@ -102,11 +106,26 @@ export async function getActiveSyncedCatalog(providerId: string): Promise<Active
|
||||
.map((connection) => connection.id);
|
||||
|
||||
const models = collectModelsForConnections(modelsByConnection, activeConnectionIds);
|
||||
if (models.length > 0) {
|
||||
return {
|
||||
authoritative: providerUsesAuthoritativeLiveCatalog(providerId),
|
||||
models,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
authoritative: models.length > 0 && providerUsesAuthoritativeLiveCatalog(providerId),
|
||||
models,
|
||||
};
|
||||
// No ACTIVE CONNECTION carries a catalog for this provider — but a provider
|
||||
// NODE can: nodes live in `provider_nodes`, never in `provider_connections`,
|
||||
// so filtering by active connection ids drops their synced catalog entirely.
|
||||
// Before #9294 this path read the provider-wide key_value set, and losing it
|
||||
// took every node's runtime metadata with it (supportedThinkingEfforts, so
|
||||
// `-high`/`-low` effort suffixes stopped resolving, plus contextWindow /
|
||||
// maxInputTokens used by the combo context-window filter).
|
||||
//
|
||||
// Fall back to that provider-wide set, and deliberately keep it
|
||||
// NON-authoritative: #9294's live-catalog gating is about what an active
|
||||
// connection actually serves, so a node-backed catalog must inform metadata
|
||||
// without ever being used to reject a model as unavailable.
|
||||
return { authoritative: false, models: await getSyncedAvailableModels(storedProviderId) };
|
||||
} catch {
|
||||
return { authoritative: false, models: [] };
|
||||
}
|
||||
|
||||
@@ -232,13 +232,19 @@ export async function callVisionModel(
|
||||
imageDataUri: string,
|
||||
config: VisionModelConfig,
|
||||
apiKey?: string,
|
||||
routerConfig?: Partial<import("./visionBridgeRouter").VisionBridgeRouterConfig>
|
||||
routerConfig?: Partial<import("./visionBridgeRouter").VisionBridgeRouterConfig>,
|
||||
deps?: import("./visionBridgeRouter").VisionBridgeRouterDeps
|
||||
): Promise<string> {
|
||||
// Auto-select the best vision model
|
||||
const modelToUse = await getBestVisionModel({
|
||||
fixedModel: config.model,
|
||||
...routerConfig,
|
||||
});
|
||||
// Auto-select the best vision model. `deps` is the router's existing
|
||||
// injectable credential-check seam — without forwarding it, tests (and any
|
||||
// embedder) cannot keep model selection away from the live connections DB.
|
||||
const modelToUse = await getBestVisionModel(
|
||||
{
|
||||
fixedModel: config.model,
|
||||
...routerConfig,
|
||||
},
|
||||
deps
|
||||
);
|
||||
// (#8430) When no vision-capable provider has usable credentials on this
|
||||
// instance, surface a clear error instead of attempting a describe call that
|
||||
// would fail with an opaque auth/serde error upstream.
|
||||
@@ -248,7 +254,7 @@ export async function callVisionModel(
|
||||
let lastError: Error | null = null;
|
||||
|
||||
// Try primary model + fallbacks
|
||||
const modelsToTry = [modelToUse, ...(await getFallbackModels(modelToUse, routerConfig))];
|
||||
const modelsToTry = [modelToUse, ...(await getFallbackModels(modelToUse, routerConfig, deps))];
|
||||
const maxAttempts = Math.min(modelsToTry.length, routerConfig?.maxFallbackAttempts ?? 3);
|
||||
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
|
||||
@@ -531,6 +531,10 @@ export const PROVIDERS = {
|
||||
KIRO: "kiro",
|
||||
AMAZON_Q: "amazon-q",
|
||||
CURSOR: "cursor",
|
||||
// #8895 — registered in src/lib/oauth/providers/index.ts but missing here, so
|
||||
// every consumer reading PROVIDERS (onboarding wizard, test-connection routing)
|
||||
// did not know Raycast Pro exists as an OAuth provider.
|
||||
RAYCAST: "raycast",
|
||||
KILOCODE: "kilocode",
|
||||
CLINE: "cline",
|
||||
CLINEPASS: "clinepass",
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
"tests/unit/adobe-firefly.test.ts",
|
||||
"tests/unit/anthropic-thinking-signature-recovery.test.ts",
|
||||
"tests/unit/antigravity-429-quota-tdd.test.ts",
|
||||
"tests/unit/antigravity-prefer-stored-project.test.ts",
|
||||
"tests/unit/api-key-rotator-health.test.ts",
|
||||
"tests/unit/appearance-widget-settings-schema.test.ts",
|
||||
"tests/unit/auth-antigravity-account-retry-v2.test.ts",
|
||||
@@ -206,6 +207,7 @@
|
||||
"tests/unit/error-sensitive-redaction.test.ts",
|
||||
"tests/unit/execute-chat-resource-pressure-breaker.test.ts",
|
||||
"tests/unit/executor-antigravity.test.ts",
|
||||
"tests/unit/executor-devin-cli-agentic-acp.test.ts",
|
||||
"tests/unit/executor-web-cookie-sweep.test.ts",
|
||||
"tests/unit/format-provider-error-cause.test.ts",
|
||||
"tests/unit/forwarded-header-budget.test.ts",
|
||||
|
||||
@@ -1488,6 +1488,29 @@
|
||||
"stream": "devin://acp/stdio"
|
||||
}
|
||||
},
|
||||
"devin-cli-agentic": {
|
||||
"format": "claude",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"nonStream": "devin://acp/stdio",
|
||||
"stream": "devin://acp/stdio"
|
||||
}
|
||||
},
|
||||
"dgrid": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
@@ -4274,6 +4297,52 @@
|
||||
"stream": "https://chat.qwen.ai/api/v2/chat/completions"
|
||||
}
|
||||
},
|
||||
"raycast": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"nonStream": "https://backend.raycast.com/api/v1/ai",
|
||||
"stream": "https://backend.raycast.com/api/v1/ai"
|
||||
}
|
||||
},
|
||||
"regolo": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"nonStream": "https://api.regolo.ai",
|
||||
"stream": "https://api.regolo.ai"
|
||||
}
|
||||
},
|
||||
"reka": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
@@ -4829,6 +4898,29 @@
|
||||
"stream": "https://hermes.ai.unturf.com/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"unorouter": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"nonStream": "https://api.unorouter.ai/v1/chat/completions",
|
||||
"stream": "https://api.unorouter.ai/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"upstage": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
|
||||
52
tests/unit/antigravity-prefer-stored-project.test.ts
Normal file
52
tests/unit/antigravity-prefer-stored-project.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Regression guard for the #8894 import break: quotaStrategies.ts imported
|
||||
* `preferAntigravityConnectionsWithStoredProject` from a module that never
|
||||
* landed (`antigravityProjectPersistence.ts`), killing the whole combo module
|
||||
* graph with ERR_MODULE_NOT_FOUND on the release tip. The helper now lives in
|
||||
* the real persistence module (`antigravityProjectPersist.ts`); these tests pin
|
||||
* its selection semantics.
|
||||
*
|
||||
* Run: node --import tsx/esm --test tests/unit/antigravity-prefer-stored-project.test.ts
|
||||
*/
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { preferAntigravityConnectionsWithStoredProject } from "../../open-sse/services/antigravityProjectPersist.ts";
|
||||
|
||||
const withProject = { id: "a", projectId: "proj-1" };
|
||||
const withNestedProject = { id: "b", providerSpecificData: { projectId: "proj-2" } };
|
||||
const withStringPsd = { id: "c", providerSpecificData: '{"projectId":"proj-3"}' };
|
||||
const withoutProject = { id: "d", projectId: null, providerSpecificData: {} };
|
||||
const withBrokenPsd = { id: "e", providerSpecificData: "{not json" };
|
||||
|
||||
test("prefers connections that already carry a stored projectId", () => {
|
||||
const pool = [withoutProject, withProject, withNestedProject];
|
||||
assert.deepEqual(
|
||||
preferAntigravityConnectionsWithStoredProject(pool).map((c) => c.id),
|
||||
["a", "b"]
|
||||
);
|
||||
});
|
||||
|
||||
test("reads projectId from a raw JSON-string providerSpecificData", () => {
|
||||
const pool = [withoutProject, withStringPsd];
|
||||
assert.deepEqual(
|
||||
preferAntigravityConnectionsWithStoredProject(pool).map((c) => c.id),
|
||||
["c"]
|
||||
);
|
||||
});
|
||||
|
||||
test("never empties the pool: no stored project anywhere → unchanged", () => {
|
||||
const pool = [withoutProject, withBrokenPsd];
|
||||
assert.deepEqual(preferAntigravityConnectionsWithStoredProject(pool), pool);
|
||||
});
|
||||
|
||||
test("empty input passes through", () => {
|
||||
assert.deepEqual(preferAntigravityConnectionsWithStoredProject([]), []);
|
||||
});
|
||||
|
||||
test("the quota-strategy module graph resolves (the #8894 break shape)", async () => {
|
||||
// Importing quotaStrategies transitively exercises the fixed import path; the
|
||||
// pre-fix tip died here with ERR_MODULE_NOT_FOUND before any test could run.
|
||||
const mod = await import("../../open-sse/services/combo/quotaStrategies.ts");
|
||||
assert.ok(mod, "quotaStrategies must be importable");
|
||||
});
|
||||
@@ -87,10 +87,15 @@ describe("detectModelFamily (pure)", () => {
|
||||
});
|
||||
|
||||
it("advertises exactly one auto/<family> catalog id per family", () => {
|
||||
assert.deepEqual(
|
||||
[...AUTO_FAMILY_IDS].sort(),
|
||||
["auto/gemini", "auto/gemma", "auto/glm", "auto/llama", "auto/mimo", "auto/minimax", "auto/zai"]
|
||||
);
|
||||
assert.deepEqual([...AUTO_FAMILY_IDS].sort(), [
|
||||
"auto/gemini",
|
||||
"auto/gemma",
|
||||
"auto/glm",
|
||||
"auto/llama",
|
||||
"auto/mimo",
|
||||
"auto/minimax",
|
||||
"auto/zai",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -133,7 +138,10 @@ describe("auto/<family> materialization (#6453)", () => {
|
||||
// "degrades gracefully" test below documents for opencode/minimax — a
|
||||
// no-auth backend that genuinely serves a family model IS a legitimate
|
||||
// member of the family pool, not just credentialed provider_connections rows.
|
||||
assert.deepEqual(providerIds, ["auggie", "glm", "zai"]);
|
||||
// `devin-cli-agentic` joined for the same documented reason as `auggie`:
|
||||
// #8914 added the Devin ACP bridge whose catalog (registry/devin/catalog.ts)
|
||||
// advertises the glm-5-2* line, so it genuinely serves the family.
|
||||
assert.deepEqual(providerIds, ["auggie", "devin-cli-agentic", "glm", "zai"]);
|
||||
// Every candidate must be a glm-family model (the Cartesian pool now surfaces
|
||||
// each backend's full glm line-up, not only the glm-5.2 default), and the
|
||||
// connected openai/gpt-4o-mini backend must be excluded — same family
|
||||
@@ -201,7 +209,6 @@ describe("auto/<family> materialization (#6453)", () => {
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it("rejects auto/<unknownfamily> with the same clean error as any unknown combo", async () => {
|
||||
await assert.rejects(
|
||||
() => builtinCatalog.createBuiltinAutoCombo("auto/unknownfam", "unknownfam"),
|
||||
|
||||
@@ -119,6 +119,32 @@ describe("CCR engine survives losing its in-memory map (#9061)", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("re-admits the disk row into the fresh map (the enforceGlobalBudget arity bug)", async () => {
|
||||
// The restart path re-admits a disk-served block through the same budgets a fresh
|
||||
// store would face. #9061 called enforceGlobalBudget(entry.bytes) with ONE argument
|
||||
// against a (owner, bytes) signature: `bytes` arrived undefined, `ccrTotalBytes +
|
||||
// undefined` is NaN, and `NaN <= MAX` is false — so the re-admit never happened and
|
||||
// the map stayed empty, re-reading from disk on every single retrieve. Typecheck
|
||||
// caught the arity; this pins the observable behaviour.
|
||||
const text = "z".repeat(2_000);
|
||||
const stored = ccr.tryStoreBlock(text, "principal-readmit");
|
||||
assert.equal(stored.stored, true);
|
||||
await ccr.flushCcrDurableWrites();
|
||||
|
||||
const restarted = await import(`${ccrPath}?restart=9061-readmit`);
|
||||
assert.equal(
|
||||
restarted.getCcrStoreStats("principal-readmit").entries,
|
||||
0,
|
||||
"a fresh instance starts with an empty map"
|
||||
);
|
||||
assert.equal(restarted.retrieveBlock(stored.hash, "principal-readmit"), text);
|
||||
assert.equal(
|
||||
restarted.getCcrStoreStats("principal-readmit").entries,
|
||||
1,
|
||||
"the disk-served block must be re-admitted into the map, not re-read every time"
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the principal boundary across the restart", async () => {
|
||||
const text = "y".repeat(2_000);
|
||||
const stored = ccr.tryStoreBlock(text, "principal-a");
|
||||
|
||||
@@ -250,7 +250,13 @@ test("chat completions route emits early keepalive while waiting for stream read
|
||||
await seedHealthyConnection();
|
||||
|
||||
globalThis.fetch = async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
// Must exceed resolveKeepaliveThreshold()'s DEFAULT_THRESHOLD_MS (2000ms) for
|
||||
// openai/* — otherwise withEarlyStreamKeepalive takes the FAST path, forwards
|
||||
// the handler response as-is and no keepalive frame is ever emitted. The old
|
||||
// 100ms only worked while unrelated handler latency happened to push the
|
||||
// total past the threshold, which made this assertion incidental rather than
|
||||
// deterministic; it stopped holding once the handler got faster.
|
||||
await new Promise((resolve) => setTimeout(resolve, 2_400));
|
||||
return new Response(
|
||||
[
|
||||
`data: ${JSON.stringify({
|
||||
|
||||
@@ -121,7 +121,7 @@ test("INTENTIONALLY_INTERNAL is exported from check-db-rules.mjs", () => {
|
||||
assert.ok(INTENTIONALLY_INTERNAL.size > 0, "INTENTIONALLY_INTERNAL must not be empty");
|
||||
});
|
||||
|
||||
test("INTENTIONALLY_INTERNAL contains the expected 37 audited modules", () => {
|
||||
test("INTENTIONALLY_INTERNAL contains the expected 38 audited modules", () => {
|
||||
const expected = [
|
||||
"_rowTypes",
|
||||
"accessTokens",
|
||||
@@ -147,6 +147,7 @@ test("INTENTIONALLY_INTERNAL contains the expected 37 audited modules", () => {
|
||||
"optimizationSettings",
|
||||
"pluginMetrics",
|
||||
"prompts",
|
||||
"probeUtils",
|
||||
"providerNodeSelect",
|
||||
"providerStats",
|
||||
"proxyLatency",
|
||||
|
||||
@@ -56,7 +56,11 @@ function capabilityEntry(limitContext: number | null) {
|
||||
};
|
||||
}
|
||||
|
||||
function capabilityEntryWithLimits(limitInput: number | null, limitContext: number | null, limitOutput = 4096) {
|
||||
function capabilityEntryWithLimits(
|
||||
limitInput: number | null,
|
||||
limitContext: number | null,
|
||||
limitOutput = 4096
|
||||
) {
|
||||
return {
|
||||
...capabilityEntry(limitContext),
|
||||
limit_input: limitInput,
|
||||
@@ -325,10 +329,12 @@ test("small input-only maxInputTokens keeps a target whose input fits even thoug
|
||||
);
|
||||
});
|
||||
|
||||
test("input-only maxInputTokens still rejects when the input itself exceeds the cap", () => {
|
||||
// The fix must not let a genuinely-too-small input cap pass. `too-small` has
|
||||
// maxInputTokens = 1, which cannot even hold the ~11-token input, so it must
|
||||
// still be dropped while the compatible target survives.
|
||||
test("input-only maxInputTokens is demoted when the input itself exceeds the cap", () => {
|
||||
// #8944 made context metadata ADVISORY: a catalog-too-small target is no longer
|
||||
// removed (a stale catalog entry must never delete the only target that could
|
||||
// accept the request at runtime), it is ordered AFTER the known-fitting ones.
|
||||
// `too-small` has maxInputTokens = 1, which cannot hold the ~11-token input, so
|
||||
// it must lose the ordering to `huge` while remaining available as a fallback.
|
||||
saveModelsDevCapabilities({
|
||||
"unit-7039-too-small": {
|
||||
"too-small": capabilityEntryWithLimits(1, 1_000_000, 500),
|
||||
@@ -344,14 +350,14 @@ test("input-only maxInputTokens still rejects when the input itself exceeds the
|
||||
|
||||
assert.deepEqual(
|
||||
out.map((entry) => entry.modelStr),
|
||||
["unit-7039-too-small/huge"]
|
||||
["unit-7039-too-small/huge", "unit-7039-too-small/too-small"]
|
||||
);
|
||||
});
|
||||
|
||||
test("maxInputTokens defaulting to contextWindow still rejects when input + output exceeds the total window (#7039 follow-up)", () => {
|
||||
// Shared-window model where maxInputTokens equals the total window size.
|
||||
// The input alone fits the input cap, but input + output overflows the
|
||||
// window, so the target must be rejected instead of passing on the input cap.
|
||||
test("maxInputTokens defaulting to contextWindow is demoted when input + output exceeds the total window (#7039 follow-up)", () => {
|
||||
// Shared-window model where maxInputTokens equals the total window size. The
|
||||
// input alone fits the input cap but input + output overflows the window, so the
|
||||
// target must not be PREFERRED — since #8944 it is demoted rather than dropped.
|
||||
saveModelsDevCapabilities({
|
||||
"unit-7039-window": {
|
||||
"shared-window": capabilityEntryWithLimits(400_000, 400_000, 200_000),
|
||||
@@ -367,7 +373,7 @@ test("maxInputTokens defaulting to contextWindow still rejects when input + outp
|
||||
|
||||
assert.deepEqual(
|
||||
out.map((entry) => entry.modelStr),
|
||||
["unit-7039-window/huge"]
|
||||
["unit-7039-window/huge", "unit-7039-window/shared-window"]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -391,16 +397,18 @@ test("model_context_override lets a small-catalog target survive a large-context
|
||||
largeContextBody(),
|
||||
noopLog
|
||||
);
|
||||
assert.deepEqual(
|
||||
out.map((entry) => entry.modelStr).sort(),
|
||||
["unit-override/big", "unit-override/capped"]
|
||||
);
|
||||
assert.deepEqual(out.map((entry) => entry.modelStr).sort(), [
|
||||
"unit-override/big",
|
||||
"unit-override/capped",
|
||||
]);
|
||||
} finally {
|
||||
removeModelContextOverride("unit-override", "capped");
|
||||
}
|
||||
});
|
||||
|
||||
test("without an override the small-catalog target is still dropped for the large request", () => {
|
||||
// #8944: "dropped" became "demoted" — the small-catalog target survives as a
|
||||
// runtime fallback but must never outrank the one whose known limit fits.
|
||||
test("without an override the small-catalog target is ordered last for the large request", () => {
|
||||
saveModelsDevCapabilities({
|
||||
"unit-override": {
|
||||
big: capabilityEntry(1_000_000),
|
||||
@@ -417,6 +425,6 @@ test("without an override the small-catalog target is still dropped for the larg
|
||||
|
||||
assert.deepEqual(
|
||||
out.map((entry) => entry.modelStr),
|
||||
["unit-override/big"]
|
||||
["unit-override/big", "unit-override/capped"]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -8,8 +8,15 @@ import { sessionDedupEngine } from "../../../open-sse/services/compression/engin
|
||||
|
||||
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
const FIXTURE = join(REPO_ROOT, "tests/fixtures/compression/session-dedup-memory-7849.ts");
|
||||
// #7849 originally bounded session-dedup with a shared "suffix work budget" that
|
||||
// emitted SUFFIX_WORK_BUDGET_WARNING when exhausted. Commit 7f36b192f0 REPLACED
|
||||
// that mechanism with the MAX_SUFFIX_STARTS / MAX_TOTAL_BLOCK_BYTES guards and
|
||||
// removed both the budget and its warning. The invariant #7849 exists for — a
|
||||
// line-rich long context must not blow the heap, and the engine must fail open —
|
||||
// is unchanged and still guarded below; only the pins on the removed mechanism
|
||||
// were rewritten. The budget size is kept as the input-shaping constant that
|
||||
// produces the pathological pair.
|
||||
const SUFFIX_WORK_BUDGET = 32 * 1024 * 1024;
|
||||
const SUFFIX_WORK_BUDGET_WARNING = "session-dedup: skipped (suffix work budget exceeded)";
|
||||
|
||||
function makeFixedWidthText(lineCount: number, lineChars: number, tag: string): string {
|
||||
return Array.from({ length: lineCount }, (_, index) => {
|
||||
@@ -38,7 +45,7 @@ function makeSharedBudgetBody(): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
test("#7849: shares the two-pass suffix-work budget across all messages", () => {
|
||||
test("#7849: the two-message pathological pair stays bounded", () => {
|
||||
const body = makeSharedBudgetBody();
|
||||
const messages = body.messages as Array<{ content: string }>;
|
||||
const perMessageWork = messages.map(({ content }) => projectedSuffixWork(content, 2));
|
||||
@@ -64,20 +71,29 @@ test("#7849: shares the two-pass suffix-work budget across all messages", () =>
|
||||
assert.equal(individualResult.stats, null, "each message must be accepted individually");
|
||||
}
|
||||
|
||||
// The pathological pair must be processed BOUNDED — quickly and without
|
||||
// corrupting the body. Pre-#7849 this shape retained one full-length suffix
|
||||
// string per line and OOM-killed the heap.
|
||||
const started = Date.now();
|
||||
const result = sessionDedupEngine.apply(body);
|
||||
assert.deepEqual(result.stats?.validationWarnings, [SUFFIX_WORK_BUDGET_WARNING]);
|
||||
assert.ok(
|
||||
Date.now() - started < 4000,
|
||||
"the pathological pair must stay fast; quadratic work would take seconds"
|
||||
);
|
||||
assert.ok(Array.isArray((result.body as { messages?: unknown[] }).messages));
|
||||
});
|
||||
|
||||
test("#7849: exhausted suffix-work budget fails open with exact zero-savings stats", () => {
|
||||
test("#7849: the pathological pair fails open, returning the input body untouched", () => {
|
||||
const body = makeSharedBudgetBody();
|
||||
const result = sessionDedupEngine.apply(body);
|
||||
|
||||
assert.strictEqual(result.body, body, "budget exhaustion must return the input body by identity");
|
||||
// Fail-open is the surviving contract: nothing deduplicable in this shape, so
|
||||
// the ORIGINAL body comes back by identity and no compression is claimed. The
|
||||
// explanatory zero-savings stats belonged to the removed budget path — the
|
||||
// current guards skip before producing any, so stats is null.
|
||||
assert.strictEqual(result.body, body, "failing open must return the input body by identity");
|
||||
assert.equal(result.compressed, false);
|
||||
assert.ok(result.stats, "budget exhaustion must return explanatory stats");
|
||||
assert.equal(result.stats.originalTokens, result.stats.compressedTokens);
|
||||
assert.equal(result.stats.savingsPercent, 0);
|
||||
assert.deepEqual(result.stats.validationWarnings, [SUFFIX_WORK_BUDGET_WARNING]);
|
||||
assert.equal(result.stats, null);
|
||||
});
|
||||
|
||||
test("#7849: near-boundary under-budget request still deduplicates", () => {
|
||||
@@ -130,9 +146,13 @@ test(
|
||||
warnings: string[];
|
||||
};
|
||||
assert.deepEqual(output.enginesRun, ["session-dedup", "lite", "rtk", "headroom", "caveman"]);
|
||||
// The OOM guard is `child.status === 0` plus the full engine chain above:
|
||||
// pre-fix this fixture killed the 512 MiB heap before the pipeline finished.
|
||||
// session-dedup must still REPORT its skip; the exact reason string moved
|
||||
// with the mechanism (7f36b192f0), so only the prefix is pinned.
|
||||
assert.ok(
|
||||
output.warnings.includes("session-dedup: skipped (suffix work budget exceeded)"),
|
||||
`expected an explicit session-dedup work-budget warning, got ${JSON.stringify(output.warnings)}`
|
||||
output.warnings.some((warning) => warning.startsWith("session-dedup: skipped")),
|
||||
`expected a session-dedup skip warning, got ${JSON.stringify(output.warnings)}`
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -30,13 +30,13 @@ const {
|
||||
isControlPlaneProxyDirectFallbackEnabled,
|
||||
} = await import("../../src/shared/utils/featureFlags.ts");
|
||||
|
||||
const EXPECTED_FEATURE_FLAG_COUNT = 45;
|
||||
const EXPECTED_FEATURE_FLAG_COUNT = 46;
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// Test group 1 — Flag definitions registry
|
||||
// ──────────────────────────────────────────────────────
|
||||
describe("featureFlagDefinitions", () => {
|
||||
it("has exactly 45 flag definitions", () => {
|
||||
it("has exactly 46 flag definitions", () => {
|
||||
assert.strictEqual(FEATURE_FLAG_DEFINITIONS.length, EXPECTED_FEATURE_FLAG_COUNT);
|
||||
});
|
||||
|
||||
@@ -332,7 +332,7 @@ describe("resolveFeatureFlag", () => {
|
||||
});
|
||||
|
||||
describe("resolveAllFeatureFlags", () => {
|
||||
it("returns all 45 flags", () => {
|
||||
it("returns all 46 flags", () => {
|
||||
const all = resolveAllFeatureFlags();
|
||||
assert.strictEqual(all.length, EXPECTED_FEATURE_FLAG_COUNT);
|
||||
});
|
||||
|
||||
@@ -21,7 +21,19 @@
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { callVisionModel, type VisionModelConfig } from "@/lib/guardrails/visionBridgeHelpers";
|
||||
import {
|
||||
callVisionModel as callVisionModelRaw,
|
||||
type VisionModelConfig,
|
||||
} from "@/lib/guardrails/visionBridgeHelpers";
|
||||
|
||||
// Inject the router's credential-check seam as INDETERMINATE (null): the fixed
|
||||
// model is used as-is and selection never touches the live connections DB — on a
|
||||
// clean box the suite otherwise dies with "No vision-capable provider connected",
|
||||
// and on a dev box auto-selection may swap the model under the assertions.
|
||||
const callVisionModel = (img: string, config: VisionModelConfig) =>
|
||||
callVisionModelRaw(img, config, undefined, undefined, {
|
||||
hasUsableCredentials: async () => null,
|
||||
});
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
|
||||
@@ -510,8 +510,14 @@ test("v1 image edit POST executes Codex through the configured connection proxy"
|
||||
host: "127.0.0.1",
|
||||
port: 1,
|
||||
});
|
||||
// #9100: the reachability probe is NON-BLOCKING — dispatch is optimistic and the
|
||||
// probe aborts the request only while it is still in flight (t14 pattern). The
|
||||
// mock must stay pending: an instantly-throwing fetch would settle the race
|
||||
// first and surface as a generic 502 upstream error instead of the proxy 503.
|
||||
// Never resolved on purpose so the aborted continuation cannot proceed.
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error("Direct fetch must not run when the configured proxy is unreachable");
|
||||
await new Promise(() => {});
|
||||
throw new Error("unreachable");
|
||||
};
|
||||
|
||||
const response = await imageEditRoute.POST(
|
||||
@@ -537,8 +543,11 @@ test("v1 image generation POST resolves proxy and executes with proxy context wh
|
||||
port: 1, // intentionally unreachable — proves proxy path was taken
|
||||
});
|
||||
|
||||
// #9100 non-blocking probe: keep the request in flight so the fast-fail can
|
||||
// abort it with the proxy-specific 503 (see the edit-route case above).
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error("fetch should not be called — proxy fast-fail should trigger first");
|
||||
await new Promise(() => {});
|
||||
throw new Error("unreachable");
|
||||
};
|
||||
|
||||
const response = await imageRoute.POST(
|
||||
|
||||
@@ -46,6 +46,8 @@ test("public login bootstrap route exposes the metadata the login page consumes"
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(body, {
|
||||
// #9491 added `authenticated` so /login can redirect an active session.
|
||||
authenticated: false,
|
||||
requireLogin: true,
|
||||
hasPassword: false,
|
||||
setupComplete: true,
|
||||
@@ -68,6 +70,8 @@ test("public login bootstrap route reports env-provided bootstrap password metad
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(body, {
|
||||
// #9491 added `authenticated` so /login can redirect an active session.
|
||||
authenticated: false,
|
||||
requireLogin: true,
|
||||
hasPassword: true,
|
||||
setupComplete: true,
|
||||
@@ -89,6 +93,8 @@ test("public login bootstrap route reports stored password metadata and disabled
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(body, {
|
||||
// #9491 added `authenticated` so /login can redirect an active session.
|
||||
authenticated: false,
|
||||
requireLogin: false,
|
||||
hasPassword: true,
|
||||
setupComplete: true,
|
||||
|
||||
@@ -6,7 +6,7 @@ import assert from "node:assert/strict";
|
||||
// (omniroute_agent_skills_list/get/coverage) are intentionally defined in BOTH
|
||||
// MCP_TOOLS (open-sse/mcp-server/schemas/tools.ts) and agentSkillTools
|
||||
// (open-sse/mcp-server/tools/agentSkillTools.ts), so the additive sum reported 121
|
||||
// while only 107 distinct tool names actually exist. countUniqueMcpTools
|
||||
// while only 108 distinct tool names actually exist. countUniqueMcpTools
|
||||
// (open-sse/mcp-server/toolCount.ts) fixes this by unioning tool names from every
|
||||
// registered collection into a Set, so each user-visible tool is counted once.
|
||||
|
||||
@@ -60,7 +60,7 @@ test("#6854: countUniqueMcpTools de-duplicates tools registered in multiple coll
|
||||
};
|
||||
|
||||
const total = countUniqueMcpTools(collections);
|
||||
assert.equal(total, 107, "the published MCP inventory must match the registered tool set");
|
||||
assert.equal(total, 108, "the published MCP inventory must match the registered tool set");
|
||||
|
||||
// Independently compute the "true" unique count by unioning every collection's
|
||||
// tool names into a Set — this must equal countUniqueMcpTools's own result AND
|
||||
|
||||
@@ -40,6 +40,7 @@ const {
|
||||
OAUTH_TIMEOUT,
|
||||
PROVIDERS: OAUTH_PROVIDER_IDS,
|
||||
QODER_CONFIG,
|
||||
RAYCAST_CONFIG,
|
||||
TRAE_CONFIG,
|
||||
WINDSURF_CONFIG,
|
||||
XAI_OAUTH_CONFIG,
|
||||
@@ -63,6 +64,7 @@ const EXPECTED_PROVIDER_KEYS = [
|
||||
"amazon-q",
|
||||
"cursor",
|
||||
"trae",
|
||||
"raycast",
|
||||
"kilocode",
|
||||
"cline",
|
||||
"clinepass",
|
||||
@@ -100,6 +102,7 @@ const EXPECTED_CONFIG_BY_PROVIDER = {
|
||||
clinepass: CLINE_CONFIG, // reuses the Cline WorkOS flow (clinepass: cline in providers/index.ts)
|
||||
windsurf: WINDSURF_CONFIG,
|
||||
"devin-cli": WINDSURF_CONFIG,
|
||||
raycast: RAYCAST_CONFIG,
|
||||
trae: TRAE_CONFIG,
|
||||
"grok-cli": GROK_BUILD_OAUTH_CONFIG,
|
||||
"xai-oauth": XAI_OAUTH_CONFIG,
|
||||
|
||||
@@ -101,6 +101,13 @@ test("Claude provider limits fail closed when an account proxy is unreachable",
|
||||
await withMockedFetch(
|
||||
(async (url) => {
|
||||
directFetchUrls.push(String(url));
|
||||
// #9100: the reachability probe is NON-BLOCKING — dispatch is optimistic and
|
||||
// the probe aborts the request only while it is still in flight (same shape
|
||||
// as t14-proxy-fast-fail). The mock must therefore stay pending: an instant
|
||||
// response would win the race and the fast-fail would never be observable.
|
||||
// Never resolved on purpose — the aborted continuation must NOT proceed to a
|
||||
// real (unmocked) fetch after this block restores globalThis.fetch.
|
||||
await new Promise(() => {});
|
||||
return claudeUsageResponse();
|
||||
}) as typeof fetch,
|
||||
async () => {
|
||||
@@ -108,10 +115,20 @@ test("Claude provider limits fail closed when an account proxy is unreachable",
|
||||
() => providerLimits.fetchAndPersistProviderLimits(connectionId, "manual"),
|
||||
/Proxy unreachable|fetch failed|ECONNREFUSED|UND_ERR_CONNECT_TIMEOUT/i
|
||||
);
|
||||
// The fail-closed proof is twofold: (1) the rejection above settled at all —
|
||||
// a direct retry would await the hung mock and never reject; (2) nothing
|
||||
// egresses AFTER the fast-fail. The in-flight count itself may be >1: the
|
||||
// Claude flow dispatches bootstrap + oauth/usage concurrently, and both are
|
||||
// optimistic pre-abort attempts, not retries.
|
||||
const urlsAtRejection = directFetchUrls.length;
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
assert.equal(
|
||||
directFetchUrls.length,
|
||||
urlsAtRejection,
|
||||
"account-proxied Claude usage must not egress anything after the fast-fail"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepEqual(directFetchUrls, [], "account-proxied Claude usage must not retry direct");
|
||||
});
|
||||
|
||||
test("non-Claude OAuth provider limits fail closed when an account proxy is unreachable", async () => {
|
||||
@@ -136,6 +153,9 @@ test("non-Claude OAuth provider limits fail closed when an account proxy is unre
|
||||
await withMockedFetch(
|
||||
(async (url) => {
|
||||
directFetchUrls.push(String(url));
|
||||
// #9100: non-blocking probe — keep the request in flight so the fast-fail
|
||||
// can abort it (see the Claude case above for the full rationale).
|
||||
await new Promise(() => {});
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
copilot_plan: "free",
|
||||
@@ -153,7 +173,10 @@ test("non-Claude OAuth provider limits fail closed when an account proxy is unre
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepEqual(directFetchUrls, [], "account-proxied OAuth usage must not retry direct");
|
||||
assert.ok(
|
||||
directFetchUrls.length <= 1,
|
||||
"at most the single optimistic in-flight attempt — account-proxied OAuth usage must never retry direct after the fast-fail"
|
||||
);
|
||||
});
|
||||
|
||||
test("Claude provider limits preserve direct retry for non-account proxy failures", async () => {
|
||||
|
||||
@@ -934,7 +934,8 @@ test("provider models route retries Antigravity discovery endpoints before retur
|
||||
// After PR #2219, the discovery flow calls loadCodeAssist first as a project
|
||||
// bootstrap; treat all bootstrap calls as non-fatal failures so the test
|
||||
// exercises the discovery retry path.
|
||||
if (urlString.includes("/v1internal:loadCodeAssist")) {
|
||||
// onboardUser is a bootstrap hop too (ff012ff420) — else it eats the single 503 below.
|
||||
if (urlString.includes(":loadCodeAssist") || urlString.includes(":onboardUser")) {
|
||||
return new Response("nope", { status: 503 });
|
||||
}
|
||||
seenUrls.push(urlString);
|
||||
@@ -983,6 +984,8 @@ test("provider models route retries Antigravity discovery endpoints before retur
|
||||
"https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels",
|
||||
]);
|
||||
assert.deepEqual(body.models, [
|
||||
// #9106: both alias ids are user-callable now, so the upstream echo survives the filter.
|
||||
{ id: "gemini-3.1-pro-high", name: "Gemini 3.1 Pro (High)" },
|
||||
{ id: "gemini-pro-agent", name: "Gemini 3.1 Pro (High)" },
|
||||
{ id: "gemini-3.6-flash-high", name: "Gemini 3.6 Flash (High)" },
|
||||
{ id: "gemini-3.6-flash-medium", name: "Gemini 3.6 Flash (Medium)" },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Characterization of the providers.ts catalog split (god-file decomposition): the host became a
|
||||
// barrel that re-exports 10 data catalogs now living under constants/providers/*, and APIKEY is
|
||||
// merged from 6 semantic family files (apikey/<family>.ts). Locks: the public surface (every catalog
|
||||
// + helpers still exported), the spread-merge integrity (197 APIKEY entries, no loss/dup), and that
|
||||
// + helpers still exported), the spread-merge integrity (198 APIKEY entries, no loss/dup), and that
|
||||
// load-time Zod validation still runs. Pure-data move → behavior must be identical.
|
||||
// Count was 171 before obsolete provider removals (PR #6675: glhf/kluster/cablyai/inclusionai etc.,
|
||||
// 171->167) plus #6126 (ClinePass dual-auth): the API-key-only APIKEY_PROVIDERS_GATEWAYS entry was
|
||||
@@ -17,7 +17,7 @@
|
||||
// sarvam+plamo in regional) to 193, then #8170 (inception/typhoon — inception in frontier-labs,
|
||||
// typhoon in regional) to 195, then Firecrawl dual search+fetch under SEARCH_PROVIDERS.firecrawl
|
||||
// (removed specialty-media duplicate) to 194, #8861 (Xiaomi MiMo Token Plan, regional) to 195, and
|
||||
// the Cheaper Inference gateway (OSS-sponsor reseller, gateways family) to 197 (UnoRouter, #9009).
|
||||
// the Cheaper Inference gateway (OSS-sponsor reseller, gateways family) to 198 (UnoRouter #9009, Raycast Pro #8895).
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
@@ -46,12 +46,12 @@ test("barrel still exports every catalog + key helpers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("APIKEY_PROVIDERS merges the 6 family files into 197 entries (no loss / no dup)", async () => {
|
||||
test("APIKEY_PROVIDERS merges the 6 family files into 198 entries (no loss / no dup)", async () => {
|
||||
const keys = Object.keys((P as Record<string, object>).APIKEY_PROVIDERS);
|
||||
assert.equal(keys.length, 197);
|
||||
assert.equal(new Set(keys).size, 197, "duplicate keys after spread-merge");
|
||||
assert.equal(keys.length, 198);
|
||||
assert.equal(new Set(keys).size, 198, "duplicate keys after spread-merge");
|
||||
// the merged object's entry-count equals the sum of the 6 semantic family files; families are a
|
||||
// strict partition (every provider in exactly one), so the sum must be exactly 197.
|
||||
// strict partition (every provider in exactly one), so the sum must be exactly 198.
|
||||
const families: [string, string][] = [
|
||||
["gateways", "APIKEY_PROVIDERS_GATEWAYS"],
|
||||
["frontier-labs", "APIKEY_PROVIDERS_FRONTIER"],
|
||||
@@ -71,7 +71,7 @@ test("APIKEY_PROVIDERS merges the 6 family files into 197 entries (no loss / no
|
||||
seen.add(k);
|
||||
}
|
||||
}
|
||||
assert.equal(famTotal, 197, "families must partition all 197 providers");
|
||||
assert.equal(famTotal, 198, "families must partition all 198 providers");
|
||||
});
|
||||
|
||||
test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => {
|
||||
|
||||
@@ -85,15 +85,20 @@ test("reservoir keeps refreshing after updateSettings() touches an already-heart
|
||||
// number of event-loop ticks: Bottleneck's own updateSettings() goes through
|
||||
// at least one real setTimeout(0) (yieldLoop) before storeOptions reflects the
|
||||
// new value.
|
||||
// #9604 replaced Bottleneck's fixed-window reservoir with the rolling lease gate
|
||||
// (open-sse/services/rollingRpmGate.ts), so `reservoir` is null now and pinning it
|
||||
// would assert a mechanism that no longer exists. What must still hold — and what
|
||||
// the Bottleneck heartbeat bug actually broke — is that the limiter SURVIVES the
|
||||
// header-learned updateSettings() and keeps admitting work (steps 3 and 4 below).
|
||||
const pollDeadline = Date.now() + 2000;
|
||||
let state = await rateLimitManager.__getLimiterStateForTests(PROVIDER, CONNECTION_ID, null);
|
||||
while (state?.reservoir !== 2 && Date.now() < pollDeadline) {
|
||||
while (!state && Date.now() < pollDeadline) {
|
||||
await wait(10);
|
||||
state = await rateLimitManager.__getLimiterStateForTests(PROVIDER, CONNECTION_ID, null);
|
||||
}
|
||||
assert.equal(state?.reservoir, 2, "reservoir must land at 2 before the slots below are consumed");
|
||||
assert.ok(state, "the limiter must still exist after the header-learned update");
|
||||
|
||||
// 3. Consume both reservoir slots.
|
||||
// 3. Consume the learned capacity.
|
||||
assert.equal(
|
||||
await rateLimitManager.withRateLimit(PROVIDER, CONNECTION_ID, null, async () => "slot-1"),
|
||||
"slot-1"
|
||||
@@ -103,10 +108,9 @@ test("reservoir keeps refreshing after updateSettings() touches an already-heart
|
||||
"slot-2"
|
||||
);
|
||||
|
||||
// 4. Reservoir is now 0. A healthy Bottleneck heartbeat refills it ~1s later
|
||||
// from the reservoirRefreshInterval/reservoirRefreshAmount configured above.
|
||||
// Race a 3rd request against a 5s timer: if the heartbeat died (unfixed bug),
|
||||
// the request stays QUEUED forever and the timer wins instead.
|
||||
// 4. Capacity is spent. Race a 3rd request against a 5s timer: if the limiter
|
||||
// stopped pacing after updateSettings() (the original bug) the request stays
|
||||
// queued forever and the timer wins instead.
|
||||
const RACE_TIMEOUT_MS = 5000;
|
||||
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
||||
const timeout = new Promise<"timed-out">((resolve) => {
|
||||
@@ -125,8 +129,8 @@ test("reservoir keeps refreshing after updateSettings() touches an already-heart
|
||||
assert.equal(
|
||||
result,
|
||||
"slot-3",
|
||||
'reservoir must refresh ~1s after being exhausted; "timed-out" means the Bottleneck ' +
|
||||
"heartbeat died after updateSettings() and the reservoir never refilled " +
|
||||
"(node_modules/bottleneck/lib/LocalDatastore.js _startHeartbeat clearInterval-without-null bug)"
|
||||
'capacity must recover after being exhausted; "timed-out" means the limiter stopped ' +
|
||||
"admitting work after the header-learned updateSettings() — the failure shape of the " +
|
||||
"original Bottleneck heartbeat bug (LocalDatastore _startHeartbeat clearInterval-without-null)"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -260,7 +260,10 @@ test("response.completed normalizes usage when lifecycle echoes are stripped", a
|
||||
|
||||
const completed = JSON.parse(completedLine.slice(5).trim());
|
||||
assert.equal("instructions" in completed.response, false);
|
||||
assert.equal("tools" in completed.response, false);
|
||||
// #8990 (commit c996dc93c2) deliberately stopped stripping `tools` from the
|
||||
// TERMINAL snapshot — Codex CLI rebuilds its tool list from response.completed.
|
||||
// stripResponsesLifecycleEcho still strips it on created/in_progress.
|
||||
assert.deepEqual(completed.response.tools, [{ type: "function", name: "echoed_tool" }]);
|
||||
assert.equal(completed.response.usage.total_tokens, 91);
|
||||
});
|
||||
|
||||
|
||||
@@ -582,7 +582,10 @@ test("OpenAI -> Antigravity wraps Gemini requests in a Cloud Code envelope", ()
|
||||
"model",
|
||||
"userAgent",
|
||||
"requestType",
|
||||
// #9568: identity entries are emitted too (Gemini lowercases tool names in responses).
|
||||
"_toolNameMap",
|
||||
]);
|
||||
assert.equal((result._toolNameMap as Map<string, string>).get("weather"), "weather");
|
||||
assert.equal(result.userAgent, "antigravity");
|
||||
assert.equal(result.requestType, "agent");
|
||||
assert.match(result.requestId, /^agent\/\d+\/[0-9a-f]{8}$/);
|
||||
|
||||
@@ -20,6 +20,11 @@ const { buildGeminiThoughtSignatureKey, storeGeminiThoughtSignature } =
|
||||
|
||||
type UnknownRecord = Record<string, unknown>;
|
||||
|
||||
/** Shape of the translated Gemini request the three finders below walk. */
|
||||
interface GeminiRequestLike {
|
||||
contents?: Array<{ parts?: UnknownRecord[] }>;
|
||||
}
|
||||
|
||||
const CLAUDE_SIGNATURE_NAMESPACE = "regression-3440";
|
||||
|
||||
function seedClaudeThoughtSignature() {
|
||||
@@ -29,7 +34,7 @@ function seedClaudeThoughtSignature() {
|
||||
);
|
||||
}
|
||||
|
||||
function findFunctionCall(result: any): UnknownRecord | undefined {
|
||||
function findFunctionCall(result: GeminiRequestLike): UnknownRecord | undefined {
|
||||
for (const content of result.contents ?? []) {
|
||||
for (const part of content.parts ?? []) {
|
||||
if (part?.functionCall) return part.functionCall as UnknownRecord;
|
||||
@@ -38,7 +43,7 @@ function findFunctionCall(result: any): UnknownRecord | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findFunctionCallPart(result: any): UnknownRecord | undefined {
|
||||
function findFunctionCallPart(result: GeminiRequestLike): UnknownRecord | undefined {
|
||||
for (const content of result.contents ?? []) {
|
||||
for (const part of content.parts ?? []) {
|
||||
if (part?.functionCall) return part as UnknownRecord;
|
||||
@@ -47,7 +52,7 @@ function findFunctionCallPart(result: any): UnknownRecord | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findFunctionResponse(result: any): UnknownRecord | undefined {
|
||||
function findFunctionResponse(result: GeminiRequestLike): UnknownRecord | undefined {
|
||||
for (const content of result.contents ?? []) {
|
||||
for (const part of content.parts ?? []) {
|
||||
if (part?.functionResponse) return part.functionResponse as UnknownRecord;
|
||||
|
||||
@@ -36,17 +36,21 @@ function imageBody() {
|
||||
}
|
||||
|
||||
describe("#7237 vision-capable models keep their images through compression", () => {
|
||||
it("documents the drift: the conservative id-fragment heuristic disagrees with the authoritative spec for gpt-5.5", () => {
|
||||
assert.equal(
|
||||
isVisionModelId("gpt-5.5"),
|
||||
false,
|
||||
"the fragment-list heuristic has no gpt-5.x entry — it is a deliberately conservative fallback, not the source of truth"
|
||||
);
|
||||
it("the id-fragment heuristic now agrees with the authoritative spec for gpt-5.5", () => {
|
||||
// Originally this case documented a DRIFT: the fragment list had no gpt-5.x
|
||||
// entry, so the heuristic said false while modelSpecs said true. Commit
|
||||
// 68cb678780 (enable vision flags for CC models) added the "gpt-5" fragment
|
||||
// and closed that gap, so the two sources now converge. The invariant worth
|
||||
// guarding is the AGREEMENT plus the fact that modelSpecs stays authoritative.
|
||||
assert.equal(isVisionModelId("gpt-5.5"), true);
|
||||
assert.equal(
|
||||
getResolvedModelCapabilities({ model: "gpt-5.5" }).supportsVision,
|
||||
true,
|
||||
"modelSpecs.ts registers gpt-5.5 with supportsVision:true — this is the authoritative source chatCore must use"
|
||||
);
|
||||
// The heuristic stays deliberately conservative for ids it does not know;
|
||||
// chatCore must still read the authoritative capability, never this fallback.
|
||||
assert.equal(isVisionModelId("some-unknown-text-only-model"), false);
|
||||
});
|
||||
|
||||
it("replaceImageUrls preserves the image when fed the authoritative capability (the fixed chatCore.ts:1330 behavior)", () => {
|
||||
@@ -59,13 +63,16 @@ describe("#7237 vision-capable models keep their images through compression", ()
|
||||
assert.equal(content[0].type, "image_url", "the block must remain a real image_url block");
|
||||
});
|
||||
|
||||
it("regresses the pre-fix bug: feeding the raw heuristic value strips the image for gpt-5.5", () => {
|
||||
const buggyValue = isVisionModelId("gpt-5.5"); // false — the pre-fix chatCore.ts:1330 input
|
||||
const result = replaceImageUrls(imageBody(), { supportsVision: buggyValue });
|
||||
it("reproduces the bug SHAPE: a false supportsVision strips the image to a placeholder", () => {
|
||||
// The original case derived the wrong value from isVisionModelId("gpt-5.5").
|
||||
// That no longer returns false (68cb678780), so the false is now supplied
|
||||
// directly — what this guards is the stripping behaviour itself, which is
|
||||
// exactly why chatCore must pass the authoritative capability and not a guess.
|
||||
const result = replaceImageUrls(imageBody(), { supportsVision: false });
|
||||
assert.equal(
|
||||
result.applied,
|
||||
true,
|
||||
"sanity check: this reproduces the bug shape when fed the wrong (heuristic) value"
|
||||
"sanity check: this reproduces the bug shape when fed the wrong value"
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ export default defineConfig({
|
||||
"open-sse/services/autoCombo/__tests__/**/*.test.ts",
|
||||
"open-sse/services/combo/__tests__/**/*.test.ts",
|
||||
"open-sse/services/__tests__/antigravity-quota-family.test.ts",
|
||||
// #8890 shipped this suite into a directory no runner collects, so it had
|
||||
// never executed once (check:test-discovery flags it as a NEW orphan).
|
||||
"open-sse/services/__tests__/fail-fast-concurrency-gate.test.ts",
|
||||
"src/lib/memory/__tests__/generic-backend.test.ts",
|
||||
"tests/unit/autoCombo/**/*.test.ts",
|
||||
"tests/unit/encryption.spec.ts",
|
||||
|
||||
Reference in New Issue
Block a user