Release v3.8.36 (#4854)

* chore(release): open v3.8.36 development cycle

* refactor(chatCore): extrai resolveCompressionSettings (#3501) (#4826)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 1/13)

* refactor(chatCore): extrai predicados puros de combo de compressão (#3501) (#4824)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 2/13)

* refactor(chatCore): extrai emitOutputStyleTelemetry (#3501) (#4811)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 3/13)

* refactor(chatCore): extrai writeCompressionAnalytics (bloco analytics completo, #3501) (#4817)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 4/13)

* refactor(chatCore): extrai runPluginOnRequestHook (#3501) (#4827)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 5/13)

* refactor(chatCore): extrai applyClientUsageBuffer (buffer/estimate de usage non-streaming, #3501) (#4832)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 6/13)

* refactor(chatCore): extrai buildPostCallGuardrailContext (contexto guardrail post-call, #3501) (#4831)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 7/13)

* refactor(chatCore): extrai storeSemanticCacheResponse (cache-store non-streaming, #3501) (#4828)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 8/13)

* refactor(chatCore): extrai buildNonStreamingResponseHeaders (headers de resposta non-streaming, #3501) (#4835)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 9/13)

* refactor(chatCore): extrai maybeConvertJsonBodyToSse (#3089 JSON→SSE streaming, #3501) (#4833)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 10/13)

* refactor(chatCore): extrai assembleStreamingResponseHeaders (headers de resposta streaming, #3501) (#4836)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 11/13)

* refactor(chatCore): extrai storeStreamingSemanticCacheResponse (cache-store streaming, #3501) (#4829)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 12/13)

* refactor(chatCore): extrai assembleStreamingPipeline (chain de transforms streaming, #3501) (#4837)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 13/13)

* ci(quality): shift heavy validations to the PR→release fast-path (release-acceleration) (#4857)

* feat(quality): add check:test-runner-api gate (vitest-only dirs must use vitest API)

* feat(release): reusable CHANGELOG i18n-mirror sync script

* chore(ops): add prune-stale-worktrees.sh (dry-run by default)

* ci(quality): run test-runner-api + docs-all + vitest + full unit suite on PR->release fast-path

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* fix(quota): cota exclusiva lista qtSd/ no /v1/models (#4806) + limite EPSILON não bloqueia (#4830)

Integrated into release/v3.8.36 — quota-exclusive qtSd/ listing (#4806) + EPSILON placeholder no longer blocks; rebuilt from stale base (3 defining commits cherry-picked clean over release tip)

* feat(sse): add Google Flow video-generation provider (#4569) (#4769)

Integrated into release/v3.8.36 — Google Flow video-generation provider (#4569), release-green validated (typecheck + 21 tests + file-size)

* fix(api): auth on compression run-telemetry + document OMNIROUTE_EVAL_CREDENTIALS (#4694, #4720) (#4796)

Integrated into release/v3.8.36 — auth on compression run-telemetry + OMNIROUTE_EVAL_CREDENTIALS doc, release-green validated (typecheck + 3 tests + env-doc-sync)

* fix(translator): strip top-level client_metadata on the OpenAI passthrough (port from 9router#1157) (#4624)

Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated)

* fix(translator): normalize `developer` role to `system` for OpenAI-format providers (#4625)

Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated)

* fix(translator): emit </think> close marker for Anthropic thinking blocks (#4633)

Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated)

* fix(translator): normalize tools to Anthropic-native shape for non-Anthropic providers (#4650)

Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated)

* fix(gemini): preserve `pattern` in antigravity tool schema sanitizer (#4651)

Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated)

* fix(perplexity): validate API keys via /v1/models endpoint (#4654)

Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated)

* fix(image): prevent compatible nodes from shadowing provider aliases (#4656)

Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated)

* fix(cli-tools): tolerate JSONC (comments, trailing commas) in tool settings (#4659)

Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated)

* fix(security): validate kiro region to prevent SSRF (GHSA-6mwv-4mrm-5p3m) (#4629)

Integrated into release/v3.8.36 — kiro region SSRF guard (GHSA-6mwv-4mrm-5p3m), port rebuilt clean over release tip

* fix(cli): harden the systray2 tray runtime (port of 9router#1080) (#4628)

Integrated into release/v3.8.36 — port rebuilt clean over release tip, release-green validated

* fix(test): validate anthropic-compatible connections via POST /v1/messages (#4657)

Integrated into release/v3.8.36 — anthropic-compat validation via POST /v1/messages (port 584cf66a), rebuilt clean + baseline; release-green

* fix(executors): strip params unsupported by the target provider/model (#4658)

Integrated into release/v3.8.36 — port rebuilt clean over release tip, release-green validated

* fix(claude-oauth): respect 429 backoff on usage endpoint to reduce spam (#4655)

Integrated into release/v3.8.36 — port rebuilt clean over release tip, release-green validated

* feat(api/v1): include alias-backed models in /v1/models listing (#4630)

Integrated into release/v3.8.36 — port rebuilt clean over release tip, release-green validated

* chore(quality): rebaseline catalog.ts 1574->1577 (#4630 aliases sobre quota-exclusive da release) (#4879)

rebaseline

* feat(compression): Kiro/CodeWhisperer tool-result compression engine (#4635)

Integrated into release/v3.8.36 — port rebuilt clean, release-green

* fix(security): don't trust loopback socket as local when behind reverse proxy (#4632)

Integrated into release/v3.8.36 — port rebuilt clean, release-green

* fix(opencode): preserve DeepSeek reasoning content in streamed responses (#4631)

Integrated into release/v3.8.36 — DeepSeek reasoning_content injection (port #1099); release-green

* fix(copilot,antigravity): cap maxOutputTokens at 16384 to stop "Invalid Argument" 400 (#4636)

Integrated into release/v3.8.36 — cap maxOutputTokens 16384 antigravity (port #779); release-green

* fix(dashboard): show custom vision models in LLM selector (#4653)

Integrated into release/v3.8.36 — custom vision models in LLM selector (port 5e5e78d3); release-green

* fix(claude): omit adaptive thinking + output_config.effort for haiku (#4661)

Integrated into release/v3.8.36 — haiku adaptive-thinking omit (port); release-green

* feat(provider): CodeBuddy CN (copilot.tencent.com) — full stack (#4664)

Integrated into release/v3.8.36 — CodeBuddy CN provider (port efd20be8); usage.ts import + public-creds allowlist line reconciled; release-green

* feat(combo): Fusion strategy — parallel panel + judge synthesis (16th strategy) (#4652)

Integrated into release/v3.8.36 — Fusion combo strategy (16th, port 87e5c1c6); combo.ts baseline reconciled; release-green

* feat(proxy-pool): Deno Deploy relays + group action buttons (#4643)

Integrated into release/v3.8.36 — Deno Deploy relays (port #1437); proxies.ts baseline reconciled + env docs restored; release-green

* fix(security): pin image fetch DNS resolution to prevent SSRF rebinding (GHSA-cmhj-wh2f-9cgx) (#4634)

Integrated into release/v3.8.36 — pin DNS for image fetch SSRF rebinding guard (GHSA-cmhj-wh2f-9cgx, port c7d07448); caller DNS stubs + test-file baseline reconciled; release-green

* fix(github): route Copilot Codex models to /responses (port from 9router#102) (#4626)

Integrated into release/v3.8.36 — route Copilot Codex models to /responses (port #102); release-green

* fix(copilot): never route Gemini/Claude variants to /responses (chat-completions only) (#4627)

Integrated into release/v3.8.36 — never route Gemini/Claude to /responses (port #1536); fused with #4626 codex routing via supportsResponsesEndpoint gate; release-green

* docs(ops): add canonical incident response runbook (#4868)

Integrated into release/v3.8.36

* docs(perf): add per-endpoint p50/p95/p99 latency + cost budgets (#4867)

Integrated into release/v3.8.36

* fix(proxy): fan out direct dispatcher streams (#4803)

Integrated into release/v3.8.36

* fix(antigravity): exclude standard Gemini rate limit message from quota exhaustion keywords (#4810)

Integrated into release/v3.8.36

* fix(sse): skip third-party tool-name cloak for Anthropic server tools (#4808)

Integrated into release/v3.8.36

* fix(install): make transformers optional for CUDA-host installs (#4807)

Integrated into release/v3.8.36

* fix(combo): propagate selected connection ID to fallback error responses for correct model lockout (#4809)

Integrated into release/v3.8.36

* fix db storage tuning settings (#4834)

Integrated into release/v3.8.36

* fix(sse): drop ccp pin when pinned provider is durably unhealthy (failover + anti-flap) (#4864)

Integrated into release/v3.8.36

* fix(claude): skip mcp__ tool-name cloak + guard missing connectionId (#4861)

Integrated into release/v3.8.36

* chore(quality): reconcile env-doc + file-size base-reds in release/v3.8.36 (#4886)

- env-doc-sync: document PIN_DROP_BACKOFF_LEVEL / PIN_DROP_GRACE_MS (added by the
  ccp-pin health gate #4864) in .env.example + ENVIRONMENT.md.
- file-size: rebaseline image-generation-handler.test.ts 1996 -> 2019 to its actual
  size (pre-existing drift).

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* fix(codex): drop non-standard codex.* events that break responses.stream (env-gated, #4602) (#4715)

Integrated into release/v3.8.36

* feat(routing): honor X-Route-Model header to override body.model (#4863)

Integrated into release/v3.8.36

* feat(live-ws): allow non-loopback clients via LIVE_WS_ALLOWED_HOSTS (closes #4873) (#4877)

Integrated into release/v3.8.36 (live-ws + combo-api commits; Tailscale CGNAT commit held pending opt-in/opt-out decision)

* chore(claude,codex): bump pinned CLI identity — Claude 2.1.158→2.1.187, Codex 0.132.0→0.142.0 (#4883)

Integrated into release/v3.8.36

* fix(security): SSRF allowlist bypass via x-relay-path nos relays Deno/Vercel (#4899)

Integrated into release/v3.8.36

* feat(quota): recuperação proativa de conexões em cooldown (cron heal) [Fase 3 #8] (#4900)

Integrated into release/v3.8.36

* fix(quota): policy inválida não vaza allow + guard connectionIds vazio [Fase 3 #10] (#4901)

Integrated into release/v3.8.36

* feat(quota): saturação real do Claude no fair-share via /api/oauth/usage (#4885)

Integrated into release/v3.8.36

* chore(dashboard): rename Qoder display label from "Qoder AI" to "Qoder" (#4733)

Integrated into release/v3.8.36

* fix(ci): include coverage/lcov.info in coverage-report artifact for SonarQube (#4670)

Integrated into release/v3.8.36

* fix(cli): bump better-sqlite3 runtime pin to 12.10.1 for Node 26 (#4685)

Integrated into release/v3.8.36

* docs: clarify Kiro is ~50 credits/month per account, not unlimited (#4690)

Integrated into release/v3.8.36

* docs(agentbridge): document Electron NODE_EXTRA_CA_CERTS, real model IDs, identity caveat (#4718)

Integrated into release/v3.8.36

* docs(ops): document the release-green family (green-prs, check:release-green, babysit, nightly) (#4679)

Integrated into release/v3.8.36

* fix(translator): replay reasoning_content on plain Xiaomi MiMo turns (port from 9router#1321) (#4639)

Integrated into release/v3.8.36

* feat(opencode-go): advertise glm-5.2 and kimi-k2.7-code (align with official Go endpoints) (#4711)

Integrated into release/v3.8.36

* feat(db): track API endpoint dimension on usage_history (#4676)

Integrated into release/v3.8.36 (migration renumbered 103→105; endpoint plumbed through extracted usage-stats helpers)

* fix(cli): SIGKILL systray child PID before IPC close to avoid macOS NSStatusItem orphan (#4732)

Integrated into release/v3.8.36

* feat(proxy-pool): Cloudflare Workers proxy deployer + pool integration (#4640)

Integrated into release/v3.8.36 (relay type added to RELAY_TYPES set; dropdown UX preserved + Cloudflare item added; proxies.ts file-size rebaselined 1057→1060)

* chore(quality): conserta base-red de release/v3.8.36 (gates + 7 testes + build MDX) (#4915)

A base tinha base-red sistêmica herdada de PRs de outras sessões, bloqueando
TODOS os PRs do ciclo (o TIA roda a suíte full em fail-safe p/ diffs hub).

4 Fast Quality Gates:
- test-discovery (#4877): live-server-allowlist.test.ts em tests/unit/server/
  (não-coletado) + vitest → nunca rodava. Convertido p/ node:test em tests/unit/security/.
- any-budget:t11 (#4664): 3 explicit-any em tokenRefresh.ts tipados (sem crescer file-size).
- docs-symbols (#4868): rotas inexistentes → /api/system/version e
  PUT /api/providers/{id} {isActive:false}.
- docs-all fabricated-claim (#4868 + #4718): 5 bin/*.sh reais criados (rollback,
  snapshot-data, restore-data, restore-policies, cold-start-bench) + _ops-common.sh
  (snapshot VACUUM INTO, guards de confirmação/TTY, testes de contrato); NODE_EXTRA_CA_CERTS
  (env de runtime Node) na allowlist do checker.

7 testes unit base-red (de features alheias à quota):
- oauth-providers-config (#4664): teste alinhado ao provider codebuddy-cn do registry.
- antigravity-model-aliases (#4636): maxOutputTokens esperado 32769→16384 (cap intencional).
- provider-request-capture #4091 (#4861): exemplo do teste trocado de mcp__ (que #4861
  isenta de cloak por causa dos 400s de assimetria de histórico) para um tool de terceiro
  cloakável — preserva o invariante de #4091 SEM reverter #4861.
- combo-error-response: convertido de vitest p/ node:test (era coletado pelo glob node:test
  e crashava); api/** e server/** removidos do vitest.config (config morta).

Build MDX (dast-smoke, #4679):
- docs/ops/RELEASE_GREEN.md não tinha frontmatter `title` → fumadocs-mdx rejeitava no
  webpack compile ("invalid frontmatter: title expected string"), quebrando o next build
  (e o deploy). Frontmatter title adicionado (único doc do collection sem ele).

17/17 Fast Quality Gates + suíte unit completa (17737 testes, 0 fail) + vitest verdes localmente.

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* feat(quota): saturação proativa por headers de tokens (universal) [Fase 3 #2] (#4907)

storeRateLimitHeaders só capturava os headers de REQUESTS (RPM/min), que não
refletem a pressão de TOKENS. Agora também parseia os headers de tokens (em toda
resposta, sucesso também) para throttle proativo antes do 429:
- Anthropic: anthropic-ratelimit-tokens-{limit,remaining,reset} (+ input/output), RFC3339.
- OpenAI: x-ratelimit-{limit,remaining,reset}-tokens, reset em duração (6m0s).

saturation = 1 − remaining/limit; resetAt normalizado a epoch (parse de duração
ReDoS-safe). getTokenHeaderSaturation por (provider, connectionId). fetchGeneric-
Saturation passa a usar esse sinal (complementa o oauth/usage do #1, que segue
primário p/ Claude). Fail-open, cache mantido, request-path inalterado.

16 testes novos + regressão (oauth/usage #1 8/8, signals 6/6) = 30/30;
typecheck:core + eslint limpos.

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* feat(quota): estratégia de combo "headroom" — seleção por folga de cota [Fase 3 #4] (#4908)

Nova estratégia de roteamento que escolhe a conexão com MAIS folga de plano:
headroom = 1 − max(util_5h, util_7d) (técnica do dario), via getSaturation
(melhorado p/ Claude no #1). Proativo em vez de só fill-first reativo.

- Helper PURO headroomRanking.ts (computeHeadroom + rankByHeadroom; saturação
  injetada, não-mutante, tie-break estável, fail-open).
- Orderer async em combo/quotaStrategies.ts (reusa a maquinaria reset-aware de
  expansão de conexões + concorrência limitada; seam injetável).
- Registrada como "headroom" em routingStrategies (combo-only); fill-first segue
  default — nenhuma estratégia existente tocada.
- baseline file-size combo.ts 3168->3180 (só +12L de dispatch; lógica fora do god-file).

16 testes novos + combo-strategies 15/15 = 31/31; typecheck:core + eslint +
file-size limpos.

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* feat(quota): cap per-(key,model) — quota_allocation_model_caps [Fase 3 #7] (#4927)

* feat(quota): cap per-(key,model) com tabela quota_allocation_model_caps [Fase 3 #7]

Fecha o buraco onde uma API key pode drenar o pool inteiro consumindo um único modelo.

Tabela nova: quota_allocation_model_caps(pool_id, api_key_id, model, cap_value, cap_unit)
PK composta (pool_id, api_key_id, model). cap_unit alinhado ao QuotaUnit existente.

Comportamento: keyA acima do cap para modelo M → bloqueada somente em M; ainda
permitida em qualquer outro modelo no mesmo pool. Cap <= EPSILON → ignorado (seed).

Consumo por-(key,model) usa bucket segregado no quota_consumption existente
(poolId mangled ':model:<model>') com window fixa 'hourly'; nenhuma nova tabela
ou método de store necessário.

Módulo novo: src/lib/db/quotaModelCaps.ts (getModelCap/setModelCap/deleteModelCap/listModelCaps)
enforce.ts ganha o pre-check em enforceQuotaShare + recording em recordConsumption.
EnforceInput e RecordConsumptionInput ganham model?: string (backward-compatible).
localDb.ts re-exporta os 4 helpers (Hard Rule #2).

TDD: tests/unit/quota-per-key-model.test.ts — 4 cenários (bloqueia em M, permite em M2,
sem cap → sem bloqueio, EPSILON → ignorado). Todos os gates de qualidade passam.

* feat(quota): plumba model resolvido no hot path para ativar o per-(key,model) cap [Fase 3 #7]

A tabela/enforce do commit anterior estavam INERTES: o hot path não passava `model`
ao enforce nem ao record, então nenhum model-cap disparava em produção.

Plumbagem (model resolvido = mesma var usada no log/roteamento, pós background-redirect/alias):
- chatCore.ts: enforceQuotaShare ganha `model`; scheduleQuotaShareConsumption recebe `model`.
- chatCore/quotaShareConsumption.ts: threade `model` no RecordConsumptionInput (non-streaming).
- spendRecorder.ts: recordStreamingConsumption já recebia `model` — agora o coloca no
  RecordConsumptionInput (streaming accrue por-modelo).
- embeddings.ts: enforce + record ganham `model`.

Namespace do cap = id do modelo RESOLVIDO (o mesmo de modelForScope/pendingScope/getUnsupportedParams),
não o requestedModel cru nem o finalModelToUpstream (sem prefixo de provider). Operador configura
o cap contra esse id. `model || undefined` em todos os pontos: vazio/null → check pulado (fail-safe,
zero latência — só um campo no objeto).

Teste de integração novo (tests/unit/quota-per-key-model-hotpath.test.ts): prova end-to-end que
N consumos via scheduleQuotaShareConsumption({model}) → enforceQuotaShare({model}) bloqueia, e que
outro modelo no mesmo pool ainda passa; + guard de que enforce SEM model nunca dispara model-cap.

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* feat(quota): session stickiness p/ integridade de prompt-cache [Fase 3 #5] (#4929)

* feat(quota): session stickiness p/ integridade de prompt-cache [Fase 3 #5]

Adiciona stickiness de sessão ao roteamento de combo: uma conversa multi-turno
é roteada para a MESMA conexão enquanto ela permanecer saudável, evitando a
perda do prompt-cache do provider (custo 5-10× sem stickiness, efeito conhecido
no dario/clewdr).

Implementação:
- `open-sse/services/combo/sessionStickiness.ts` (novo, <800 linhas):
  mapa em memória (messageHash → connectionId) com TTL 15 min + cap 500 entradas;
  `applySessionStickiness` promove a conexão sticky ao índice 0 dos targets
  ordenados pelo strategy, guardado por `computeHeadroom > 0.15` (threshold);
  quando saturada (headroom ≤ 0.15), o binding é limpo e a seleção normal reage.
  Hash da sessão = SHA-256 dos primeiros chars da 1ª mensagem user → 16 hex chars.
  Seam de teste: `__setStickinessHeadroomFetcherForTests`.
- `open-sse/services/combo.ts`: import + 2 pontos de integração (pré-eval-scores
  e pós-success), dentro do orçamento congelado de 3180 linhas.
- `tests/unit/combo-session-stickiness.test.ts`: 19 testes node:test + assert/strict,
  todos via injeção de fetcher (zero rede/DB).

Threshold 0.15: conexão a >85% de utilização está a um burst de rate-limit;
o benefício de cache não compensa manter-se numa conexão degradada. Valor
alinhado com a zona de soft-penalty do restante do engine de quota-share.

* test(combo): isola combo-strategies da session stickiness (#5)

selectedConnectionFor reusa o mesmo body, então o sticky map (#5) fixava a
connection após a 1ª chamada e quebrava o round-robin tie-break do teste
reset-aware. Limpa o sticky map no início da helper — a stickiness tem suíte
própria (combo-session-stickiness). Sem enfraquecer asserts.

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* feat(quota): buckets multi-janela por conexão (5h/7d/per-model) [Fase 3 #3] (#4928)

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* refactor(providers): decompõe catálogo providers.ts em módulos de dados (godfile sweep, #3501) (#4917)

Integrado em release/v3.8.36 (godfile sweep providers.ts, #3501)

* refactor(pricing): decompõe pricing.ts em shared-tiers + DEFAULT_PRICING particionado (godfile sweep, #3501) (#4918)

Integrado em release/v3.8.36 (godfile sweep pricing.ts, #3501)

* refactor(api): extrai camada-folha pura de validation.ts (URL/headers/transport) (#4921)

Integrado em release/v3.8.36 (validation.ts split fatia 1 — leaf layer)

* refactor(api): extrai validators web-cookie + Meta AI de validation.ts (#4922)

Integrado em release/v3.8.36 (validation.ts split fatia 2 — web-cookie + Meta AI)

* refactor(api): extrai validators enterprise-cloud + probe compartilhado de validation.ts (#4923)

Integrado em release/v3.8.36 (validation.ts split fatia 3 — enterprise-cloud + probe)

* refactor(api): extrai validators áudio/speech + misc apikey de validation.ts (#4930)

Integrado em release/v3.8.36 (validation.ts split fatia 4 — áudio/speech + misc apikey)

* feat(quota): estratégia dedicada de quota-share (DRR + P2C in-flight + gating per-model) [Fase 3 #9] (#4939)

* feat(quota): estratégia dedicada de quota-share (DRR + P2C in-flight + gating per-model) [Fase 3 #9]

Estratégia interna "quota-share" isolada num módulo dedicado — NÃO toca a seleção/
fair-share genérica (decisão do dono: não mexer no que já funciona). Os combos qtSd/
(quotaCombos.ts) passam de fill-first para essa strategy; combo.ts ganha só 1 branch
de dispatch que delega 100% ao módulo (nenhum case existente alterado).

- quotaShareStrategy.ts: gating per-model (isBucketSaturated do #3) + DRR (quantum
  proporcional ao weight) + P2C sobre carga in-flight.
- quotaShareInflight.ts: contador in-flight com TTL/lease de 120s — fallback do
  decrement-on-abort sem precisar instrumentar o combo genérico.
- "quota-share" registrada como strategy INTERNA (não exposta na UI).
- testes de síntese (quota-combo-balancing, quota-multiprovider) alinhados: a strategy
  esperada dos combos qtSd/ passa de "fill-first" para "quota-share" (alinhamento ao novo
  comportamento intencional, não mascaramento — os 73 testes de qtSd/ seguem verdes).

* test(quota-share): alinha 2 scope-guards ao godfile sweep (base-reds que bloqueavam o CI)

Dois testes de "arquivo contém X" quebraram por decomposições de godfile que outras
sessões mergearam no release DURANTE a validação de #9 — NÃO são regressão de #9
(que não toca validation/oauth). Alinhados ao novo layout, asserts preservados:
- proxy-bypass-scope-guard #3226: bypassProxyPatch foi extraído de validation.ts para
  validation/headers.ts (split #4921–#4930) → o teste lê a camada de validação.
- sse-error-passthrough #3324: a windsurf authHint foi extraída de providers.ts para
  providers/oauth.ts → o teste lê o novo local.

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* refactor(api): extrai validators search + embedding/rerank de validation.ts (#4932)

Integrated into release/v3.8.36

* refactor(api): extrai format-validators (OpenAI/Anthropic) de validation.ts (#4933)

Integrated into release/v3.8.36

* refactor(db): extrai model-permission matching de db/apiKeys.ts (#4936)

Integrated into release/v3.8.36

* refactor(db): extrai row-parsers + tipos compartilhados de db/apiKeys.ts (#4943)

Integrated into release/v3.8.36

* refactor(db): extrai column-mapping (snake↔camel) de db/core.ts (#4947)

Integrated into release/v3.8.36

* refactor(db): extrai schema-column reconciliation de db/core.ts (#4948)

Integrated into release/v3.8.36

* refactor(sse): extrai scalar/format helpers de services/usage.ts (#4949)

Integrated into release/v3.8.36

* refactor(sse): extrai quota-core (UsageQuota + builders) de services/usage.ts (#4950)

Integrated into release/v3.8.36

* fix(translator): regroup parallel tool results adjacent to their assistant (#4714) (#4882)

Integrated into release/v3.8.36 (fixes #4714)

* fix(qoder): exchange PAT for jt-* job token before Cosy chat (#4683) (#4884)

Integrated into release/v3.8.36 (fixes #4683)

* refactor(sse): dedup fallback tool_call id helper (#4736)

Integrated into release/v3.8.36

* refactor(open-sse): extract safeParseJSON util, dedup tryParseJSON (#4735)

Integrated into release/v3.8.36

* fix(compression): eliminate ReDoS in math_inline preservation pattern (#4795) (#4838)

Integrated into release/v3.8.36 (fixes #4795)

* fix(combo): fetch models dynamically from custom provider endpoints (#4860)

Integrated into release/v3.8.36

* feat(providers): update volcengine-ark model list with DeepSeek V4 (#4905)

Integrated into release/v3.8.36

* fix(translator): provider thinking compatibility (DeepSeek/Gemini) (#4946)

Integrated into release/v3.8.36

* feat(combo): task-aware routing strategy (#4945)

Integrated into release/v3.8.36

* refactor(sse): extrai a família MiniMax de services/usage.ts (#4952)

Integrated into release/v3.8.36

* refactor(sse): extrai a família GLM de services/usage.ts (#4953)

Integrated into release/v3.8.36

* refactor(sse): extrai a família Antigravity de services/usage.ts (#4956)

Integrated into release/v3.8.36

* fix(dashboard): show custom provider given-name instead of internal id across dashboard pages (#4603) (#4960)

Integrated into release/v3.8.36 (fixes #4603)

* fix(api): evict stale in-memory rate-limit windows to stop slow heap leak (#4041) (#4957)

Integrated into release/v3.8.36 (fixes #4041)

* fix(api): parse /v1/responses body once instead of 3-4x on the hot path (#4041) (#4958)

Integrated into release/v3.8.36 (fixes #4041)

* fix(translator): preserve legitimate empty-string tool arguments in openai-to-claude streaming (#4951) (#4959)

Integrated into release/v3.8.36 (fixes #4951)

* chore(quality): reconcile file-size baseline for #4960 provider-display-name (#4961)

Integrated into release/v3.8.36

* fix(dashboard): restore home provider-topology card hidden by #4596 default (#4963)

Integrated into release/v3.8.36 — restores home topology card (#4596 regression)

* fix(build): drop @omniroute/open-sse from optimizePackageImports (build OOM) (#4968)

Integrated into release/v3.8.36 — fixes build OOM (optimizePackageImports open-sse)

* fix(quota): migração 107 ativa estratégia quota-share nos combos qtSd/ existentes [Fase 3 #9] (#4962)

Integrated into release/v3.8.36

* feat(quota): respeita max_concurrent por conexão no roteamento (#4965)

Integrated into release/v3.8.36

* feat(quota): combo quota-share espera cooldown curto e re-despacha (Variante A) (#4967)

Integrated into release/v3.8.36

* fix(quality): resolve base-reds da release — db-rules allowlist + task-aware router precedence (#4973)

Dois base-reds pré-existentes que reprovavam o CI da release v3.8.36 (Fast
Quality Gates + Unit Tests fast-path), independentes de qualquer feature em voo:

1. check:db-rules / allowlist: os módulos db-internal caseMapping (#4947) e
   schemaColumns (#4948), extraídos de db/core.ts e importados só por ele, não
   estavam em INTENTIONALLY_INTERNAL. Registrados na allowlist (correção
   canônica — são internos legítimos, não re-exportados pelo localDb).

2. auto-strategy honra LKGP/cost (combo-routing-engine.test.ts, 2 testes): o
   task-aware reordering (#4945, reorderByTaskWeight) roda para strategy "auto" e
   era aplicado DEPOIS do router explícito (selectWithStrategy: lkgp/cost),
   sobrescrevendo o orderedTargets[0] que o operador escolheu. Instrumentação
   provou: post-filter [0]=claude (LKGP) → post-task [0]=gpt-oss. Correção: quando
   o auto usa router explícito, preserva o [0] dele e deixa o task-aware refinar
   só a cauda de fallback. gpt-oss-120b PERMANECE tool-capable (não é mudança de
   catálogo; o model-capabilities-registry test segue verde).

Validado: 121 testes (combo-routing-engine + combo-task-aware + registry) verdes,
red-check confirmado, db-rules/file-size/typecheck/lint/prettier OK.

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* feat(quota): serializa concorrência por conexão no caminho quota-share (FASE 2.1) (#4970)

O gating de quota-share em selectQuotaShareTarget é fail-open: uma conexão
at-cap só é despriorizada, nunca bloqueada. Com 1 conexão por conta de
assinatura (caso comum), chamadas concorrentes ainda floodam a conta (→ 429 +
cooldown) — provado live na .15: 3 chamadas concorrentes com max_concurrent=1
despacharam todas em 94ms.

Adiciona um semáforo POR CONEXÃO em torno do dispatch quota-share: chamadas
excedentes esperam na fila em vez de floodar (key qsconn:<connectionId>, cap =
max_concurrent da conexão). Fail-open em fila saturada/timeout para nunca
piorar disponibilidade. Gated por strategy===quota-share + kill-switch
resilienceSettings.quotaShareConcurrencyLimit (default on; UI no ResilienceTab).

Lógica extraível isolada no leaf puro combo/quotaShareConcurrency.ts
(unit-testado: estabilidade da key, no-op sem cap, serialização real,
fail-open). Settings + schema + UI espelham comboCooldownWait.

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* docs(resilience): document Quota-Share Concurrency Control (max_concurrent + serialization + cooldown-wait) (#4980)

Documents the v3.8.36 quota-share concurrency layers in RESILIENCE_GUIDE.md:
per-connection max_concurrent cap, the quota-share request serialization semaphore
(FASE 2.1, qsconn:<connectionId>, fail-open, kill-switch), and the combo
cooldown-aware retry — so operators know how to cap a subscription account's
concurrency and why the routing gate alone cannot contain a single-connection flood.

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* fix(dashboard): proxy-pool success gating, sync timestamp, opt-in Redis (#4878) (#4988)

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* fix(sse): fail over on 400 responses carrying rate-limit text (#4976) (#4986)

* fix(sse): fail over on 400 responses carrying rate-limit text (#4976)

* chore(quality): rebaseline accountFallback.ts file-size for #4976 fix

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* fix(compression): stop RTK over-truncating file-read tool results (#4559) (#4987)

* fix(compression): stop RTK over-truncating file-read tool results (#4559)

* chore(quality): trim #4559 comment to keep rtk/index.ts within size cap

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* fix(sse): honor per-account proxies and fingerprint rotation in opencode executor (#4954) (#4989)

* fix(sse): honor per-account proxies and fingerprint rotation in opencode executor (#4954)

* chore(quality): rebaseline auth.ts file-size for #4954 (+39: synthetic no-auth providerSpecificData hydration of fingerprints/accountProxies; irreducible credential-path wiring, covered by opencode-proxy-rotation-4954.test.ts + 159 auth/noauth regression)

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* fix(sse): soft-penalize exhausted providers in auto-combo scoring (#4540) (#4990)

* fix(sse): soft-penalize exhausted providers in auto-combo scoring (#4540)

* chore(quality): document STATUS_SOFT_DEPRIORITIZE_FACTOR + rebaseline combo.ts for #4540

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* fix(dashboard): switch to visible filter after auto-hiding failed models in test-all (#4887) (#4991)

* fix(dashboard): switch to visible filter after auto-hiding failed models in OAuth provider test-all (#4887)

* test(dashboard): move #4887 test into tests/unit/ui so a CI runner collects it

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* fix(pollinations): only enable jsonMode when JSON output is requested (#3981) (#5009)

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* fix(antigravity): default safetySettings to all-OFF for parity with native Gemini paths (#5003) (#5008)

* fix(antigravity): default safetySettings to all-OFF for parity with native Gemini paths (#5003)

* docs(changelog): restore #3981 pollinations entry eaten by merge

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* fix(chatgpt-web): map advertised gpt-5.5/5.4-pro/5.2-pro slugs to prevent silent model substitution (#4665) (#5010)

* fix(chatgpt-web): map advertised gpt-5.5/5.4-pro/5.2-pro slugs to prevent silent model substitution (#4665)

MODEL_MAP was missing the advertised catalog ids gpt-5.5, gpt-5.5-pro,
gpt-5.4-pro and gpt-5.2-pro, so MODEL_MAP[model] ?? model sent the dot-form
id verbatim to the ChatGPT backend-api, which silently rejected it and served
the default Plus model. Map each to its dash-form slug. gpt-4-5 is already
dash-form and falls through correctly, so it is intentionally left unmapped.

Extends the executor MODEL_MAP test with the four ids and adds a drift guard
asserting every advertised dot-form catalog id reaches the backend in dash-form
(never verbatim), guarding future catalog<->map drift.

file-size: tests/unit/chatgpt-web.test.ts frozen baseline 2809->2855 (+46) for
the added test cases and drift-guard test; executor source unchanged in baseline.

* docs(changelog): restore #3981/#5003 entries eaten by merge

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* feat(combos): add editable per-combo description field persisted via /api/combos (#5005) (#5011)

* feat(combos): add editable per-combo description field persisted via /api/combos (#5005)

* docs(changelog): restore #3981/#5003/#4665 entries eaten by merge

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* Fix Ollama Cloud max reasoning effort (#4993)

Integrated into release/v3.8.36

* fix(copilot): replace execSync with execFile to prevent command injection (#5024)

Integrated into release/v3.8.36

* fix(plugin): auth.json dual-key fallback for auto-prefix migration (#5027)

Integrated into release/v3.8.36

* feat(endpoint): per-endpoint custom system prompt injection (#5022)

Integrated into release/v3.8.36

* fix(headroom): translate openai-responses input through OpenAI for compression (#5023)

Integrated into release/v3.8.36

* docs(changelog): add entries for #4993, #5024, #5027 (release notes credit)

* fix(api): stop /api/system/env/repair 500 on packaged install (#5006) (#5028)

* fix(api): stop /api/system/env/repair 500 on packaged install — lazy createRequire in sync-env.mjs (#5006)

scripts/dev/sync-env.mjs ran createRequire(import.meta.url) at module
top-level. When webpack bundles it into the standalone env-repair route,
import.meta.url is frozen to the build-machine path (file:///home/runner/...)
and createRequire throws during module evaluation, so the whole route
module fails to load and every GET returns HTTP 500 — breaking the
onboarding wizard on packaged/global installs.

- Move createRequire into the guarded better-sqlite3 block (only place
  that needs it); a bad import.meta.url now returns the safe default.
- resolveRootDir() falls back to process.cwd() when fileURLToPath throws.
- route.ts passes an explicit rootDir (process.cwd()) so the helper never
  derives the root from the frozen import.meta.url, matching the .env
  target used by createEnvBackup().
- Regression guard: assert sync-env.mjs has no top-level createRequire +
  getEnvSyncPlan(oauth) works with explicit rootDir without throwing.

* docs(changelog): restore #4993/#5023/#5024/#5027 + custom-system-prompt/headroom entries eaten by release merge

* chore(quality): rebaseline 3 inherited base-reds from release merge

Files NOT touched by this PR — grew on release/v3.8.36 via --admin merges and
inherited here through 'git merge origin/release':
- open-sse/executors/base.ts 1414->1416 (#4993 Ollama Cloud max-effort)
- src/lib/db/settings.ts 1149->1151 (#5023 custom system prompt)
- src/app/(dashboard)/.../endpoint/EndpointPageClient.tsx 2570->2612 (custom system prompt UI)

* chore(release): finalize v3.8.36 CHANGELOG + docs (2026-06-25)

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>
Co-authored-by: KooshaPari <42529354+KooshaPari@users.noreply.github.com>
Co-authored-by: Makcim Ivanov <makcimbx@gmail.com>
Co-authored-by: Chewji <126886556+Chewji9875@users.noreply.github.com>
Co-authored-by: Anton <39598727+NomenAK@users.noreply.github.com>
Co-authored-by: Demiurge The Single <megamen932@gmail.com>
Co-authored-by: Randi <55005611+rdself@users.noreply.github.com>
Co-authored-by: Éder Costa <eder.almeida.costa@gmail.com>
Co-authored-by: Jefferson Felizardo <jeffer1312@gmail.com>
Co-authored-by: Arthur Bodera <abodera@gmail.com>
Co-authored-by: Hamsa_M <116961508+hamsa0x7@users.noreply.github.com>
Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-25 13:17:40 -03:00
committed by GitHub
parent cadc3f10b7
commit a7ae9550bd
534 changed files with 68304 additions and 16622 deletions

View File

@@ -700,7 +700,7 @@ test("chat pipeline applies Codex CLI fingerprint to OAuth responses requests",
assert.equal(call.headers.Version, getCodexClientVersion());
assert.equal(call.headers["Openai-Beta"], "responses=experimental");
assert.equal(call.headers["X-Codex-Beta-Features"], "responses_websockets");
assert.equal(call.headers["User-Agent"], "codex-cli/0.132.0 (Windows 10.0.26200; x64)");
assert.equal(call.headers["User-Agent"], "codex-cli/0.142.0 (Windows 10.0.26200; x64)");
assert.equal(call.headers["x-codex-window-id"], "conv_codex_fingerprint:0");
assert.ok(call.headers["x-client-request-id"], "expected Codex request id header");
assert.ok(call.headers["x-codex-turn-metadata"], "expected Codex turn metadata header");

View File

@@ -0,0 +1,44 @@
import test from "node:test";
import assert from "node:assert/strict";
// #4976 — A 400 response whose body carries rate-limit semantics (e.g. MiMoCode's
// "Detected high-frequency non-compliant requests from you.") was misclassified as a
// non-fallbackable generic 400, so MiMo-auto combo never failed over and the raw
// failure surfaced to Cline as `[502]: fetch failed`. checkFallbackError must now
// detect rate-limit text on a 400 and treat it as fallback-worthy
// (RATE_LIMIT_EXCEEDED, connection-cooldown scope) WITHOUT regressing the #2101
// malformed-400 infinite-loop guard.
const { checkFallbackError } = await import("../../open-sse/services/accountFallback.ts");
const { RateLimitReason } = await import("../../open-sse/config/constants.ts");
test("#4976 400 with rate-limit text (MiMoCode) → fallback with RATE_LIMIT_EXCEEDED", () => {
const res = checkFallbackError(
400,
"Detected high-frequency non-compliant requests from you.",
0,
null,
"mimocode"
);
assert.equal(res.shouldFallback, true);
assert.equal(res.reason, RateLimitReason.RATE_LIMIT_EXCEEDED);
});
test("#4976 400 with Chinese rate-limit text → fallback with RATE_LIMIT_EXCEEDED", () => {
const res = checkFallbackError(400, "检测到您的请求频率过高,请稍后再试", 0, null, "mimocode");
assert.equal(res.shouldFallback, true);
assert.equal(res.reason, RateLimitReason.RATE_LIMIT_EXCEEDED);
});
test("#4976 regression: a generic non-rate-limit 400 still does NOT fall over (#2101 guard)", () => {
const res = checkFallbackError(400, "Invalid JSON: unexpected token at position 12");
assert.equal(res.shouldFallback, false);
});
test("#4976 regression: a malformed 400 stays MODEL_CAPACITY, not reclassified as rate-limit", () => {
// Malformed-request detection must win over the new rate-limit text check so the
// #2101 infinite-loop guard (zero-cooldown MODEL_CAPACITY) is preserved.
const res = checkFallbackError(400, "messages must alternate between user and assistant");
assert.equal(res.shouldFallback, true);
assert.equal(res.reason, RateLimitReason.MODEL_CAPACITY);
});

View File

@@ -0,0 +1,80 @@
// Regression test for the anthropic-compatible connection validator.
//
// Upstream fix: GET /models is not part of the Anthropic API spec; many
// compatible proxies either 404, 401, or 403 on /models even with a valid
// API key. The connection test must therefore not reject the credentials
// solely on a 401/403 from /models — it must fall back to POST /v1/messages
// (the canonical Anthropic auth probe) and treat any non-401/403 messages
// response as proof that the key was accepted.
//
// Ported from decolua/9router 584cf66a (Co-author: Rehan Choirul).
import test from "node:test";
import assert from "node:assert/strict";
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
const originalFetch = globalThis.fetch;
test.afterEach(() => {
globalThis.fetch = originalFetch;
});
test(
"anthropic-compatible validation falls back to /messages when /models returns 403",
async () => {
const calls: { url: string; method: string }[] = [];
globalThis.fetch = async (url: any, init: any = {}) => {
const u = String(url);
const method = String(init?.method || "GET").toUpperCase();
calls.push({ url: u, method });
if (u.endsWith("/models")) {
return new Response(JSON.stringify({ error: "forbidden on models" }), { status: 403 });
}
// /messages: upstream accepts the key but rejects the toy payload with 400.
return new Response(JSON.stringify({ error: "bad request" }), { status: 400 });
};
const result = await validateProviderApiKey({
provider: "anthropic-compatible-403-on-models",
apiKey: "sk-test",
providerSpecificData: { baseUrl: "https://proxy.example.com/v1/messages" },
});
// BEFORE the fix this returned { valid: false, error: "Invalid API key" }
// because validateAnthropicCompatibleProvider short-circuited on the 403
// from GET /models without ever probing POST /v1/messages.
assert.equal(result.valid, true, "403 on /models alone must NOT mark the key invalid");
assert.equal(result.error, null);
// The validator must actually exercise the messages endpoint.
const messagesCall = calls.find(
(call) => call.url.endsWith("/messages") && call.method === "POST"
);
assert.ok(messagesCall, "expected a POST /messages probe after /models 403");
}
);
test(
"anthropic-compatible validation still rejects when /messages itself returns 401",
async () => {
// Symmetry guard: the fix must NOT make every 403/401 pass. Only the
// messages probe is authoritative — if it also rejects auth, the key is bad.
globalThis.fetch = async (url: any) => {
const u = String(url);
if (u.endsWith("/models")) {
return new Response(JSON.stringify({ error: "no models endpoint" }), { status: 403 });
}
return new Response(JSON.stringify({ error: "invalid_api_key" }), { status: 401 });
};
const result = await validateProviderApiKey({
provider: "anthropic-compatible-truly-bad-key",
apiKey: "sk-bad",
providerSpecificData: { baseUrl: "https://proxy.example.com/v1/messages" },
});
assert.equal(result.valid, false);
assert.equal(result.error, "Invalid API key");
}
);

View File

@@ -0,0 +1,109 @@
// Regression test for the Anthropic-compatible stream "thinking block never closes" bug.
//
// When clients consume the OpenAI-compatible stream that OmniRoute synthesises from
// Claude-native SSE, they need an explicit signal that the thinking/reasoning section
// has ended; otherwise the UI stays stuck on the "thinking" indicator even after the
// upstream stream has cleanly completed.
//
// Inspired by upstream decolua/9router PR #454.
//
// Before the fix, the `content_block_stop` event for a thinking block emitted NO
// terminating chunk at all (a previous drift had emitted `reasoning_content: ""`,
// which is semantically a no-op and does not signal "thinking complete" to clients
// such as Claude Code).
//
// The fix emits a `content: "</think>"` chunk on close — matching the convention
// already used throughout OmniRoute (see openai-responses.ts / responsesTransformer.ts
// which split on `</think>` to separate reasoning from final content).
import test from "node:test";
import assert from "node:assert/strict";
const { claudeToOpenAIResponse } = await import(
"../../open-sse/translator/response/claude-to-openai.ts"
);
function newState() {
return {
toolCalls: new Map(),
toolNameMap: new Map(),
messageId: "msg_test",
model: "claude-3-7-sonnet",
toolCallIndex: 0,
};
}
test("claudeToOpenAIResponse emits </think> close marker on thinking content_block_stop", () => {
const state = newState();
// Open thinking block.
claudeToOpenAIResponse(
{
type: "content_block_start",
index: 0,
content_block: { type: "thinking", thinking: "" },
},
state
);
// Stream reasoning delta.
claudeToOpenAIResponse(
{
type: "content_block_delta",
index: 0,
delta: { type: "thinking_delta", thinking: "Plan first." },
},
state
);
// Close the thinking block.
const closeChunks = claudeToOpenAIResponse(
{ type: "content_block_stop", index: 0 },
state
);
assert.ok(Array.isArray(closeChunks), "stop event must return an array of chunks");
assert.ok(
closeChunks.length >= 1,
"stop event for thinking block must emit at least one close-marker chunk"
);
const hasCloseMarker = closeChunks.some(
(chunk) => chunk?.choices?.[0]?.delta?.content === "</think>"
);
assert.ok(
hasCloseMarker,
`expected a chunk with delta.content === "</think>"; got ${JSON.stringify(closeChunks)}`
);
// After close, state flag must be cleared so subsequent thinking blocks are tracked correctly.
assert.equal(state.inThinkingBlock, false);
});
test("claudeToOpenAIResponse does not emit </think> on stop of non-thinking blocks", () => {
const state = newState();
// Open + immediately close a text block — must NOT inject </think>.
claudeToOpenAIResponse(
{
type: "content_block_start",
index: 0,
content_block: { type: "text", text: "" },
},
state
);
const closeChunks = claudeToOpenAIResponse(
{ type: "content_block_stop", index: 0 },
state
);
const arr = Array.isArray(closeChunks) ? closeChunks : [];
const hasCloseMarker = arr.some(
(chunk) => chunk?.choices?.[0]?.delta?.content === "</think>"
);
assert.equal(
hasCloseMarker,
false,
"text-block close must not emit </think> sentinel"
);
});

View File

@@ -55,6 +55,17 @@ test("classify429: AG 'Individual quota reached' message → quota_exhausted", (
assert.equal(classify429(msg), "quota_exhausted");
});
test("classify429: standard Gemini rate limit 'resource has been exhausted' -> rate_limited or unknown, not quota_exhausted", () => {
const msg =
"RESOURCE_EXHAUSTED: Resource has been exhausted (e.g. queries per minute limit was reached).";
const result = classify429(msg);
assert.notEqual(
result,
"quota_exhausted",
"RESOURCE_EXHAUSTED rate limit should not be classified as quota_exhausted"
);
});
// ── DB persistence (the missing wire — Bug #2) ───────────────────────────────
test("markConnectionQuotaExhausted persists 24h cooldown; isConnectionRateLimited returns true", async () => {

View File

@@ -20,7 +20,10 @@ function getPublicModel(id: string) {
// source of truth here (was duplicated as an inline if-ladder in usage.ts). It operates on
// the UPSTREAM quota namespace, where `gemini-3.5-flash-low` is the Medium tier's bucket.
test("toClientAntigravityQuotaModelId maps upstream quota buckets to client tiers", () => {
assert.equal(toClientAntigravityQuotaModelId("gemini-3.5-flash-extra-low"), "gemini-3.5-flash-low");
assert.equal(
toClientAntigravityQuotaModelId("gemini-3.5-flash-extra-low"),
"gemini-3.5-flash-low"
);
// Dual-meaning id: in the quota namespace this bucket is the Medium tier.
assert.equal(toClientAntigravityQuotaModelId("gemini-3.5-flash-low"), "gemini-3.5-flash-medium");
assert.equal(toClientAntigravityQuotaModelId("gemini-3-flash-agent"), "gemini-3.5-flash-high");
@@ -189,9 +192,7 @@ test("AntigravityExecutor.transformRequest maps Gemini 3.5 Flash tiers to live u
// executor never sets those fields, so they were vacuously true and gave false
// confidence. #3821-review LEDGER-10.)
assert.equal(result.model, "gemini-3-flash-agent");
assert.deepEqual(result.request.contents, [
{ role: "user", parts: [{ text: "Hello" }] },
]);
assert.deepEqual(result.request.contents, [{ role: "user", parts: [{ text: "Hello" }] }]);
});
test("AntigravityExecutor.transformRequest sends Claude through Gemini-compatible Cloud Code schema", async () => {
@@ -220,7 +221,10 @@ test("AntigravityExecutor.transformRequest sends Claude through Gemini-compatibl
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
const request = result.request as any;
assert.deepEqual(request.contents, [{ role: "user", parts: [{ text: "Hello" }] }]);
assert.equal(request.generationConfig.maxOutputTokens, 32769);
// Capped to MAX_ANTIGRAVITY_OUTPUT_TOKENS (16384) by the executor (#4636) to avoid
// the Antigravity Cloud Code 400 on maxOutputTokens > 16384, overriding the
// thinkingBudget+1 bump (which would otherwise be 32769).
assert.equal(request.generationConfig.maxOutputTokens, 16384);
assert.equal(request.generationConfig.temperature, 0.5);
assert.equal(request.generationConfig.topK, 40);
assert.equal(request.generationConfig.topP, 1);

View File

@@ -0,0 +1,60 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { AntigravityExecutor } from "../../open-sse/executors/antigravity.ts";
import { DEFAULT_SAFETY_SETTINGS } from "../../open-sse/translator/helpers/geminiHelper.ts";
// Regression for #5003: the Antigravity (Google Cloud Code) request builder explicitly set
// `safetySettings: undefined`, which `JSON.stringify` drops entirely. With no safetySettings
// reaching Cloud Code, Google applies its server-side safety defaults that false-flag benign
// technical prompts as `prohibited_content` (HTTP 200 with a blocked body that combo failover
// treats as terminal). The native Gemini paths all default to all-OFF
// (DEFAULT_SAFETY_SETTINGS); Antigravity must match for parity.
test("transformRequest defaults safetySettings to all-OFF when none supplied (#5003)", async () => {
const executor = new AntigravityExecutor();
const body = {
request: {
contents: [{ role: "user", parts: [{ text: "hi" }] }],
generationConfig: {},
},
};
const result = await executor.transformRequest("antigravity/claude-sonnet-4-6", body, true, {
projectId: "project-1",
});
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
const innerRequest = result.request as Record<string, unknown>;
assert.deepEqual(
innerRequest.safetySettings,
DEFAULT_SAFETY_SETTINGS,
"safetySettings must default to all-OFF for parity with native Gemini paths"
);
});
test("transformRequest honors a caller-supplied safetySettings (#5003)", async () => {
const executor = new AntigravityExecutor();
const callerSafety = [
{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_MEDIUM_AND_ABOVE" },
];
const body = {
request: {
contents: [{ role: "user", parts: [{ text: "hi" }] }],
generationConfig: {},
safetySettings: callerSafety,
},
};
const result = await executor.transformRequest("antigravity/claude-sonnet-4-6", body, true, {
projectId: "project-1",
});
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
const innerRequest = result.request as Record<string, unknown>;
assert.deepEqual(
innerRequest.safetySettings,
callerSafety,
"a caller-supplied safetySettings must not be clobbered"
);
});

View File

@@ -0,0 +1,116 @@
/**
* Unit tests for the standardized combo error response helper.
*
* Bug #3 from `plans/2026-06-23-omniroute-v3.8.34-deep-audit.md`:
* every 4xx response from `/api/combos/{id}` must include a stable
* machine-readable `code` token, an `error.message`, optional
* `error.details`, and `requestId` correlation. These tests assert the
* shape and HTTP status for every branch the route uses.
*
* Runner: node:test (the runner that collects tests/unit/api/**), not vitest —
* the original vitest import crashed under the node:test runner that the
* package.json glob also feeds it to (check:test-discovery).
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { buildComboErrorBody, comboErrorResponse } from "@/lib/api/comboErrorResponse";
import { ERROR_CODES } from "@/shared/constants/errorCodes";
describe("buildComboErrorBody", () => {
it("emits the canonical { error: { code, message, category } } envelope", () => {
const body = buildComboErrorBody("COMBO_001");
assert.equal(body.error.code, "COMBO_001");
assert.equal(body.error.message, "Request body is not valid JSON");
assert.equal(body.error.category, "COMBO");
});
it("includes details when provided", () => {
const body = buildComboErrorBody("COMBO_002", {
issues: [{ path: ["name"], message: "Required" }],
});
assert.equal(body.error.code, "COMBO_002");
assert.deepEqual(body.error.details, {
issues: [{ path: ["name"], message: "Required" }],
});
});
it("omits details when undefined", () => {
const body = buildComboErrorBody("COMBO_007");
assert.ok(!("details" in body.error));
});
it("falls back to INTERNAL_001 for an unknown code", () => {
const body = buildComboErrorBody(
"COMBO_999" as unknown as Parameters<typeof buildComboErrorBody>[0]
);
// An unknown code falls through to INTERNAL_001 from the catalog.
assert.equal(body.error.code, "INTERNAL_001");
});
});
describe("comboErrorResponse", () => {
it("returns the catalog httpStatus when no override is given", async () => {
const res = comboErrorResponse("COMBO_001");
assert.equal(res.status, ERROR_CODES.COMBO_001.httpStatus);
assert.equal(res.status, 400);
const body = await res.json();
assert.equal(body.error.code, "COMBO_001");
});
it("respects an explicit status override", async () => {
const res = comboErrorResponse("COMBO_006", 409, { name: "qs:foo" });
assert.equal(res.status, 409);
const body = await res.json();
assert.equal(body.error.code, "COMBO_006");
assert.deepEqual(body.error.details, { name: "qs:foo" });
});
it("attaches x-request-id when a Request is supplied", async () => {
const req = new Request("https://example.com/api/combos/abc", {
headers: { "x-request-id": "test-corr-id-1234" },
});
const res = comboErrorResponse("COMBO_004", 400, undefined, req);
assert.ok(res.headers.get("x-request-id"));
});
it("does NOT leak internal combo names in DAG errors (sanitized reason tag)", async () => {
// The route should translate a thrown `Error("cycle detected: combo-A")`
// into `{ reason: "cycle-detected" }` — never the raw message.
const reason = /cycle/i.test("cycle detected: combo-A") ? "cycle-detected" : "invalid-graph";
const res = comboErrorResponse("COMBO_005", 400, {
comboName: "user-facing-only",
reason,
});
assert.equal(res.status, 400);
const body = await res.json();
assert.equal(body.error.code, "COMBO_005");
assert.equal(body.error.details.reason, "cycle-detected");
// Crucial: the raw "combo-A" string must NOT appear in the response body.
const text = JSON.stringify(body);
assert.ok(!text.includes("combo-A"));
});
});
describe("all five route 4xx branches have a defined COMBO_* code", () => {
// The route uses these codes; this is a regression test against accidental
// removal of a code from the catalog (would break clients parsing `code`).
const expectedCodes = [
"COMBO_001", // JSON parse failure (route.ts L49-58)
"COMBO_002", // zod schema failure (route.ts L65)
"COMBO_003", // composite tier config (route.ts L117)
"COMBO_004", // name collision (route.ts L124)
"COMBO_005", // DAG validation (route.ts L139)
"COMBO_006", // quota-share conflict 409 (route.ts L71-78)
"COMBO_007", // not found 404 (route.ts L31)
] as const;
for (const code of expectedCodes) {
it(`${code} is registered with httpStatus 400 or 409 or 404`, () => {
const def = ERROR_CODES[code];
assert.notEqual(def, undefined);
assert.ok([400, 404, 409].includes(def.httpStatus));
assert.equal(def.category, "COMBO");
});
}
});

View File

@@ -0,0 +1,42 @@
// Characterization of the db/apiKeys.ts model-permission split (god-file decomposition): the pure
// permission-matching logic (Claude-Code alias/prefix resolution + wildcard/glob matching) moved into
// db/apiKeys/modelPermissions.ts. Behavior-preserving move — these functions decide whether a model is
// allowed for a key, so the locks here exercise the wildcard semantics directly. The DB-backed
// isModelAllowedForKey flow stays covered by api-key-policy / combo-provider-wildcard.
import { test } from "node:test";
import assert from "node:assert/strict";
const M = await import("../../src/lib/db/apiKeys/modelPermissions.ts");
test("module exposes the matching helpers + Claude-Code alias sets", () => {
for (const name of [
"modelPatternMatches",
"matchesWildcardPattern",
"segmentMatchesWildcard",
"hasClaudeCodeWildcardPermission",
"isPotentialUnprefixedClaudeCodeModel",
"addModelCandidate",
"stripExtendedContextSuffix",
]) {
assert.equal(typeof (M as Record<string, unknown>)[name], "function", `missing ${name}`);
}
assert.ok((M.CLAUDE_CODE_PROVIDER_PREFIXES as Set<string>).has("cc"));
assert.ok((M.CLAUDE_CODE_SHORT_ALIASES as Set<string>).has("sonnet"));
});
test("matchesWildcardPattern honors segment globs and exact matches", () => {
assert.equal(M.matchesWildcardPattern("openai/*", "openai/gpt-4o"), true);
assert.equal(M.matchesWildcardPattern("openai/*", "anthropic/claude"), false);
assert.equal(M.matchesWildcardPattern("gpt-4o", "gpt-4o"), true);
});
test("modelPatternMatches matches a candidate against an exact or wildcard pattern", () => {
assert.equal(M.modelPatternMatches("openai/*", ["openai/gpt-4o"]), true);
assert.equal(M.modelPatternMatches("gpt-4o", ["gpt-4o"]), true);
assert.equal(M.modelPatternMatches("gpt-4o", ["gpt-4o-mini"]), false);
});
test("stripExtendedContextSuffix removes the extended-context marker", () => {
const out = M.stripExtendedContextSuffix("claude-sonnet-4-5[1m]");
assert.equal(out.includes("[1m]"), false);
});

View File

@@ -0,0 +1,90 @@
// Characterization of the db/apiKeys.ts row-parsers split (god-file decomposition): the pure column
// parsers that coerce raw SQLite values into typed shapes moved into db/apiKeys/rowParsers.ts, and the
// two shared row types (AccessSchedule / RateLimitRule) into db/apiKeys/types.ts. Behavior-preserving
// move — these locks pin the coercion semantics (JSON lists, 0/1 flags, nullable timestamps, schedule
// validation) and that the host still re-exports the two public types. DB-backed hydration stays
// covered by api-key-policy / combo-provider-wildcard.
import { test } from "node:test";
import assert from "node:assert/strict";
const P = await import("../../src/lib/db/apiKeys/rowParsers.ts");
test("module exposes all fifteen parsers", () => {
for (const name of [
"parseAllowedModels",
"parseAllowedCombos",
"parseNoLog",
"parseAutoResolve",
"parseDisableNonPublicModels",
"parseAllowUsageCommand",
"parseIsActive",
"parseAccessSchedule",
"parseRateLimits",
"parseAllowedConnections",
"parseAllowedQuotas",
"parseStringList",
"parseNullableTimestamp",
"parseIsBanned",
"parseStreamDefaultMode",
]) {
assert.equal(typeof (P as Record<string, unknown>)[name], "function", `missing ${name}`);
}
});
test("parseAllowedModels keeps only string entries, tolerates junk", () => {
assert.deepEqual(P.parseAllowedModels('["a","b",1,null]'), ["a", "b"]);
assert.deepEqual(P.parseAllowedModels(""), []);
assert.deepEqual(P.parseAllowedModels("not json"), []);
assert.deepEqual(P.parseAllowedModels(null), []);
});
test("flag parsers honor the 0/1/true/false matrix", () => {
assert.equal(P.parseNoLog(1), true);
assert.equal(P.parseNoLog("1"), true);
assert.equal(P.parseNoLog(0), false);
// isActive defaults to active unless explicitly falsy
assert.equal(P.parseIsActive(undefined), true);
assert.equal(P.parseIsActive(0), false);
assert.equal(P.parseIsActive("0"), false);
assert.equal(P.parseIsBanned(1), true);
assert.equal(P.parseIsBanned(0), false);
});
test("parseStreamDefaultMode collapses to legacy unless json", () => {
assert.equal(P.parseStreamDefaultMode("json"), "json");
assert.equal(P.parseStreamDefaultMode("legacy"), "legacy");
assert.equal(P.parseStreamDefaultMode("anything"), "legacy");
});
test("parseNullableTimestamp trims and nulls empties", () => {
assert.equal(P.parseNullableTimestamp(" 2026-01-01 "), "2026-01-01");
assert.equal(P.parseNullableTimestamp(" "), null);
assert.equal(P.parseNullableTimestamp(42), null);
});
test("parseAccessSchedule validates shape + clamps days, else null", () => {
const ok = P.parseAccessSchedule(
JSON.stringify({ enabled: true, from: "08:00", until: "18:00", days: [0, 3, 9], tz: "UTC" })
);
assert.ok(ok);
assert.deepEqual(ok?.days, [0, 3]); // 9 dropped (out of 0..6)
assert.equal(P.parseAccessSchedule('{"enabled":true}'), null);
assert.equal(P.parseAccessSchedule("[]"), null);
assert.equal(P.parseAccessSchedule(""), null);
});
test("parseRateLimits keeps well-formed numeric rules only", () => {
const out = P.parseRateLimits(
JSON.stringify([{ limit: 10, window: 60 }, { limit: "x", window: 1 }, null])
);
assert.deepEqual(out, [{ limit: 10, window: 60 }]);
assert.equal(P.parseRateLimits("not array"), null);
assert.equal(P.parseRateLimits(""), null);
});
test("host re-exports the two public row types (compile-time) and the parsers stay wired", async () => {
// type-only re-export can't be probed at runtime; assert the host module still loads + exposes its API
const HOST = await import("../../src/lib/db/apiKeys.ts");
assert.equal(typeof (HOST as Record<string, unknown>).getApiKeys, "function");
assert.equal(typeof (HOST as Record<string, unknown>).isModelAllowedForKey, "function");
});

View File

@@ -88,6 +88,31 @@ test("sanitizeReasoningEffortForProvider: xiaomi-mimo normalizes max → xhigh b
);
});
test("sanitizeReasoningEffortForProvider: Ollama Cloud preserves max", () => {
const log = makeLog();
const body = {
model: "glm-5.2",
reasoning_effort: "max",
messages: [{ role: "user", content: "hi" }],
};
const result = sanitizeReasoningEffortForProvider(body, "ollama-cloud", "glm-5.2", log);
assert.equal(result, body, "Ollama Cloud accepts max literally");
assert.equal((result as any).reasoning_effort, "max");
assert.equal(log.messages.length, 0);
});
test("sanitizeReasoningEffortForProvider: Ollama Cloud preserves nested max", () => {
const body = {
model: "glm-5.2",
reasoning: { effort: "max", summary: "auto" },
input: [],
};
const result = sanitizeReasoningEffortForProvider(body, "ollama-cloud", "glm-5.2", null);
assert.equal(result, body, "Ollama Cloud accepts max literally");
assert.equal((result as any).reasoning.effort, "max");
assert.equal((result as any).reasoning.summary, "auto");
});
test("sanitizeReasoningEffortForProvider: OpenRouter DeepSeek normalizes max → xhigh", () => {
const log = makeLog();
const body = {

View File

@@ -0,0 +1,35 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { findRunnerMismatches } from "../../../scripts/check/check-test-runner-api.mjs";
function tmpRepo() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "runner-api-"));
fs.mkdirSync(path.join(root, "tests/unit/autoCombo"), { recursive: true });
return root;
}
test("flags a vitest-only-dir test that imports node:test", () => {
const root = tmpRepo();
fs.writeFileSync(
path.join(root, "tests/unit/autoCombo/bad.test.ts"),
`import { describe, it } from "node:test";\ndescribe("x", () => it("y", () => {}));\n`
);
const bad = findRunnerMismatches(root);
assert.equal(bad.length, 1);
assert.match(bad[0].file, /autoCombo\/bad\.test\.ts$/);
assert.match(bad[0].reason, /vitest-only/);
fs.rmSync(root, { recursive: true, force: true });
});
test("accepts a vitest-only-dir test that imports vitest", () => {
const root = tmpRepo();
fs.writeFileSync(
path.join(root, "tests/unit/autoCombo/good.test.ts"),
`import { describe, it } from "vitest";\ndescribe("x", () => it("y", () => {}));\n`
);
assert.equal(findRunnerMismatches(root).length, 0);
fs.rmSync(root, { recursive: true, force: true });
});

View File

@@ -0,0 +1,45 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join } from "node:path";
const repoRoot = process.cwd();
function readJson<T = Record<string, unknown>>(relPath: string): T {
return JSON.parse(readFileSync(join(repoRoot, relPath), "utf8")) as T;
}
test("@huggingface/transformers is optional so onnxruntime CUDA install failures cannot abort OmniRoute install", () => {
const pkg = readJson<{
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
}>("package.json");
assert.equal(
pkg.dependencies?.["@huggingface/transformers"],
undefined,
"transformers must not be a regular dependency because it pulls onnxruntime-node install scripts"
);
assert.equal(pkg.optionalDependencies?.["@huggingface/transformers"], "3.5.2");
});
test("package-lock marks transformers and its onnxruntime runtime as optional", () => {
const lock = readJson<{
packages: Record<string, { optional?: boolean; dependencies?: Record<string, string>; optionalDependencies?: Record<string, string> }>;
}>("package-lock.json");
assert.equal(
lock.packages[""]?.dependencies?.["@huggingface/transformers"],
undefined,
"root lock dependencies must not keep transformers as mandatory"
);
assert.equal(lock.packages[""]?.optionalDependencies?.["@huggingface/transformers"], "3.5.2");
for (const packagePath of [
"node_modules/@huggingface/transformers",
"node_modules/onnxruntime-node",
"node_modules/onnxruntime-common",
]) {
assert.equal(lock.packages[packagePath]?.optional, true, `${packagePath} should be optional`);
}
});

View File

@@ -0,0 +1,47 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { syncChangelogSection } from "../../../scripts/release/sync-changelog-i18n.mjs";
function repo() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "cl-i18n-"));
fs.writeFileSync(
path.join(root, "CHANGELOG.md"),
"# Changelog\n\n## [Unreleased]\n\n---\n\n## [9.9.9] — 2026-01-01\n\n- **feat:** big new thing\n- **fix:** another\n\n---\n\n## [9.9.8] — 2025-12-01\n\n- old\n"
);
for (const loc of ["fr", "de"]) {
fs.mkdirSync(path.join(root, "docs/i18n", loc), { recursive: true });
fs.writeFileSync(
path.join(root, "docs/i18n", loc, "CHANGELOG.md"),
"# Changelog\n\n---\n\n## [9.9.9] — TBD\n\n_stub_\n\n---\n\n## [9.9.8] — 2025-12-01\n\n- old\n"
);
}
return root;
}
test("replaces the version section in every mirror with the root section", () => {
const root = repo();
const n = syncChangelogSection(root, "9.9.9", "9.9.8");
assert.equal(n, 2);
const fr = fs.readFileSync(path.join(root, "docs/i18n/fr/CHANGELOG.md"), "utf8");
assert.match(fr, /big new thing/);
assert.doesNotMatch(fr, /_stub_/);
fs.rmSync(root, { recursive: true, force: true });
});
test("inserts the section when a mirror lacks it", () => {
const root = repo();
// remove the 9.9.9 section from the fr mirror entirely
fs.writeFileSync(
path.join(root, "docs/i18n/fr/CHANGELOG.md"),
"# Changelog\n\n---\n\n## [9.9.8] — 2025-12-01\n\n- old\n"
);
const n = syncChangelogSection(root, "9.9.9", "9.9.8");
assert.equal(n, 2);
const fr = fs.readFileSync(path.join(root, "docs/i18n/fr/CHANGELOG.md"), "utf8");
assert.match(fr, /## \[9\.9\.9\]/);
assert.match(fr, /big new thing/);
fs.rmSync(root, { recursive: true, force: true });
});

View File

@@ -0,0 +1,78 @@
// Characterization of applyClientUsageBuffer — the non-streaming usage buffer/estimate block
// extracted from handleChatCore (chatCore god-file decomposition, #3501). Deps are injected so the
// buffer-vs-estimate branch and the in-place mutation of translatedResponse.usage are observable.
// Locks: usage present → buffer+filter; usage absent → estimate from content length; empty content
// (length 2 from JSON.stringify("")) still estimates; the mutation target is translatedResponse.
import { test } from "node:test";
import assert from "node:assert/strict";
const { applyClientUsageBuffer } = await import(
"../../open-sse/handlers/chatCore/clientUsageBuffer.ts"
);
function makeDeps(overrides: Record<string, unknown> = {}) {
const calls = { buffer: [] as unknown[], estimate: [] as unknown[], filter: [] as unknown[] };
const deps = {
addBufferToUsage: (u: unknown) => {
calls.buffer.push(u);
return { ...(u as object), _buffered: true };
},
estimateUsage: (...a: unknown[]) => {
calls.estimate.push(a);
return { _estimated: true };
},
filterUsageForFormat: (u: unknown, _fmt: unknown) => {
calls.filter.push(u);
return { ...(u as object), _filtered: true };
},
...overrides,
} as Parameters<typeof applyClientUsageBuffer>[3];
return { deps, calls };
}
test("usage present → buffer then filter, mutates in place", () => {
const { deps, calls } = makeDeps();
const resp: Record<string, unknown> = { usage: { prompt_tokens: 5 } };
applyClientUsageBuffer(resp, { messages: [] }, "openai", deps);
assert.equal(calls.buffer.length, 1);
assert.equal(calls.estimate.length, 0);
assert.equal((resp.usage as Record<string, unknown>)._buffered, true);
assert.equal((resp.usage as Record<string, unknown>)._filtered, true);
});
test("no usage but content present → estimate then filter", () => {
const { deps, calls } = makeDeps();
const resp: Record<string, unknown> = {
choices: [{ message: { content: "hello world" } }],
};
applyClientUsageBuffer(resp, { messages: [] }, "openai", deps);
assert.equal(calls.buffer.length, 0);
assert.equal(calls.estimate.length, 1);
assert.equal((resp.usage as Record<string, unknown>)._estimated, true);
assert.equal((resp.usage as Record<string, unknown>)._filtered, true);
// estimateUsage receives (body, contentLength, format)
const args = calls.estimate[0] as unknown[];
assert.equal(args[2], "openai");
assert.equal(typeof args[1], "number");
});
test("empty content → JSON.stringify('') length 2 > 0 still estimates", () => {
const { deps, calls } = makeDeps();
const resp: Record<string, unknown> = {};
applyClientUsageBuffer(resp, {}, "claude", deps);
// content "" → JSON.stringify("") = '""' length 2 → contentLength 2 > 0
assert.equal(calls.estimate.length, 1);
const args = calls.estimate[0] as unknown[];
assert.equal(args[1], 2);
});
test("content length is computed from choices[0].message.content", () => {
const { deps, calls } = makeDeps();
const resp: Record<string, unknown> = {
choices: [{ message: { content: "abc" } }],
};
applyClientUsageBuffer(resp, {}, "openai", deps);
// JSON.stringify("abc") = '"abc"' → length 5
const args = calls.estimate[0] as unknown[];
assert.equal(args[1], 5);
});

View File

@@ -0,0 +1,129 @@
// Characterization of writeCompressionAnalytics — the full compression analytics write extracted
// from handleChatCore's request-setup compression path (chatCore god-file decomposition, #3501).
// Returns the write promise; uses a real temp DB. Locks: the analytics row mapping (tokens, engine,
// validation_fallback, output_mode), the per-engine breakdown insert, and fail-open.
import { test, before, after } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const testDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "omni-comp-analytics-test-"));
process.env.DATA_DIR = testDataDir;
const coreDb = await import("../../src/lib/db/core.ts");
const { writeCompressionAnalytics } = await import(
"../../open-sse/handlers/chatCore/compressionAnalyticsWrite.ts"
);
type Stats = Parameters<typeof writeCompressionAnalytics>[0]["stats"];
function makeStats(overrides: Record<string, unknown> = {}): Stats {
return {
originalTokens: 200,
compressedTokens: 80,
fallbackApplied: false,
engine: "rtk",
compressionComboId: null,
durationMs: 12,
rtkRawOutputPointers: [],
engineBreakdown: [],
...overrides,
} as unknown as Stats;
}
function analyticsRow(requestId: string): Record<string, unknown> | undefined {
try {
return coreDb
.getDbInstance()
.prepare(
"SELECT mode, engine, original_tokens AS orig, compressed_tokens AS comp, tokens_saved AS saved, validation_fallback AS vf FROM compression_analytics WHERE request_id = ?"
)
.get(requestId) as Record<string, unknown> | undefined;
} catch {
return undefined;
}
}
function breakdownCount(requestId: string): number {
try {
const row = coreDb
.getDbInstance()
.prepare("SELECT COUNT(*) AS n FROM compression_engine_breakdown WHERE request_id = ?")
.get(requestId) as { n: number } | undefined;
return row?.n ?? 0;
} catch {
return 0;
}
}
async function waitFor(pred: () => boolean, timeoutMs = 3000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline && !pred()) {
await new Promise((r) => setTimeout(r, 25));
}
}
function baseOpts(requestId: string, statsOverrides = {}) {
return {
stats: makeStats(statsOverrides),
provider: "openai",
effectiveModel: "gpt-x",
effectiveServiceTier: "standard",
comboName: null,
mode: "rtk",
compressionComboId: null,
skillRequestId: requestId,
cavemanOutputModeApplied: false,
cavemanOutputModeIntensity: null,
} as Parameters<typeof writeCompressionAnalytics>[0];
}
before(async () => {
await coreDb.ensureDbInitialized();
});
after(() => {
coreDb.resetDbInstance();
try {
fs.rmSync(testDataDir, { recursive: true, force: true });
} catch {
// best-effort cleanup
}
});
test("writes the analytics row with mapped tokens/engine and resolves", async () => {
await writeCompressionAnalytics(baseOpts("ca-req-1"));
const row = analyticsRow("ca-req-1");
assert.ok(row, "expected a compression_analytics row");
assert.equal(row!.engine, "rtk");
assert.equal(row!.orig, 200);
assert.equal(row!.comp, 80);
assert.equal(row!.saved, 120);
assert.equal(row!.vf, 0);
});
test("validation_fallback is 1 when the stats flagged a fallback", async () => {
await writeCompressionAnalytics(baseOpts("ca-req-2", { fallbackApplied: true }));
const row = analyticsRow("ca-req-2");
assert.equal(row!.vf, 1);
});
test("inserts a per-engine breakdown when present", async () => {
await writeCompressionAnalytics(
baseOpts("ca-req-3", {
engineBreakdown: [
{ engine: "rtk", originalTokens: 200, compressedTokens: 120, durationMs: 5 },
{ engine: "caveman", originalTokens: 120, compressedTokens: 80, durationMs: 4 },
],
})
);
await waitFor(() => breakdownCount("ca-req-3") >= 2);
assert.equal(breakdownCount("ca-req-3"), 2);
});
test("never rejects even on a bad write (fail-open)", async () => {
coreDb.resetDbInstance();
await assert.doesNotReject(writeCompressionAnalytics(baseOpts("ca-req-4")));
await coreDb.ensureDbInitialized();
});

View File

@@ -0,0 +1,64 @@
// Characterization of the pure compression-combo predicates extracted from handleChatCore's
// compression setup (chatCore god-file decomposition, #3501). No DB, no handler state.
import { test } from "node:test";
import assert from "node:assert/strict";
const { isBuiltinStackedPipeline, isStackedCompressionCombo } = await import(
"../../open-sse/handlers/chatCore/compressionComboPredicates.ts"
);
test("isBuiltinStackedPipeline true only for the rtk(standard)→caveman(full) shape", () => {
assert.equal(
isBuiltinStackedPipeline([{ engine: "rtk" }, { engine: "caveman" }] as never),
true
);
assert.equal(
isBuiltinStackedPipeline([
{ engine: "rtk", intensity: "standard" },
{ engine: "caveman", intensity: "full" },
] as never),
true
);
});
test("isBuiltinStackedPipeline false for wrong length / engines / intensities / config", () => {
assert.equal(isBuiltinStackedPipeline(undefined), false);
assert.equal(isBuiltinStackedPipeline([] as never), false);
assert.equal(isBuiltinStackedPipeline([{ engine: "rtk" }] as never), false);
assert.equal(
isBuiltinStackedPipeline([{ engine: "caveman" }, { engine: "rtk" }] as never),
false
);
assert.equal(
isBuiltinStackedPipeline([
{ engine: "rtk", intensity: "aggressive" },
{ engine: "caveman", intensity: "full" },
] as never),
false
);
assert.equal(
isBuiltinStackedPipeline([
{ engine: "rtk", config: { x: 1 } },
{ engine: "caveman" },
] as never),
false
);
});
test("isStackedCompressionCombo true when the combo has >= 1 pipeline layer", () => {
assert.equal(isStackedCompressionCombo(null), false);
assert.equal(
isStackedCompressionCombo({ id: "c", pipeline: [], languagePacks: [], outputMode: false, outputModeIntensity: "full" } as never),
false
);
assert.equal(
isStackedCompressionCombo({
id: "c",
pipeline: [{ engine: "rtk" }],
languagePacks: [],
outputMode: false,
outputModeIntensity: "full",
} as never),
true
);
});

View File

@@ -0,0 +1,44 @@
// Characterization of resolveCompressionSettings — the compression settings read extracted from
// handleChatCore's Proactive Context Compression setup (chatCore god-file decomposition, #3501).
// Uses a real temp DB (getCompressionSettings reads/seeds the settings row). Locks: the derived
// enabled / contextEditingEnabled flags and the disabled fallback shape.
import { test, before, after } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const testDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "omni-comp-settings-test-"));
process.env.DATA_DIR = testDataDir;
const coreDb = await import("../../src/lib/db/core.ts");
const { resolveCompressionSettings } = await import(
"../../open-sse/handlers/chatCore/compressionSettings.ts"
);
before(async () => {
await coreDb.ensureDbInitialized();
});
after(() => {
coreDb.resetDbInstance();
try {
fs.rmSync(testDataDir, { recursive: true, force: true });
} catch {
// best-effort cleanup
}
});
test("returns the settings object from the DB", async () => {
const result = await resolveCompressionSettings();
assert.ok(result.settings, "expected a settings object from the seeded DB");
});
test("derives enabled and contextEditingEnabled from the settings", async () => {
const result = await resolveCompressionSettings();
// `enabled` mirrors settings.enabled exactly
assert.equal(result.enabled, result.settings!.enabled);
// `contextEditingEnabled` is the strict === true derivation of the nested flag
assert.equal(result.contextEditingEnabled, result.settings!.contextEditing?.enabled === true);
assert.equal(typeof result.contextEditingEnabled, "boolean");
});

View File

@@ -0,0 +1,73 @@
// Characterization of maybeConvertJsonBodyToSse — the #3089 non-SSE JSON → SSE conversion
// extracted from handleChatCore's streaming entry (chatCore god-file decomposition, #3501). Real
// Response objects + injected deps make the content-type gate, the synthesize-or-rebuild branch,
// and the header rewrites observable. Locks: pass-through for SSE/ndjson/no-body, conversion to
// text/event-stream when synthesizable, and rebuild-with-consumed-body otherwise.
import { test } from "node:test";
import assert from "node:assert/strict";
const { maybeConvertJsonBodyToSse } = await import(
"../../open-sse/handlers/chatCore/jsonBodyToSse.ts"
);
const ctx = { log: undefined, provider: "openai", model: "gpt-x" };
function makeDeps(synth: (s: string) => string | null) {
return {
withBodyTimeout: async <T>(p: Promise<T>) => p,
synthesizeOpenAiSseFromJson: synth,
} as Parameters<typeof maybeConvertJsonBodyToSse>[2];
}
test("already text/event-stream → returned unchanged (same reference)", async () => {
const resp = new Response("data: {}\n\n", {
headers: { "content-type": "text/event-stream" },
});
const out = await maybeConvertJsonBodyToSse(resp, ctx, makeDeps(() => "X"));
assert.equal(out, resp);
});
test("application/x-ndjson → returned unchanged", async () => {
const resp = new Response('{"a":1}\n', { headers: { "content-type": "application/x-ndjson" } });
const out = await maybeConvertJsonBodyToSse(resp, ctx, makeDeps(() => "X"));
assert.equal(out, resp);
});
test("no body → returned unchanged", async () => {
const resp = new Response(null, { headers: { "content-type": "application/json" } });
const out = await maybeConvertJsonBodyToSse(resp, ctx, makeDeps(() => "X"));
assert.equal(out, resp);
});
test("application/json + synthesizable → SSE response with text/event-stream, no content-length", async () => {
const resp = new Response('{"choices":[]}', {
headers: { "content-type": "application/json", "content-length": "14" },
});
const out = await maybeConvertJsonBodyToSse(resp, ctx, makeDeps(() => "data: synth\n\n"));
assert.notEqual(out, resp);
assert.equal(out.headers.get("content-type"), "text/event-stream");
assert.equal(out.headers.get("content-length"), null);
assert.equal(await out.text(), "data: synth\n\n");
});
test("application/json + not synthesizable → rebuilt with consumed body, content-type unchanged", async () => {
const resp = new Response('{"not":"chat"}', {
headers: { "content-type": "application/json" },
});
const out = await maybeConvertJsonBodyToSse(resp, ctx, makeDeps(() => null));
assert.notEqual(out, resp);
// not converted → content-type stays application/json (rebuilt headers)
assert.equal(out.headers.get("content-type"), "application/json");
assert.equal(await out.text(), '{"not":"chat"}');
});
test("status and statusText are preserved on conversion", async () => {
const resp = new Response('{"choices":[]}', {
status: 201,
statusText: "Created",
headers: { "content-type": "application/json" },
});
const out = await maybeConvertJsonBodyToSse(resp, ctx, makeDeps(() => "data: x\n\n"));
assert.equal(out.status, 201);
assert.equal(out.statusText, "Created");
});

View File

@@ -0,0 +1,73 @@
// Characterization of buildNonStreamingResponseHeaders — the cache-MISS response header builder
// extracted from handleChatCore's non-streaming success path (chatCore god-file decomposition,
// #3501). attachOmniRouteMetaHeaders + now are injected so the static headers, the meta payload,
// and the optional compression header are observable. Locks: Content-Type + cache MISS, latencyMs =
// now - startTime, and the compression header only when meta is present.
import { test } from "node:test";
import assert from "node:assert/strict";
const { buildNonStreamingResponseHeaders } = await import(
"../../open-sse/handlers/chatCore/nonStreamingResponseHeaders.ts"
);
function makeDeps(now = 1000) {
const metaCalls: Array<{ headers: Record<string, string>; meta: Record<string, unknown> }> = [];
const deps = {
attachOmniRouteMetaHeaders: (headers: Record<string, string>, meta: Record<string, unknown>) => {
metaCalls.push({ headers, meta });
headers["x-omniroute-meta"] = "attached";
},
now: () => now,
} as Parameters<typeof buildNonStreamingResponseHeaders>[1];
return { deps, metaCalls };
}
function baseArgs(overrides: Record<string, unknown> = {}) {
return {
provider: "openai",
model: "gpt-x",
startTime: 600,
responseUsage: { prompt_tokens: 5 },
estimatedCost: 0.0012,
requestId: "req-1",
compressionResponseMeta: undefined,
...overrides,
} as Parameters<typeof buildNonStreamingResponseHeaders>[0];
}
test("static headers: Content-Type json + cache MISS", () => {
const { deps } = makeDeps();
const h = buildNonStreamingResponseHeaders(baseArgs(), deps);
assert.equal(h["Content-Type"], "application/json");
// cache marker key/value
assert.ok(Object.values(h).includes("MISS"));
});
test("meta receives provider/model/cacheHit false/latency/usage/cost/requestId", () => {
const { deps, metaCalls } = makeDeps(1000);
buildNonStreamingResponseHeaders(baseArgs({ startTime: 600 }), deps);
assert.equal(metaCalls.length, 1);
const meta = metaCalls[0].meta;
assert.equal(meta.provider, "openai");
assert.equal(meta.model, "gpt-x");
assert.equal(meta.cacheHit, false);
assert.equal(meta.latencyMs, 400); // now 1000 - startTime 600
assert.deepEqual(meta.usage, { prompt_tokens: 5 });
assert.equal(meta.costUsd, 0.0012);
assert.equal(meta.requestId, "req-1");
});
test("no compression meta → no compression header", () => {
const { deps } = makeDeps();
const h = buildNonStreamingResponseHeaders(baseArgs({ compressionResponseMeta: undefined }), deps);
assert.ok(!Object.values(h).includes("engine:x"));
});
test("compression meta present → compression header set to that value", () => {
const { deps } = makeDeps();
const h = buildNonStreamingResponseHeaders(
baseArgs({ compressionResponseMeta: "engine:x; source=header" }),
deps
);
assert.ok(Object.values(h).includes("engine:x; source=header"));
});

View File

@@ -0,0 +1,102 @@
// Characterization of emitOutputStyleTelemetry — the output-style run-telemetry hook extracted from
// handleChatCore's request-setup compression path (chatCore god-file decomposition, #3501).
// Fire-and-forget; uses a real temp DB and polls compression_run_telemetry. Locks: the null guard
// (no-op), and that an applied output-style result records a run-telemetry row with the resolved
// request id / model / provider / source.
import { test, before, after } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const testDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "omni-os-telemetry-test-"));
process.env.DATA_DIR = testDataDir;
const coreDb = await import("../../src/lib/db/core.ts");
const { emitOutputStyleTelemetry } = await import(
"../../open-sse/handlers/chatCore/outputStyleTelemetry.ts"
);
function rowFor(requestId: string): Record<string, unknown> | undefined {
try {
return coreDb
.getDbInstance()
.prepare(
"SELECT request_id, model, provider, source FROM compression_run_telemetry WHERE request_id = ?"
)
.get(requestId) as Record<string, unknown> | undefined;
} catch {
// table is created lazily by the first fire-and-forget insert; treat "no such table" as "no row"
return undefined;
}
}
async function waitForRow(requestId: string, timeoutMs = 3000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (rowFor(requestId)) break;
await new Promise((r) => setTimeout(r, 25));
}
return rowFor(requestId);
}
before(async () => {
await coreDb.ensureDbInitialized();
});
after(() => {
coreDb.resetDbInstance();
try {
fs.rmSync(testDataDir, { recursive: true, force: true });
} catch {
// best-effort cleanup
}
});
test("null outputStyleResult is a no-op (returns synchronously, no throw)", async () => {
assert.doesNotThrow(() =>
emitOutputStyleTelemetry({
outputStyleResult: null,
skillRequestId: "os-noop",
traceId: "t",
effectiveModel: "gpt-x",
provider: "openai",
compressionComboId: null,
estimatedTokens: 100,
})
);
await new Promise((r) => setTimeout(r, 80));
assert.equal(rowFor("os-noop"), undefined);
});
test("applied output-style result records a run-telemetry row (source=active-profile when combo id set)", async () => {
emitOutputStyleTelemetry({
outputStyleResult: { body: {} as never, applied: true, appliedStyles: [], skippedReason: undefined },
skillRequestId: "os-req-1",
traceId: "trace-1",
effectiveModel: "gpt-os",
provider: "openai",
compressionComboId: "combo-9",
estimatedTokens: 250,
});
const row = await waitForRow("os-req-1");
assert.ok(row, "expected a compression_run_telemetry row");
assert.equal(row!.model, "gpt-os");
assert.equal(row!.provider, "openai");
assert.equal(row!.source, "active-profile");
});
test("falls back to traceId for request id and source=default without a combo id", async () => {
emitOutputStyleTelemetry({
outputStyleResult: { body: {} as never, applied: true, appliedStyles: [] },
skillRequestId: null,
traceId: "os-trace-2",
effectiveModel: "gpt-x",
provider: "anthropic",
compressionComboId: null,
estimatedTokens: 10,
});
const row = await waitForRow("os-trace-2");
assert.ok(row, "expected the row keyed by the traceId fallback");
assert.equal(row!.source, "default");
});

View File

@@ -0,0 +1,73 @@
// Characterization of runPluginOnRequestHook — the plugin onRequest gate extracted from
// handleChatCore's request entry (chatCore god-file decomposition, #3501). Hooks are in-memory.
// Locks: the discriminated result — blocked (403 Response) vs body-rewrite vs pass-through — and
// fail-open on a throwing hook.
import { test, afterEach } from "node:test";
import assert from "node:assert/strict";
const { registerHook, unregisterHook } = await import("../../src/lib/plugins/hooks.ts");
const { runPluginOnRequestHook } = await import(
"../../open-sse/handlers/chatCore/pluginOnRequest.ts"
);
const PLUGIN = "test-onrequest-plugin";
afterEach(() => {
unregisterHook("onRequest", PLUGIN);
});
function baseArgs(overrides: Record<string, unknown> = {}) {
return {
requestId: "req-1",
body: { messages: [{ role: "user", content: "hi" }] },
model: "gpt-x",
provider: "openai",
apiKeyInfo: null,
...overrides,
} as Parameters<typeof runPluginOnRequestHook>[0];
}
test("no registered hooks → pass-through (blocked:false, no body)", async () => {
const gate = await runPluginOnRequestHook(baseArgs());
assert.equal(gate.blocked, false);
});
test("a blocking hook → blocked:true with a 403 JSON Response", async () => {
registerHook("onRequest", PLUGIN, async () => ({
blocked: true,
response: { error: "nope" },
}));
const gate = await runPluginOnRequestHook(baseArgs());
assert.equal(gate.blocked, true);
if (!gate.blocked) return;
assert.equal(gate.response.status, 403);
const payload = await gate.response.json();
assert.deepEqual(payload, { error: "nope" });
});
test("a blocking hook without a response → generic plugin_block 403", async () => {
registerHook("onRequest", PLUGIN, async () => ({ blocked: true }));
const gate = await runPluginOnRequestHook(baseArgs());
assert.equal(gate.blocked, true);
if (!gate.blocked) return;
assert.equal(gate.response.status, 403);
const payload = (await gate.response.json()) as { error?: { type?: string } };
assert.equal(payload.error?.type, "plugin_block");
});
test("a body-rewriting hook → blocked:false with the new body", async () => {
const rewritten = { messages: [{ role: "user", content: "rewritten" }] };
registerHook("onRequest", PLUGIN, async () => ({ body: rewritten }));
const gate = await runPluginOnRequestHook(baseArgs());
assert.equal(gate.blocked, false);
if (gate.blocked) return;
assert.deepEqual(gate.body, rewritten);
});
test("a throwing hook → fail-open pass-through (blocked:false)", async () => {
registerHook("onRequest", PLUGIN, async () => {
throw new Error("boom");
});
const gate = await runPluginOnRequestHook(baseArgs());
assert.equal(gate.blocked, false);
});

View File

@@ -0,0 +1,72 @@
// Characterization of buildPostCallGuardrailContext — the context object passed to
// guardrailRegistry.runPostCallHooks, extracted from handleChatCore (chatCore god-file
// decomposition, #3501). resolveDisabledGuardrails is injected so the mapping is observable in
// isolation. Locks: the field mapping (sourceFormat/targetFormat), the constants (method "POST",
// stream false), endpoint/headers null-coalescing, and that disabled-resolution gets apiKeyInfo/
// body/headers.
import { test } from "node:test";
import assert from "node:assert/strict";
const { buildPostCallGuardrailContext } = await import(
"../../open-sse/handlers/chatCore/postCallGuardrailContext.ts"
);
function baseArgs(overrides: Record<string, unknown> = {}) {
return {
apiKeyInfo: { id: "key-1" },
body: { messages: [] },
clientRawRequest: { headers: { "x-test": "1" }, endpoint: "/v1/chat/completions" },
log: { tag: "log" },
model: "gpt-x",
provider: "openai",
responsePayloadFormat: "openai",
clientResponseFormat: "claude",
...overrides,
} as Parameters<typeof buildPostCallGuardrailContext>[0];
}
test("maps fields, constants, and source/target formats", () => {
const seen: unknown[] = [];
const ctx = buildPostCallGuardrailContext(baseArgs(), (arg: unknown) => {
seen.push(arg);
return ["g1"];
});
assert.equal(ctx.method, "POST");
assert.equal(ctx.stream, false);
assert.equal(ctx.model, "gpt-x");
assert.equal(ctx.provider, "openai");
assert.equal(ctx.sourceFormat, "openai");
assert.equal(ctx.targetFormat, "claude");
assert.equal(ctx.endpoint, "/v1/chat/completions");
assert.deepEqual(ctx.disabledGuardrails, ["g1"]);
// resolveDisabledGuardrails received apiKeyInfo/body/headers
assert.deepEqual(seen[0], {
apiKeyInfo: { id: "key-1" },
body: { messages: [] },
headers: { "x-test": "1" },
});
});
test("null clientRawRequest → endpoint/headers null", () => {
const ctx = buildPostCallGuardrailContext(baseArgs({ clientRawRequest: null }), () => []);
assert.equal(ctx.endpoint, null);
assert.equal(ctx.headers, null);
});
test("missing endpoint → null (|| null), headers passes through", () => {
const ctx = buildPostCallGuardrailContext(
baseArgs({ clientRawRequest: { headers: { a: "b" } } }),
() => []
);
assert.equal(ctx.endpoint, null);
assert.deepEqual(ctx.headers, { a: "b" });
});
test("null apiKeyInfo coalesces to null for resolveDisabledGuardrails", () => {
let received: { apiKeyInfo?: unknown } = {};
buildPostCallGuardrailContext(baseArgs({ apiKeyInfo: null }), (arg: { apiKeyInfo?: unknown }) => {
received = arg;
return [];
});
assert.equal(received.apiKeyInfo, null);
});

View File

@@ -0,0 +1,109 @@
// Characterization of storeSemanticCacheResponse — the Phase 9.1 non-streaming semantic-cache
// store extracted from handleChatCore (chatCore god-file decomposition, #3501). Deps are injected
// so the gating logic + signature derivation are observable without the real cache backend.
// Locks: the 3-way gate (enabled + cacheable-for-write + small-enough), the signature args
// (messages ?? input, temperature, top_p, apiKeyId), and the `prompt + completion || 0` token count.
import { test } from "node:test";
import assert from "node:assert/strict";
const { storeSemanticCacheResponse } = await import(
"../../open-sse/handlers/chatCore/semanticCacheStore.ts"
);
type Stored = { sig: unknown; model: string; response: unknown; tokens: number };
function makeDeps(overrides: Record<string, unknown> = {}) {
const stored: Stored[] = [];
const calls = { cacheable: 0, small: 0, signature: 0 };
const deps = {
isCacheableForWrite: (..._a: unknown[]) => {
calls.cacheable++;
return true;
},
isSmallEnoughForSemanticCache: (..._a: unknown[]) => {
calls.small++;
return true;
},
generateSignature: (...a: unknown[]) => {
calls.signature++;
return `sig:${JSON.stringify(a)}`;
},
setCachedResponse: (sig: unknown, model: string, response: unknown, tokens: number) => {
stored.push({ sig, model, response, tokens });
},
...overrides,
} as Parameters<typeof storeSemanticCacheResponse>[1];
return { deps, stored, calls };
}
function baseArgs(overrides: Record<string, unknown> = {}) {
return {
enabled: true,
body: { messages: [{ role: "user", content: "hi" }], temperature: 0, top_p: 1 },
headers: undefined,
translatedResponse: { id: "resp-1" },
model: "gpt-x",
apiKeyId: "key-1",
usage: { prompt_tokens: 10, completion_tokens: 5 },
log: undefined,
...overrides,
} as Parameters<typeof storeSemanticCacheResponse>[0];
}
test("happy path → stores under a signature, tokensSaved = prompt + completion", () => {
const { deps, stored } = makeDeps();
storeSemanticCacheResponse(baseArgs(), deps);
assert.equal(stored.length, 1);
assert.equal(stored[0].model, "gpt-x");
assert.deepEqual(stored[0].response, { id: "resp-1" });
assert.equal(stored[0].tokens, 15);
});
test("disabled → no store, no gate calls past enabled", () => {
const { deps, stored, calls } = makeDeps();
storeSemanticCacheResponse(baseArgs({ enabled: false }), deps);
assert.equal(stored.length, 0);
assert.equal(calls.cacheable, 0);
});
test("not cacheable-for-write → no store", () => {
const { deps, stored } = makeDeps({ isCacheableForWrite: () => false });
storeSemanticCacheResponse(baseArgs(), deps);
assert.equal(stored.length, 0);
});
test("too large → no store", () => {
const { deps, stored } = makeDeps({ isSmallEnoughForSemanticCache: () => false });
storeSemanticCacheResponse(baseArgs(), deps);
assert.equal(stored.length, 0);
});
test("signature uses messages when present, with model/temperature/top_p/apiKeyId", () => {
const { deps, stored } = makeDeps();
storeSemanticCacheResponse(baseArgs(), deps);
const sig = stored[0].sig as string;
assert.ok(sig.includes("gpt-x"));
assert.ok(sig.includes("key-1"));
});
test("falls back to body.input when messages absent", () => {
let captured: unknown[] = [];
const { deps } = makeDeps({
generateSignature: (...a: unknown[]) => {
captured = a;
return "sig";
},
});
storeSemanticCacheResponse(
baseArgs({ body: { input: "the-input", temperature: 0, top_p: 1 } }),
deps
);
// args: (model, messages ?? input, temperature, top_p, apiKeyId)
assert.equal(captured[1], "the-input");
});
test("missing usage → tokensSaved coerces to 0 (NaN || 0)", () => {
const { deps, stored } = makeDeps();
storeSemanticCacheResponse(baseArgs({ usage: undefined }), deps);
assert.equal(stored[0].tokens, 0);
});

View File

@@ -0,0 +1,96 @@
// Characterization of storeStreamingSemanticCacheResponse — the streaming Phase 9.1 cache store
// extracted from handleChatCore's onStreamComplete (chatCore god-file decomposition, #3501). Deps
// are injected so the gate + `_streamed` strip + signature + token accounting are observable
// without the real cache backend. Locks: the 4-way gate (enabled + status 200 + body + cacheable),
// the `_streamed` strip, skip-on-too-large, `Number(...) || 0` tokens, and fail-open on throw.
import { test } from "node:test";
import assert from "node:assert/strict";
const { storeStreamingSemanticCacheResponse } = await import(
"../../open-sse/handlers/chatCore/streamingSemanticCacheStore.ts"
);
type Stored = { sig: unknown; model: string; body: Record<string, unknown>; tokens: number };
function makeDeps(overrides: Record<string, unknown> = {}) {
const stored: Stored[] = [];
const deps = {
isCacheableForWrite: () => true,
isSmallEnoughForSemanticCache: () => true,
generateSignature: (...a: unknown[]) => `sig:${JSON.stringify(a)}`,
setCachedResponse: (sig: unknown, model: string, body: Record<string, unknown>, tokens: number) =>
stored.push({ sig, model, body, tokens }),
...overrides,
} as Parameters<typeof storeStreamingSemanticCacheResponse>[1];
return { deps, stored };
}
function baseArgs(overrides: Record<string, unknown> = {}) {
return {
enabled: true,
streamStatus: 200,
streamResponseBody: { id: "resp-1", _streamed: true },
body: { messages: [{ role: "user", content: "hi" }], temperature: 0, top_p: 1 },
headers: undefined,
model: "gpt-x",
apiKeyId: "key-1",
streamUsage: { prompt_tokens: 12, completion_tokens: 8 },
log: undefined,
...overrides,
} as Parameters<typeof storeStreamingSemanticCacheResponse>[0];
}
test("happy path → stores cleaned body (no _streamed), tokens = prompt + completion", () => {
const { deps, stored } = makeDeps();
storeStreamingSemanticCacheResponse(baseArgs(), deps);
assert.equal(stored.length, 1);
assert.equal(stored[0].model, "gpt-x");
assert.equal(stored[0].tokens, 20);
assert.equal("_streamed" in stored[0].body, false);
assert.equal(stored[0].body.id, "resp-1");
});
test("non-200 stream status → no store", () => {
const { deps, stored } = makeDeps();
storeStreamingSemanticCacheResponse(baseArgs({ streamStatus: 500 }), deps);
assert.equal(stored.length, 0);
});
test("disabled → no store", () => {
const { deps, stored } = makeDeps();
storeStreamingSemanticCacheResponse(baseArgs({ enabled: false }), deps);
assert.equal(stored.length, 0);
});
test("missing response body → no store", () => {
const { deps, stored } = makeDeps();
storeStreamingSemanticCacheResponse(baseArgs({ streamResponseBody: null }), deps);
assert.equal(stored.length, 0);
});
test("not cacheable-for-write → no store", () => {
const { deps, stored } = makeDeps({ isCacheableForWrite: () => false });
storeStreamingSemanticCacheResponse(baseArgs(), deps);
assert.equal(stored.length, 0);
});
test("too large → no store (early return inside try)", () => {
const { deps, stored } = makeDeps({ isSmallEnoughForSemanticCache: () => false });
storeStreamingSemanticCacheResponse(baseArgs(), deps);
assert.equal(stored.length, 0);
});
test("missing usage → tokens coerce to 0", () => {
const { deps, stored } = makeDeps();
storeStreamingSemanticCacheResponse(baseArgs({ streamUsage: null }), deps);
assert.equal(stored[0].tokens, 0);
});
test("a throwing dep is swallowed (fail-open, non-critical)", () => {
const { deps } = makeDeps({
setCachedResponse: () => {
throw new Error("cache write boom");
},
});
assert.doesNotThrow(() => storeStreamingSemanticCacheResponse(baseArgs(), deps));
});

View File

@@ -0,0 +1,102 @@
// Characterization of assembleStreamingPipeline — the streaming transform-chain assembly extracted
// from handleChatCore's streaming success path (chatCore god-file decomposition, #3501). All
// transform factories are injected; a fake "stream" records each pipeThrough so the exact chain
// order and the branch conditions (PII explicit vs feature-flag, progress, echo) are observable
// without real ReadableStreams. Locks: transform order, the progress header side-effect, and the
// PII branch precedence (explicit createPiiTransform wins over the feature flag).
import { test } from "node:test";
import assert from "node:assert/strict";
const { assembleStreamingPipeline } = await import(
"../../open-sse/handlers/chatCore/streamingPipeline.ts"
);
// A fake stream: each pipeThrough appends the transform's tag and returns a new fake stream.
function fakeStream(tag: string, log: string[]) {
return {
tag,
pipeThrough(t: { __tag: string }) {
log.push(t.__tag);
return fakeStream(t.__tag, log);
},
};
}
function makeDeps(over: Record<string, unknown> = {}) {
const log: string[] = [];
const deps = {
wantsProgress: () => false,
pipeWithDisconnect: (..._a: unknown[]) => fakeStream("pii-base", log),
isFeatureFlagEnabled: () => false,
createPiiSseTransform: () => ({ __tag: "pii-flag" }),
createProgressTransform: () => ({ __tag: "progress" }),
createSseHeartbeatTransform: () => ({ __tag: "heartbeat" }),
shapeForClientFormat: (f: unknown) => f,
createModelEchoTransform: () => ({ __tag: "echo" }),
...over,
} as Parameters<typeof assembleStreamingPipeline>[1];
return { deps, log };
}
function baseArgs(over: Record<string, unknown> = {}) {
return {
providerResponse: {},
transformStream: {},
streamController: { signal: {} as AbortSignal },
createPiiTransform: undefined,
clientRawRequestHeaders: null,
clientResponseFormat: "openai",
echoModel: null,
responseHeaders: {} as Record<string, string>,
...over,
} as Parameters<typeof assembleStreamingPipeline>[0];
}
test("baseline (no pii, no progress, no echo) → only heartbeat in the chain", () => {
const { deps, log } = makeDeps();
assembleStreamingPipeline(baseArgs(), deps);
assert.deepEqual(log, ["heartbeat"]);
});
test("feature-flag PII → pii-flag transform applied before heartbeat", () => {
const { deps, log } = makeDeps({ isFeatureFlagEnabled: () => true });
assembleStreamingPipeline(baseArgs(), deps);
assert.deepEqual(log, ["pii-flag", "heartbeat"]);
});
test("explicit createPiiTransform wins over the feature flag", () => {
const { deps, log } = makeDeps({ isFeatureFlagEnabled: () => true });
const explicit = () => ({ __tag: "pii-explicit" });
assembleStreamingPipeline(baseArgs({ createPiiTransform: explicit }), deps);
assert.deepEqual(log, ["pii-explicit", "heartbeat"]);
});
test("progress enabled → progress transform + progress header set", () => {
const { deps, log } = makeDeps({ wantsProgress: () => true });
const args = baseArgs();
assembleStreamingPipeline(args, deps);
assert.deepEqual(log, ["progress", "heartbeat"]);
assert.ok(Object.values(args.responseHeaders).includes("enabled"));
});
test("progress disabled → no progress header", () => {
const { deps } = makeDeps({ wantsProgress: () => false });
const args = baseArgs();
assembleStreamingPipeline(args, deps);
assert.deepEqual(args.responseHeaders, {});
});
test("echoModel set → echo transform applied last", () => {
const { deps, log } = makeDeps();
assembleStreamingPipeline(baseArgs({ echoModel: "alias-x" }), deps);
assert.deepEqual(log, ["heartbeat", "echo"]);
});
test("full chain order: pii → progress → heartbeat → echo", () => {
const { deps, log } = makeDeps({
isFeatureFlagEnabled: () => true,
wantsProgress: () => true,
});
assembleStreamingPipeline(baseArgs({ echoModel: "alias-x" }), deps);
assert.deepEqual(log, ["pii-flag", "progress", "heartbeat", "echo"]);
});

View File

@@ -0,0 +1,65 @@
// Characterization of assembleStreamingResponseHeaders — the streaming response header builder
// extracted from handleChatCore's streaming success path (chatCore god-file decomposition, #3501).
// buildStreamingResponseHeaders is injected so the merge of upstream headers + request-id + the
// optional compression header is observable. Locks: zeroed latency/usage/cost at stream start, the
// x-omniroute-request-id, and the compression header only when meta is present.
import { test } from "node:test";
import assert from "node:assert/strict";
const { assembleStreamingResponseHeaders } = await import(
"../../open-sse/handlers/chatCore/streamingResponseHeaders.ts"
);
function makeBuild() {
const calls: Array<{ headers: unknown; meta: Record<string, unknown> }> = [];
const build = (headers: unknown, meta: Record<string, unknown>) => {
calls.push({ headers, meta });
return { "x-upstream": "kept" };
};
return { build: build as Parameters<typeof assembleStreamingResponseHeaders>[1], calls };
}
function baseArgs(overrides: Record<string, unknown> = {}) {
return {
providerHeaders: new Headers({ "content-type": "text/event-stream" }),
provider: "openai",
model: "gpt-x",
pendingRequestId: "preq-1",
compressionResponseMeta: undefined,
...overrides,
} as Parameters<typeof assembleStreamingResponseHeaders>[0];
}
test("merges upstream headers and sets x-omniroute-request-id", () => {
const { build } = makeBuild();
const h = assembleStreamingResponseHeaders(baseArgs(), build);
assert.equal(h["x-upstream"], "kept");
assert.equal(h["x-omniroute-request-id"], "preq-1");
});
test("buildStreamingResponseHeaders receives zeroed latency/usage/cost and cacheHit false", () => {
const { build, calls } = makeBuild();
assembleStreamingResponseHeaders(baseArgs(), build);
assert.equal(calls.length, 1);
assert.equal(calls[0].meta.cacheHit, false);
assert.equal(calls[0].meta.latencyMs, 0);
assert.equal(calls[0].meta.usage, null);
assert.equal(calls[0].meta.costUsd, 0);
assert.equal(calls[0].meta.provider, "openai");
assert.equal(calls[0].meta.model, "gpt-x");
});
test("no compression meta → no compression header", () => {
const { build } = makeBuild();
const h = assembleStreamingResponseHeaders(baseArgs({ compressionResponseMeta: undefined }), build);
assert.ok(!Object.values(h).includes("engine:z"));
});
test("compression meta present → compression header set", () => {
const { build } = makeBuild();
const h = assembleStreamingResponseHeaders(
baseArgs({ compressionResponseMeta: "engine:z; source=routing" }),
build
);
assert.ok(Object.values(h).includes("engine:z; source=routing"));
});

View File

@@ -1119,6 +1119,13 @@ test("Executor MODEL_MAP: dot-form OmniRoute IDs translate to dash-form ChatGPT
["gpt-5.4-thinking-mini", "gpt-5-4-t-mini"],
["gpt-5.2-thinking", "gpt-5-2-thinking"],
["o3", "o3"],
// Regression #4665: these advertised catalog ids were missing from
// MODEL_MAP and fell through as their dot-form slug verbatim, which the
// ChatGPT backend-api silently rejects → served the default Plus model.
["gpt-5.5", "gpt-5-5"],
["gpt-5.5-pro", "gpt-5-5-pro"],
["gpt-5.4-pro", "gpt-5-4-pro"],
["gpt-5.2-pro", "gpt-5-2-pro"],
];
for (const [omniId, expectedSlug] of cases) {
m.calls.urls.length = 0;
@@ -1141,6 +1148,45 @@ test("Executor MODEL_MAP: dot-form OmniRoute IDs translate to dash-form ChatGPT
}
});
test("MODEL_MAP drift guard: every advertised dot-form catalog id resolves to a dash-form backend slug (no verbatim fall-through) (#4665)", async () => {
reset();
const { getRegistryEntry } = await import("../../open-sse/config/providerRegistry.ts");
const ids = (getRegistryEntry("chatgpt-web")?.models || []).map((m) => m.id);
// Dot-form ids must never reach the ChatGPT backend verbatim — the backend
// only accepts dash-form slugs. (Ids that are already dash-form, e.g.
// "gpt-4-5", legitimately pass through unchanged and are exempt.)
const dotFormIds = ids.filter((id) => id.includes("."));
const m = installMockFetch();
try {
for (const omniId of dotFormIds) {
m.calls.urls.length = 0;
m.calls.bodies.length = 0;
const executor = new ChatGptWebExecutor();
await executor.execute({
model: omniId,
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials: { apiKey: "test" },
signal: AbortSignal.timeout(10_000),
log: null,
});
const convIdx = m.calls.urls.findIndex((u) => u.endsWith("/backend-api/f/conversation"));
const body = JSON.parse(m.calls.bodies[convIdx]);
assert.ok(
!body.model.includes("."),
`${omniId} reached the backend as "${body.model}" (still dot-form) — missing MODEL_MAP entry causes silent model substitution`,
);
assert.notEqual(
body.model,
omniId,
`${omniId} fell through MODEL_MAP verbatim — add a dash-form mapping`,
);
}
} finally {
m.restore();
}
});
// ─── thinking_effort PATCH user_last_used_model_config ─────────────────────
test("thinking_effort: high → PATCH user_last_used_model_config with extended", async () => {

View File

@@ -121,12 +121,13 @@ 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 29 audited modules", () => {
test("INTENTIONALLY_INTERNAL contains the expected 32 audited modules", () => {
const expected = [
"_rowTypes",
"accessTokens",
"apiKeyColumnFallbacks",
"apiKeyUsageLimitFields",
"caseMapping",
"cleanup",
"cliToolState",
"comboForecast",
@@ -141,11 +142,13 @@ test("INTENTIONALLY_INTERNAL contains the expected 29 audited modules", () => {
"migrationRunner",
"notion",
"obsidian",
"optimizationSettings",
"pluginMetrics",
"prompts",
"providerNodeSelect",
"providerStats",
"recovery",
"schemaColumns",
"secrets",
"serviceModels",
"stateReset",

View File

@@ -23,10 +23,9 @@ test("flags a clientId behind a process.env fallback (env || literal)", () => {
});
test("flags clientSecret and apiKey literals too", () => {
const src = [
`clientSecret: "GOCSPX-secret-literal",`,
`apiKey: "AIzaSyLeakedFirebaseKey",`,
].join("\n");
const src = [`clientSecret: "GOCSPX-secret-literal",`, `apiKey: "AIzaSyLeakedFirebaseKey",`].join(
"\n"
);
const v = findLiteralCreds(src, new Set(), "x.ts");
assert.equal(v.length, 2);
});
@@ -41,7 +40,7 @@ test("does NOT flag resolvePublicCredMulti() with literal env-name args", () =>
assert.deepEqual(findLiteralCreds(src, new Set(), "x.ts"), []);
});
test("does NOT flag empty-string fallback (process.env || \"\")", () => {
test('does NOT flag empty-string fallback (process.env || "")', () => {
const src = `clientIdDefault: process.env.GITLAB_OAUTH_CLIENT_ID || "",`;
assert.deepEqual(findLiteralCreds(src, new Set(), "x.ts"), []);
});
@@ -75,10 +74,7 @@ test("a NEW literal is still flagged even with the real frozen allowlist", () =>
});
test("real scanned files produce ZERO violations with the frozen allowlist (gate exits 0)", () => {
const scanned = [
"open-sse/config/providerRegistry.ts",
"src/lib/oauth/constants/oauth.ts",
];
const scanned = ["open-sse/config/providerRegistry.ts", "src/lib/oauth/constants/oauth.ts"];
for (const rel of scanned) {
const src = fs.readFileSync(path.join(repoRoot, rel), "utf8") as string;
const v = findLiteralCreds(src, KNOWN_LITERAL_CREDS, rel);
@@ -91,10 +87,7 @@ test("every frozen literal is actually present in a scanned file (no dead allowl
// per-provider plugins (#3993), so providerRegistry.ts is now a re-export barrel;
// entries keyed by an explicit `file:line:value` are checked against the file named
// in the key (which is where the literal actually lives), not this anchor blob.
const anchorFiles = [
"open-sse/config/providerRegistry.ts",
"src/lib/oauth/constants/oauth.ts",
];
const anchorFiles = ["open-sse/config/providerRegistry.ts", "src/lib/oauth/constants/oauth.ts"];
const anchorBlob = anchorFiles
.map((rel) => fs.readFileSync(path.join(repoRoot, rel), "utf8") as string)
.join("\n");
@@ -107,9 +100,15 @@ test("every frozen literal is actually present in a scanned file (no dead allowl
// match on file content rather than the exact line number.
const file = entry.replace(/:\d+:.*$/, "");
const src = fs.readFileSync(path.join(repoRoot, file), "utf8") as string;
assert.ok(src.includes(value), `frozen literal not found in its source file ${file}: ${value}`);
assert.ok(
src.includes(value),
`frozen literal not found in its source file ${file}: ${value}`
);
} else {
assert.ok(anchorBlob.includes(value), `frozen literal not found in any anchor file: ${value}`);
assert.ok(
anchorBlob.includes(value),
`frozen literal not found in any anchor file: ${value}`
);
}
}
});
@@ -127,8 +126,16 @@ test("with an empty allowlist the real scanned files surface zero violations (al
) as string;
const regViolations = findLiteralCreds(reg, new Set(), "providerRegistry.ts");
const oauthViolations = findLiteralCreds(oauth, new Set(), "oauth.ts");
assert.equal(regViolations.length, 0, `providerRegistry.ts should be clean, got: ${regViolations.join(", ")}`);
assert.equal(oauthViolations.length, 0, `oauth.ts should be clean, got: ${oauthViolations.join(", ")}`);
assert.equal(
regViolations.length,
0,
`providerRegistry.ts should be clean, got: ${regViolations.join(", ")}`
);
assert.equal(
oauthViolations.length,
0,
`oauth.ts should be clean, got: ${oauthViolations.join(", ")}`
);
});
// --- 6A.8: expanded scope (open-sse/** and src/lib/oauth/**) ---
@@ -164,21 +171,33 @@ test("6A.8 stale: known literal that was removed from the codebase is detected a
const liveViolations = findLiteralCreds(src, new Set(), "file.ts");
// The allowlist has an entry "old-literal" but it's not in live violations.
const stale = (reportStale as (a: Set<string>, b: string[], c: string) => string[])(
new Set(["old-literal"]), liveViolations, "check-public-creds"
new Set(["old-literal"]),
liveViolations,
"check-public-creds"
);
assert.deepEqual(stale, ["old-literal"]);
});
test("6A.8: open-sse/services/usage.ts FP — function-signature apiKey is suppressed by allowlist", () => {
// open-sse/services/usage.ts L543: `getMiniMaxUsage(apiKey: string, provider: "minimax" | "minimax-cn")`
// open-sse/services/usage/minimax.ts L213: `getMiniMaxUsage(apiKey: string, provider: "minimax" | "minimax-cn")`
// The CRED_KEY_RE matches `apiKey:` in the TypeScript function-parameter type annotation.
// "minimax" and "minimax-cn" are provider-name strings in the type, NOT credentials.
// Frozen in KNOWN_LITERAL_CREDS as FPs by file:line:value key.
const realSrc = fs.readFileSync(path.join(repoRoot, "open-sse/services/usage.ts"), "utf8") as string;
// Frozen in KNOWN_LITERAL_CREDS as FPs by file:line:value key. The MiniMax family was
// extracted from services/usage.ts into services/usage/minimax.ts (god-file decomposition),
// so the FP moved with the getMiniMaxUsage signature.
const minimaxPath = "open-sse/services/usage/minimax.ts";
const realSrc = fs.readFileSync(path.join(repoRoot, minimaxPath), "utf8") as string;
// With empty allowlist the FP shows up (it IS flagged by the regex).
const vWithEmpty = findLiteralCreds(realSrc, new Set(), "open-sse/services/usage.ts");
assert.ok(vWithEmpty.some((v) => v.includes("minimax")), `expected FP 'minimax' violations with empty allowlist, got: ${vWithEmpty.join(", ")}`);
const vWithEmpty = findLiteralCreds(realSrc, new Set(), minimaxPath);
assert.ok(
vWithEmpty.some((v) => v.includes("minimax")),
`expected FP 'minimax' violations with empty allowlist, got: ${vWithEmpty.join(", ")}`
);
// With KNOWN_LITERAL_CREDS the FPs are suppressed.
const vWithAllowlist = findLiteralCreds(realSrc, KNOWN_LITERAL_CREDS, "open-sse/services/usage.ts");
assert.deepEqual(vWithAllowlist, [], `FP violations should be suppressed, got: ${vWithAllowlist.join(", ")}`);
const vWithAllowlist = findLiteralCreds(realSrc, KNOWN_LITERAL_CREDS, minimaxPath);
assert.deepEqual(
vWithAllowlist,
[],
`FP violations should be suppressed, got: ${vWithAllowlist.join(", ")}`
);
});

View File

@@ -0,0 +1,47 @@
/**
* tests/unit/claude-codex-identity-version-sync.test.ts
*
* Guards the pinned CLI identity versions against drift. The Claude Code version
* lives in FOUR places (claudeIdentity, anthropicHeaders, claudeCodeCompatible,
* ccBridgeTransforms) and MUST stay in lockstep — a partial bump produces an
* inconsistent wire fingerprint. The Codex client version lives in codexClient.
*
* When you capture a newer claude-cli / codex release, bump ALL constants and
* update the pinned values below in the same change.
*/
import test from "node:test";
import assert from "node:assert/strict";
const id = await import("../../open-sse/executors/claudeIdentity.ts");
const hdr = await import("../../open-sse/config/anthropicHeaders.ts");
const compat = await import("../../open-sse/services/claudeCodeCompatible.ts");
const bridge = await import("../../open-sse/services/ccBridgeTransforms.ts");
const codexCfg = await import("../../open-sse/config/codexClient.ts");
test("Claude CLI version constants are in lockstep across all 4 sources", () => {
const V = id.CLAUDE_CODE_VERSION;
assert.equal(hdr.CLAUDE_CLI_VERSION, V, "anthropicHeaders.CLAUDE_CLI_VERSION drift");
assert.equal(compat.CLAUDE_CODE_COMPATIBLE_VERSION, V, "claudeCodeCompatible version drift");
assert.equal(bridge.DEFAULT_CLAUDE_CODE_VERSION, V, "ccBridgeTransforms version drift");
assert.equal(
hdr.CLAUDE_CLI_USER_AGENT,
`claude-cli/${V} (external, cli)`,
"CLAUDE_CLI_USER_AGENT drift"
);
assert.equal(
compat.CLAUDE_CODE_COMPATIBLE_USER_AGENT,
`claude-cli/${V} (external, sdk-cli)`,
"CLAUDE_CODE_COMPATIBLE_USER_AGENT drift"
);
});
test("Claude CLI is pinned to the captured 2.1.187 release", () => {
assert.equal(id.CLAUDE_CODE_VERSION, "2.1.187");
});
test("Codex client is pinned to the captured 0.142.0 release", () => {
assert.equal(codexCfg.getCodexClientVersion(), "0.142.0");
assert.equal(codexCfg.getCodexUserAgent(), "codex-cli/0.142.0 (Windows 10.0.26200; x64)");
assert.equal(codexCfg.getCodexDefaultHeaders().Version, "0.142.0");
});

View File

@@ -0,0 +1,99 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { normalizeClaudeHaikuConstraints } from "@omniroute/open-sse/services/claudeHaikuConstraints.ts";
// Claude Haiku 4.5 (and other haiku-tier models) reject:
// 1. `thinking.type:"adaptive"` (only Sonnet/Opus support adaptive thinking)
// 2. `output_config.effort` (also Sonnet/Opus only)
// Several OmniRoute paths can still emit these on Haiku targets:
// - Native Claude passthrough from newer Claude Code / Cowork clients
// - OpenAI→Claude translator buckets when reasoning_effort is "max"/"xhigh"
// - Per-model thinking defaults from the request flow
// This normalizer is the final, provider-agnostic guard keyed on the resolved
// upstream model. Mirrors upstream 9router 401d93bd5 (`claude.js`).
describe("normalizeClaudeHaikuConstraints", () => {
it("converts thinking.type:'adaptive' to a manual 'enabled' shape on haiku", () => {
const input = {
model: "claude-haiku-4-5-20251001",
thinking: { type: "adaptive" },
};
const out = normalizeClaudeHaikuConstraints(input, input.model);
assert.deepEqual(out.thinking, { type: "enabled", budget_tokens: 10000 });
});
it("strips output_config.effort on haiku and keeps other output_config fields", () => {
const input = {
model: "claude-haiku-4.5",
output_config: { effort: "high", other_field: "keep-me" },
};
const out = normalizeClaudeHaikuConstraints(input, input.model);
assert.deepEqual(out.output_config, { other_field: "keep-me" });
});
it("removes output_config entirely when stripping effort leaves it empty", () => {
const input = {
model: "claude-haiku-4-5-20251001",
output_config: { effort: "max" },
};
const out = normalizeClaudeHaikuConstraints(input, input.model);
assert.equal((out as Record<string, unknown>).output_config, undefined);
});
it("applies BOTH transforms when the body carries adaptive thinking + effort", () => {
const input = {
model: "claude-haiku-4.5",
thinking: { type: "adaptive" },
output_config: { effort: "xhigh" },
};
const out = normalizeClaudeHaikuConstraints(input, input.model);
assert.deepEqual(out.thinking, { type: "enabled", budget_tokens: 10000 });
assert.equal((out as Record<string, unknown>).output_config, undefined);
});
it("is a no-op for Sonnet (adaptive is valid there)", () => {
const input = {
model: "claude-sonnet-4-5",
thinking: { type: "adaptive" },
output_config: { effort: "high" },
};
const out = normalizeClaudeHaikuConstraints(input, input.model);
assert.strictEqual(out, input);
});
it("is a no-op for Opus (adaptive is valid there)", () => {
const input = {
model: "claude-opus-4-7",
thinking: { type: "adaptive" },
output_config: { effort: "max" },
};
const out = normalizeClaudeHaikuConstraints(input, input.model);
assert.strictEqual(out, input);
});
it("leaves haiku bodies that have no adaptive thinking or effort untouched", () => {
const input = {
model: "claude-haiku-4-5-20251001",
thinking: { type: "enabled", budget_tokens: 4096 },
messages: [{ role: "user", content: "hi" }],
};
const out = normalizeClaudeHaikuConstraints(input, input.model);
assert.strictEqual(out, input);
});
it("returns the body untouched when model is null/undefined", () => {
const input = { thinking: { type: "adaptive" }, output_config: { effort: "high" } };
assert.strictEqual(normalizeClaudeHaikuConstraints(input, null), input);
assert.strictEqual(normalizeClaudeHaikuConstraints(input, undefined), input);
});
it("matches case-insensitively (any model id containing 'haiku')", () => {
const input = {
model: "claude-3-5-Haiku-latest",
thinking: { type: "adaptive" },
};
const out = normalizeClaudeHaikuConstraints(input, input.model);
assert.deepEqual(out.thinking, { type: "enabled", budget_tokens: 10000 });
});
});

View File

@@ -13,6 +13,7 @@ import assert from "node:assert/strict";
import {
cloakThirdPartyToolNames,
needsThirdPartyCloak,
isAnthropicServerToolType,
} from "../../open-sse/services/claudeCodeToolRemapper.ts";
import {
sanitizeClaudeToolSchema,
@@ -107,6 +108,48 @@ describe("cloakThirdPartyToolNames", () => {
assert.equal(needsThirdPartyCloak("read_file"), true);
assert.equal(needsThirdPartyCloak("mixture_of_agents"), true);
});
it("needsThirdPartyCloak leaves mcp__ namespace untouched (#4861)", () => {
// Genuine Claude Code MCP names Anthropic accepts natively; cloaking them
// caused round-trip "Tool reference 'mcp__…' not found" 400s on claude OAuth.
assert.equal(needsThirdPartyCloak("mcp__filesystem__read_file"), false);
assert.equal(needsThirdPartyCloak("mcp__github__create_issue"), false);
assert.equal(needsThirdPartyCloak("mcp__server"), false);
});
it("preserves the reserved name of a versioned Anthropic server tool", () => {
const body: AnyRecord = {
tools: [{ type: "web_search_20250305", name: "web_search", max_uses: 5 }],
};
cloakThirdPartyToolNames(body);
// Anthropic requires tools.N.web_search_20250305.name === "web_search".
assert.equal((body.tools as AnyRecord[])[0].name, "web_search");
// No reverse-map entry needed because nothing was cloaked.
assert.equal((body._toolNameMap as Map<string, string> | undefined)?.has("WebSearch") ?? false, false);
});
it("still cloaks a genuine third-party tool sitting next to a server tool", () => {
const body: AnyRecord = {
tools: [
{ type: "web_search_20250305", name: "web_search" },
{ name: "mixture_of_agents" },
],
};
cloakThirdPartyToolNames(body);
assert.equal((body.tools as AnyRecord[])[0].name, "web_search");
assert.equal((body.tools as AnyRecord[])[1].name, "MixtureOfAgents");
});
it("isAnthropicServerToolType detects versioned + non-versioned server tools", () => {
assert.equal(isAnthropicServerToolType("web_search_20250305"), true);
assert.equal(isAnthropicServerToolType("code_execution_20250522"), true);
assert.equal(isAnthropicServerToolType("web_search"), true);
assert.equal(isAnthropicServerToolType("web_search_preview"), true);
// Not server tools — must remain cloakable.
assert.equal(isAnthropicServerToolType("mixture_of_agents"), false);
assert.equal(isAnthropicServerToolType("Bash"), false);
assert.equal(isAnthropicServerToolType(undefined), false);
});
});
describe("sanitizeClaudeToolSchemas — boolean schema preservation", () => {

View File

@@ -0,0 +1,66 @@
import { describe, it, beforeEach } from "node:test";
import { strict as assert } from "node:assert";
import {
OAUTH_USAGE_429_COOLDOWN_MS,
_resetClaudeOauthUsageCooldown,
isClaudeOauthUsageCoolingDown,
markClaudeOauthUsage429,
} from "../../open-sse/services/claudeUsageCooldown.ts";
describe("claude OAuth usage 429 backoff", () => {
beforeEach(() => {
_resetClaudeOauthUsageCooldown();
});
it("starts uncooled for any token", () => {
assert.equal(isClaudeOauthUsageCoolingDown("tok-a"), false);
assert.equal(isClaudeOauthUsageCoolingDown(undefined), false);
});
it("treats a single 429 as entering cooldown for that token only", () => {
const t0 = 1_000_000;
markClaudeOauthUsage429("tok-a", t0);
assert.equal(isClaudeOauthUsageCoolingDown("tok-a", t0 + 1), true);
// A different token is unaffected — cooldown is per-token, not global.
assert.equal(isClaudeOauthUsageCoolingDown("tok-b", t0 + 1), false);
});
it("repeated 429s on the same token do NOT spam: stays cooling once entered", () => {
const t0 = 2_000_000;
markClaudeOauthUsage429("tok-a", t0);
// Simulate the scheduler hitting 429 again 10s, 30s, 60s later
for (const dt of [10_000, 30_000, 60_000]) {
markClaudeOauthUsage429("tok-a", t0 + dt);
assert.equal(
isClaudeOauthUsageCoolingDown("tok-a", t0 + dt),
true,
`still cooling at +${dt}ms`
);
}
});
it("cooldown expires after OAUTH_USAGE_429_COOLDOWN_MS and the token becomes eligible again", () => {
const t0 = 3_000_000;
markClaudeOauthUsage429("tok-a", t0);
assert.equal(
isClaudeOauthUsageCoolingDown("tok-a", t0 + OAUTH_USAGE_429_COOLDOWN_MS - 1),
true
);
assert.equal(
isClaudeOauthUsageCoolingDown("tok-a", t0 + OAUTH_USAGE_429_COOLDOWN_MS + 1),
false,
"should be eligible again after cooldown window"
);
});
it("undefined/missing access token is a no-op (does not poison the map)", () => {
const t0 = 4_000_000;
markClaudeOauthUsage429(undefined, t0);
assert.equal(isClaudeOauthUsageCoolingDown(undefined, t0 + 1), false);
});
it("cooldown defaults to 3 minutes (matches upstream OAUTH_429_COOLDOWN_MS)", () => {
assert.equal(OAUTH_USAGE_429_COOLDOWN_MS, 180_000);
});
});

View File

@@ -0,0 +1,127 @@
/**
* Regression test for the JSONC-tolerant config reader used by every
* cli-tools settings route.
*
* Ported from upstream `decolua/9router@6c10edf8`:
* "fix(cli-tools): tolerate JSONC configs in CLI tool settings routes".
*
* Before the fix, `readSettings`/`readConfig` helpers only caught `ENOENT` and
* re-threw on any other error — so a config file with a single trailing
* comma (valid JSONC, emitted by tools like opencode) crashed the GET route
* with a 500 and the dashboard misread the response as "tool not installed".
*
* After the fix:
* - trailing commas are stripped before parsing (JSONC tolerated);
* - any other parse error returns `null` (or the caller's fallback) so the
* dashboard renders "installed but not configured".
*/
import test from "node:test";
import assert from "node:assert/strict";
import { promises as fs } from "node:fs";
import os from "node:os";
import path from "node:path";
const { parseJsoncOrNull, readJsoncConfig } = await import(
"../../src/app/api/cli-tools/_lib/jsoncConfig.ts"
);
test("parseJsoncOrNull tolerates trailing commas in objects", () => {
const jsonc = `{
"model": "gpt-5",
"tools": ["a", "b",],
"nested": { "x": 1, },
}`;
const parsed = parseJsoncOrNull<{ model: string; tools: string[] }>(jsonc);
assert.ok(parsed, "parser must accept JSONC with trailing commas");
assert.equal(parsed.model, "gpt-5");
assert.deepEqual(parsed.tools, ["a", "b"]);
});
test("parseJsoncOrNull returns null on truly malformed JSON", () => {
assert.equal(parseJsoncOrNull("{ not json at all"), null);
});
test("readJsoncConfig returns fallback when file is missing", async () => {
const missing = path.join(os.tmpdir(), `cli-tools-jsonc-missing-${Date.now()}.json`);
assert.equal(await readJsoncConfig(missing), null);
assert.deepEqual(await readJsoncConfig(missing, {}), {});
});
test("readJsoncConfig parses a JSONC file with trailing commas (regression)", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "cli-tools-jsonc-"));
const file = path.join(dir, "settings.json");
await fs.writeFile(
file,
`{
"apiKey": "sk-test",
"model": "claude-sonnet-4-5",
}
`,
"utf-8"
);
try {
const parsed = await readJsoncConfig<{ apiKey: string; model: string }>(file);
assert.ok(parsed, "JSONC file with trailing comma must NOT crash the reader");
assert.equal(parsed.apiKey, "sk-test");
assert.equal(parsed.model, "claude-sonnet-4-5");
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
});
test("readJsoncConfig returns fallback on corrupted config instead of throwing", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "cli-tools-jsonc-bad-"));
const file = path.join(dir, "settings.json");
await fs.writeFile(file, "{ this is not valid json at all !!! ", "utf-8");
try {
// Must NOT throw — the dashboard renders "installed but not configured"
// when this returns null, instead of "not installed" on a 500.
assert.equal(await readJsoncConfig(file), null);
assert.deepEqual(await readJsoncConfig(file, {}), {});
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
});
/**
* Source-guard: every cli-tools settings route's read helper must go through
* the JSONC-tolerant reader. A regression to raw `JSON.parse(content)` inside
* a `readSettings` / `readConfig` helper would re-introduce the original
* 500-on-JSONC bug, so we assert the routes do not contain that pattern.
*
* Same source-guard pattern as `tests/unit/source-guard-*.test.ts`.
*/
test("cli-tools settings routes use the JSONC-tolerant reader (source-guard)", async () => {
// For each route, look for the GET-read helper region (top of file, up to
// the first `export async function GET`) and assert it imports + uses the
// JSONC-tolerant reader instead of raw `JSON.parse(content)`.
const routes = [
"claude-settings",
"cline-settings",
"droid-settings",
"kilo-settings",
"openclaw-settings",
];
const repoRoot = path.resolve(import.meta.dirname ?? ".", "..", "..");
for (const r of routes) {
const src = await fs.readFile(
path.join(repoRoot, "src", "app", "api", "cli-tools", r, "route.ts"),
"utf-8"
);
const getIdx = src.indexOf("export async function GET");
assert.ok(getIdx > 0, `${r}: expected an exported GET handler`);
const head = src.slice(0, getIdx);
assert.ok(
/from\s+["']\.\.\/_lib\/jsoncConfig["']/.test(src),
`${r}: must import readJsoncConfig from ../_lib/jsoncConfig`
);
assert.ok(
!/JSON\.parse\(\s*content\s*\)/.test(head),
`${r}: read helper still calls raw JSON.parse(content) — port the JSONC fix`
);
assert.ok(
/readJsoncConfig\s*[<(]/.test(head),
`${r}: read helper must invoke readJsoncConfig`
);
}
});

View File

@@ -87,12 +87,12 @@ test("CLI fingerprint preserves Codex executor User-Agent and maps legacy Copilo
"codex",
{
Authorization: "Bearer token",
"User-Agent": "codex-cli/0.132.0 (Windows 10.0.26200; x64)",
"User-Agent": "codex-cli/0.142.0 (Windows 10.0.26200; x64)",
},
{ model: "gpt-5.5", messages: [], stream: true }
);
assert.equal(codex.headers["User-Agent"], "codex-cli/0.132.0 (Windows 10.0.26200; x64)");
assert.equal(codex.headers["User-Agent"], "codex-cli/0.142.0 (Windows 10.0.26200; x64)");
assert.deepEqual(Object.keys(JSON.parse(codex.bodyString)), ["model", "stream", "messages"]);
const copilot = applyFingerprint(

View File

@@ -0,0 +1,75 @@
// Regression coverage for the systray2 runtime swap (port of upstream PR #1080).
// The bug upstream: the bundled Go binary in node_modules/systray2/traybin
// sometimes ships without the executable bit on macOS/Linux, causing the tray
// to spawn() with EACCES and the icon never to appear.
//
// We can't load the real native binary in unit tests, but we can verify the
// pure helpers that drive the swap: the package + version pin and the
// chmod helper's behavior on a synthetic binary tree.
import test from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, statSync, chmodSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
SYSTRAY_PACKAGE,
SYSTRAY_VERSION,
chmodSystrayBinAt,
resolveSystrayBinName,
} from "../../bin/cli/runtime/trayRuntime.ts";
test("systray2 is pinned to a 2.x version (PR #1080 fix)", () => {
assert.equal(SYSTRAY_PACKAGE, "systray2");
// The original systray@1.0.5 binary is incompatible with modern macOS dyld;
// upstream PR #1080 moved to the systray2 2.x line. Pin must stay ≥ 2.x.
assert.match(SYSTRAY_VERSION, /^2\./, `expected systray2@2.x, got ${SYSTRAY_VERSION}`);
});
test("resolveSystrayBinName returns null on win32 and a *_release name elsewhere", () => {
assert.equal(resolveSystrayBinName("win32"), null);
assert.equal(resolveSystrayBinName("darwin"), "tray_darwin_release");
assert.equal(resolveSystrayBinName("linux"), "tray_linux_release");
});
test("chmodSystrayBinAt sets +x on the bundled tray binary when present", () => {
const root = mkdtempSync(join(tmpdir(), "omniroute-systray-bin-"));
try {
const platform = process.platform === "win32" ? "linux" : process.platform;
const binName = resolveSystrayBinName(platform)!;
const binDir = join(root, "node_modules", "systray2", "traybin");
mkdirSync(binDir, { recursive: true });
const binPath = join(binDir, binName);
writeFileSync(binPath, "#!/bin/sh\nexit 0\n", { mode: 0o644 });
chmodSync(binPath, 0o644); // explicit: not executable
const result = chmodSystrayBinAt(root, platform);
assert.equal(result.changed, true, "expected chmodSystrayBinAt to chmod the bin");
// 0o111 covers the three execute bits — assert at least one is set.
const mode = statSync(binPath).mode & 0o777;
assert.ok((mode & 0o111) !== 0, `expected exec bits on bin, got mode=${mode.toString(8)}`);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("chmodSystrayBinAt is a no-op when the binary doesn't exist", () => {
const root = mkdtempSync(join(tmpdir(), "omniroute-systray-bin-"));
try {
const result = chmodSystrayBinAt(root, "linux");
assert.equal(result.changed, false);
assert.equal(result.reason, "missing");
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("chmodSystrayBinAt skips win32 (uses PowerShell tray, no Go binary)", () => {
const root = mkdtempSync(join(tmpdir(), "omniroute-systray-bin-"));
try {
const result = chmodSystrayBinAt(root, "win32");
assert.equal(result.changed, false);
assert.equal(result.reason, "win32-skip");
} finally {
rmSync(root, { recursive: true, force: true });
}
});

View File

@@ -0,0 +1,262 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
AI_PROVIDERS,
USAGE_SUPPORTED_PROVIDERS,
FREE_APIKEY_PROVIDER_IDS,
} from "../../src/shared/constants/providers.ts";
import { REGISTRY } from "../../open-sse/config/providerRegistry.ts";
import { getExecutor } from "../../open-sse/executors/index.ts";
import { CodeBuddyCnExecutor } from "../../open-sse/executors/codebuddy-cn.ts";
import {
PROVIDERS as OAUTH_PROVIDER_IDS,
CODEBUDDY_CN_CONFIG,
} from "../../src/lib/oauth/constants/oauth.ts";
import PROVIDERS_MAP from "../../src/lib/oauth/providers/index.ts";
import { supportsTokenRefresh } from "../../open-sse/services/tokenRefresh.ts";
test("codebuddy-cn is registered as an OAuth provider in the UI catalog", () => {
const p = AI_PROVIDERS["codebuddy-cn"];
assert.ok(p, "AI_PROVIDERS['codebuddy-cn'] must exist");
assert.equal(p.id, "codebuddy-cn");
assert.equal(p.alias, "cbcn");
assert.equal(p.name, "CodeBuddy CN");
});
test("codebuddy-cn registry entry has expected shape", () => {
const r = REGISTRY["codebuddy-cn"];
assert.ok(r, "REGISTRY['codebuddy-cn'] must exist");
assert.equal(r.alias, "cbcn");
assert.equal(r.executor, "codebuddy-cn");
assert.equal(r.baseUrl, "https://copilot.tencent.com/v2/chat/completions");
assert.equal(r.authHeader, "bearer");
// Headers carry the Tencent CodeBuddy CLI fingerprint
assert.equal(r.headers?.["X-Product"], "SaaS");
assert.equal(r.headers?.["X-IDE-Type"], "CLI");
assert.equal(r.headers?.["X-IDE-Name"], "CLI");
assert.equal(r.headers?.["x-requested-with"], "XMLHttpRequest");
assert.equal(r.headers?.["x-codebuddy-request"], "1");
// 15 models from the upstream catalog
const ids = r.models.map((m) => m.id);
for (const expected of [
"glm-5.2",
"glm-5.1",
"glm-5.0",
"glm-5.0-turbo",
"glm-5v-turbo",
"glm-4.7",
"minimax-m3",
"minimax-m2.7",
"kimi-k2.7",
"kimi-k2.6",
"kimi-k2.5",
"hy3-preview",
"deepseek-v4-pro",
"deepseek-v4-flash",
"deepseek-v3-2-volc",
]) {
assert.ok(ids.includes(expected), `model ${expected} must be registered`);
}
assert.equal(r.models.length, 15);
});
test("codebuddy-cn glm-5.2 carries 1M context and openai-style reasoning that cannot be disabled", () => {
const r = REGISTRY["codebuddy-cn"];
const m = r.models.find((x) => x.id === "glm-5.2");
assert.ok(m, "glm-5.2 must exist");
// contextLength is the OmniRoute analogue of upstream's contextWindow
assert.equal(m.contextLength, 1000000);
assert.equal(m.supportsReasoning, true);
});
test("codebuddy-cn vision flag is set on the visual models", () => {
const r = REGISTRY["codebuddy-cn"];
for (const id of ["glm-5v-turbo", "minimax-m3", "kimi-k2.7", "deepseek-v4-pro"]) {
const m = r.models.find((x) => x.id === id);
assert.ok(m, `${id} must exist`);
assert.equal(m.supportsVision, true, `${id} should have vision`);
}
});
test("getExecutor returns the CodeBuddyCnExecutor for 'codebuddy-cn' and the 'cbcn' alias", () => {
const e = getExecutor("codebuddy-cn");
assert.ok(e instanceof CodeBuddyCnExecutor, "executor must be CodeBuddyCnExecutor");
const aliasExec = getExecutor("cbcn");
assert.ok(aliasExec instanceof CodeBuddyCnExecutor, "alias 'cbcn' must resolve to same executor");
});
test("CodeBuddyCnExecutor.transformRequest forces stream:true and adds OpenAI-style reasoning_summary", () => {
const e = new CodeBuddyCnExecutor();
const out = e.transformRequest(
"glm-5.2",
{ model: "glm-5.2", messages: [{ role: "user", content: "hi" }], stream: false },
false,
{} as unknown as Parameters<typeof e.transformRequest>[3]
);
assert.ok(out && typeof out === "object" && !Array.isArray(out));
const body = out as Record<string, unknown>;
assert.equal(body.stream, true, "stream must be forced to true");
assert.equal(body.reasoning_effort, "medium", "default reasoning_effort must be 'medium'");
assert.equal(body.reasoning_summary, "auto", "reasoning_summary must be 'auto'");
});
test("CodeBuddyCnExecutor preserves explicit reasoning_effort", () => {
const e = new CodeBuddyCnExecutor();
const out = e.transformRequest(
"glm-5.2",
{ model: "glm-5.2", messages: [], reasoning_effort: "high" },
true,
{} as unknown as Parameters<typeof e.transformRequest>[3]
);
const body = out as Record<string, unknown>;
assert.equal(body.reasoning_effort, "high");
assert.equal(body.reasoning_summary, "auto");
});
test("CodeBuddyCnExecutor strips reasoning_effort when caller asks for none/off", () => {
const e = new CodeBuddyCnExecutor();
for (const effort of ["none", "off"]) {
const out = e.transformRequest(
"glm-5.2",
{ model: "glm-5.2", messages: [], reasoning_effort: effort },
true,
{} as unknown as Parameters<typeof e.transformRequest>[3]
);
const body = out as Record<string, unknown>;
assert.equal(
Object.prototype.hasOwnProperty.call(body, "reasoning_effort"),
false,
`reasoning_effort must be omitted for ${effort}`
);
assert.notEqual(body.reasoning_summary, "auto", `reasoning_summary must not be auto for ${effort}`);
}
});
test("codebuddy-cn OAuth provider is wired with device_code flow and GET-poll on state", async () => {
assert.equal(OAUTH_PROVIDER_IDS.CODEBUDDY_CN, "codebuddy-cn");
const map = (PROVIDERS_MAP as Record<string, any>);
const cb = map["codebuddy-cn"];
assert.ok(cb, "PROVIDERS map must include 'codebuddy-cn'");
assert.equal(cb.flowType, "device_code");
assert.equal(typeof cb.requestDeviceCode, "function");
assert.equal(typeof cb.pollToken, "function");
// Drive pollToken with a stubbed fetch and assert it hits the token URL via GET
// with the state as a query param (upstream's distinguishing detail).
const origFetch = globalThis.fetch;
const calls: { url: string; init?: RequestInit }[] = [];
globalThis.fetch = async (url: any, init?: RequestInit) => {
calls.push({ url: String(url), init });
return new Response(
JSON.stringify({
code: 0,
data: {
accessToken: "AT",
refreshToken: "RT",
tokenType: "Bearer",
expiresIn: 3600,
},
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
) as unknown as Response;
};
try {
const result = await cb.pollToken(CODEBUDDY_CN_CONFIG, "STATE-123");
assert.ok(result?.ok, "successful poll should be ok:true");
assert.equal(result?.data?.access_token, "AT");
const got = calls.at(-1);
assert.ok(got, "fetch must have been called");
assert.equal(got.init?.method, "GET", "poll method must be GET");
assert.match(got.url, /\?state=STATE-123$/, "state must appear as ?state= query param");
} finally {
globalThis.fetch = origFetch;
}
});
test("codebuddy-cn token refresh handler is wired in tokenRefresh.ts", () => {
assert.equal(supportsTokenRefresh("codebuddy-cn"), true);
});
test("codebuddy-cn is in USAGE_SUPPORTED_PROVIDERS and quota handler parses Tencent accounts", async () => {
assert.ok(USAGE_SUPPORTED_PROVIDERS.includes("codebuddy-cn"));
const { getCodeBuddyCnUsage } = await import(
"../../open-sse/services/usage/codebuddy-cn.ts"
);
const origFetch = globalThis.fetch;
// Compose a mixed payload: one refill (CycleEndTime << DeductionEndTime) and
// two bonus packs (CycleEndTime == DeductionEndTime), out of expiry order.
const cycleEnd = "2026-08-01T00:00:00Z";
const deductionEnd = 32503680000; // very far future (seconds)
const bonus1End = "2026-07-01T00:00:00Z";
const bonus2End = "2026-06-25T00:00:00Z";
globalThis.fetch = async () => {
return new Response(
JSON.stringify({
code: 0,
data: {
Response: {
Data: {
Accounts: [
{
PackageName: "Refill Pack",
CycleStartTime: "2026-07-01T00:00:00Z",
CycleEndTime: cycleEnd,
DeductionEndTime: deductionEnd,
CycleCapacitySize: 500,
CycleCapacityUsed: 12.34,
},
{
PackageName: "Bonus A",
CycleStartTime: "2026-05-01T00:00:00Z",
CycleEndTime: bonus1End,
DeductionEndTime: new Date(bonus1End).getTime() / 1000,
CapacitySize: 50,
CapacityUsed: 1,
},
{
PackageName: "Bonus B",
CycleStartTime: "2026-05-01T00:00:00Z",
CycleEndTime: bonus2End,
DeductionEndTime: new Date(bonus2End).getTime() / 1000,
CapacitySize: 25,
CapacityUsed: 5,
},
],
},
},
},
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
) as unknown as Response;
};
try {
const result = await getCodeBuddyCnUsage("ACCESS_TOKEN", undefined, undefined);
assert.ok(result && typeof result === "object");
const r = result as Record<string, any>;
assert.equal(r.plan, "Refill Pack");
assert.ok(r.quotas);
// Refill (Monthly) uses CycleCapacity*; Bonus packs use plain Capacity*; soonest-expiring first.
assert.ok(r.quotas.Monthly, "Monthly refill quota must be present");
assert.equal(r.quotas.Monthly.total, 500);
assert.equal(r.quotas.Monthly.used, 12.34);
assert.ok(r.quotas["Bonus Pack 1"], "first bonus must be present");
assert.ok(r.quotas["Bonus Pack 2"], "second bonus must be present");
// Soonest-expiring first: Bonus B (Jun 25) before Bonus A (Jul 1).
assert.equal(r.quotas["Bonus Pack 1"].total, 25, "Bonus Pack 1 should be the soonest (B)");
assert.equal(r.quotas["Bonus Pack 2"].total, 50, "Bonus Pack 2 should be the later (A)");
} finally {
globalThis.fetch = origFetch;
}
});
test("codebuddy-cn is treated as a managed dual-auth provider (oauth + apikey accepted by POST /api/providers)", async () => {
// The provider creation gate trusts FREE_APIKEY_PROVIDER_IDS to admit
// OAuth-category providers that also accept a direct API key (like qoder).
assert.ok(
FREE_APIKEY_PROVIDER_IDS.has("codebuddy-cn"),
"codebuddy-cn must be admitted by the dual-auth gate"
);
});

View File

@@ -0,0 +1,51 @@
// Regression guard for #4715: the Codex HTTP transport forwards the upstream SSE
// stream verbatim, including the non-standard `event: codex.rate_limits` frame
// (no `data:` line). That frame breaks the OpenAI SDK's responses.stream() with
// HTTP 502 "Controller is already closed". filterNonstandardCodexSse() strips
// every `codex.*` event block from the byte stream while preserving standard ones.
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { filterNonstandardCodexSse } from "../../open-sse/executors/codex.ts";
function sseResponse(body: string): Response {
return new Response(body, {
status: 200,
headers: { "content-type": "text/event-stream" },
});
}
async function readAll(res: Response): Promise<string> {
return await res.text();
}
describe("filterNonstandardCodexSse (#4715)", () => {
it("drops codex.* event blocks but keeps standard response.* events", async () => {
const stream =
"event: response.created\ndata: {\"type\":\"response.created\"}\n\n" +
"event: codex.rate_limits\n\n" +
"event: response.output_text.delta\ndata: {\"delta\":\"hi\"}\n\n" +
"event: response.completed\ndata: {\"type\":\"response.completed\"}\n\n";
const out = await readAll(filterNonstandardCodexSse(sseResponse(stream)));
assert.ok(!out.includes("codex.rate_limits"), "codex.* frame must be stripped");
assert.ok(out.includes("response.created"), "standard events preserved");
assert.ok(out.includes("response.output_text.delta"), "standard delta preserved");
assert.ok(out.includes("response.completed"), "terminal event preserved");
});
it("passes through non-SSE responses untouched", async () => {
const json = new Response("{\"ok\":true}", {
status: 200,
headers: { "content-type": "application/json" },
});
const out = filterNonstandardCodexSse(json);
assert.equal(await out.text(), "{\"ok\":true}");
});
it("drops a trailing codex.* block with no double-newline terminator (flush path)", async () => {
const stream =
"event: response.created\ndata: {}\n\n" + "event: codex.token_count\ndata: {}";
const out = await readAll(filterNonstandardCodexSse(sseResponse(stream)));
assert.ok(out.includes("response.created"));
assert.ok(!out.includes("codex.token_count"));
});
});

View File

@@ -0,0 +1,271 @@
/**
* TDD — shouldWaitForComboCooldown (pure decision helper for the quota-share
* combo cooldown-aware retry, Variante A).
*
* The helper decides whether a quota-share (qtSd/) combo that would otherwise
* crystallize a 429 `model_cooldown` should instead WAIT for a short transient
* cooldown and re-dispatch. It is pure (no I/O, no clock) so every branch is
* exhaustively covered here:
* - happy path (short transient cooldown within budget → wait)
* - reason === "quota_exhausted" EXCLUDED (would otherwise wait until midnight)
* - reason in the extra non-retryable set (auth_error/not_found/...) EXCLUDED
* - waitMs above the configured ceiling → no wait
* - waitMs <= 0 → no wait
* - attempt >= maxAttempts → no wait
* - budget insufficient (budgetLeftMs < waitMs) → no wait
* - settings.enabled === false → no wait
*
* Security note (auth.ts:533 isRetryableModelLockoutReason): `quota_exhausted`
* is classified RETRYABLE there, so the helper MUST exclude it explicitly or a
* combo would wait until midnight. This file pins that exclusion.
*/
import test from "node:test";
import assert from "node:assert/strict";
const { shouldWaitForComboCooldown, resolveComboCooldownWaitDecision, COMBO_COOLDOWN_WAIT_MARGIN_MS } =
await import("../../open-sse/services/combo/comboCooldownRetry.ts");
function baseSettings(overrides: Partial<Record<string, unknown>> = {}) {
return {
enabled: true,
maxWaitMs: 5000,
maxAttempts: 2,
budgetMs: 8000,
...overrides,
};
}
function baseInput(overrides: Record<string, unknown> = {}) {
return {
reason: "rate_limit",
waitMs: 3000,
attempt: 0,
budgetLeftMs: 8000,
settings: baseSettings(),
...overrides,
};
}
test("happy path: short transient cooldown within budget → wait=true", () => {
const r = shouldWaitForComboCooldown(baseInput() as never);
assert.equal(r.wait, true);
assert.equal(r.waitMs, 3000);
});
test("reason quota_exhausted is EXCLUDED even though it is otherwise 'retryable'", () => {
const r = shouldWaitForComboCooldown(baseInput({ reason: "quota_exhausted" }) as never);
assert.equal(r.wait, false);
});
test("reason auth_error is EXCLUDED (non-retryable safety set)", () => {
const r = shouldWaitForComboCooldown(baseInput({ reason: "auth_error" }) as never);
assert.equal(r.wait, false);
});
test("reason not_found is EXCLUDED", () => {
const r = shouldWaitForComboCooldown(baseInput({ reason: "not_found" }) as never);
assert.equal(r.wait, false);
});
test("reason not_found_local is EXCLUDED", () => {
const r = shouldWaitForComboCooldown(baseInput({ reason: "not_found_local" }) as never);
assert.equal(r.wait, false);
});
test("missing/unknown reason (null) → no wait (only an explicit transient reason qualifies)", () => {
const r = shouldWaitForComboCooldown(baseInput({ reason: null }) as never);
assert.equal(r.wait, false);
});
test("waitMs above the configured ceiling → no wait", () => {
const r = shouldWaitForComboCooldown(
baseInput({ waitMs: 5001, settings: baseSettings({ maxWaitMs: 5000 }) }) as never
);
assert.equal(r.wait, false);
// waitMs is still surfaced for logging even when we decline to wait.
assert.equal(r.waitMs, 5001);
});
test("waitMs exactly at the ceiling → wait (inclusive bound)", () => {
const r = shouldWaitForComboCooldown(
baseInput({ waitMs: 5000, settings: baseSettings({ maxWaitMs: 5000 }) }) as never
);
assert.equal(r.wait, true);
});
test("waitMs <= 0 → no wait", () => {
assert.equal(shouldWaitForComboCooldown(baseInput({ waitMs: 0 }) as never).wait, false);
assert.equal(shouldWaitForComboCooldown(baseInput({ waitMs: -10 }) as never).wait, false);
});
test("attempt >= maxAttempts → no wait", () => {
const r = shouldWaitForComboCooldown(
baseInput({ attempt: 2, settings: baseSettings({ maxAttempts: 2 }) }) as never
);
assert.equal(r.wait, false);
});
test("attempt below maxAttempts (last allowed) → wait", () => {
const r = shouldWaitForComboCooldown(
baseInput({ attempt: 1, settings: baseSettings({ maxAttempts: 2 }) }) as never
);
assert.equal(r.wait, true);
});
test("budget insufficient (budgetLeftMs < waitMs) → no wait", () => {
const r = shouldWaitForComboCooldown(baseInput({ waitMs: 3000, budgetLeftMs: 2999 }) as never);
assert.equal(r.wait, false);
});
test("budget exactly equal to waitMs → wait (inclusive bound)", () => {
const r = shouldWaitForComboCooldown(baseInput({ waitMs: 3000, budgetLeftMs: 3000 }) as never);
assert.equal(r.wait, true);
});
test("settings.enabled === false → no wait", () => {
const r = shouldWaitForComboCooldown(
baseInput({ settings: baseSettings({ enabled: false }) }) as never
);
assert.equal(r.wait, false);
});
test("non-finite / garbage waitMs → no wait (defensive)", () => {
assert.equal(shouldWaitForComboCooldown(baseInput({ waitMs: Number.NaN }) as never).wait, false);
assert.equal(
shouldWaitForComboCooldown(baseInput({ waitMs: Number.POSITIVE_INFINITY }) as never).wait,
false
);
});
test("returned waitMs is clamped to a finite number (0 when input invalid)", () => {
const r = shouldWaitForComboCooldown(baseInput({ waitMs: Number.NaN }) as never);
assert.equal(Number.isFinite(r.waitMs), true);
assert.equal(r.waitMs, 0);
});
// ── resolveComboCooldownWaitDecision (target resolution + hint/fallback) ──────
const M = COMBO_COOLDOWN_WAIT_MARGIN_MS;
function decisionInput(overrides: Record<string, unknown> = {}) {
return {
targets: [{ provider: "openai", connectionId: "conn-1" }],
earliestRetryAfter: new Date(Date.now() + 2000).toISOString(),
attempt: 0,
budgetLeftMs: 8000,
settings: baseSettings(),
lookupLock: (_p: string, _c: string) => ({ reason: "rate_limit", remainingMs: 2000 }),
computeWaitMs: (_r: unknown) => 2000,
...overrides,
};
}
test("resolve: locked target with transient reason within budget → wait", () => {
const r = resolveComboCooldownWaitDecision(decisionInput() as never);
assert.equal(r.wait, true);
// wait = max(lock remaining, hint) + margin so the lock reliably clears.
assert.equal(r.waitMs, 2000 + M);
assert.equal(r.reason, "rate_limit");
});
test("resolve: quota_exhausted lock → no wait (the critical security guard)", () => {
const r = resolveComboCooldownWaitDecision(
decisionInput({
lookupLock: () => ({ reason: "quota_exhausted", remainingMs: 3000 }),
}) as never
);
assert.equal(r.wait, false);
});
test("resolve: settings.enabled false short-circuits before any lookup", () => {
let lookups = 0;
const r = resolveComboCooldownWaitDecision(
decisionInput({
settings: baseSettings({ enabled: false }),
lookupLock: () => {
lookups += 1;
return { reason: "rate_limit", remainingMs: 3000 };
},
}) as never
);
assert.equal(r.wait, false);
assert.equal(lookups, 0);
});
test("resolve: no locked target → no wait", () => {
const r = resolveComboCooldownWaitDecision(decisionInput({ lookupLock: () => null }) as never);
assert.equal(r.wait, false);
assert.equal(r.reason, null);
});
test("resolve: expired lock (remainingMs <= 0) is ignored → no wait", () => {
const r = resolveComboCooldownWaitDecision(
decisionInput({ lookupLock: () => ({ reason: "rate_limit", remainingMs: 0 }) }) as never
);
assert.equal(r.wait, false);
});
test("resolve: picks the soonest-to-recover locked target across many", () => {
const r = resolveComboCooldownWaitDecision(
decisionInput({
targets: [
{ provider: "openai", connectionId: "a" },
{ provider: "openai", connectionId: "b" },
],
// 'a' recovers in 3s, 'b' in 2s → 'b' wins; both rate_limit so 'b' (2s)
// drives the wait. No usable hint → wait = lock remaining + margin.
lookupLock: (_p: string, c: string) =>
c === "b"
? { reason: "rate_limit", remainingMs: 2000 }
: { reason: "rate_limit", remainingMs: 3000 },
computeWaitMs: () => null,
}) as never
);
assert.equal(r.wait, true);
assert.equal(r.waitMs, 2000 + M);
});
test("resolve: uses lock remainingMs (+margin) when the retry-after hint yields nothing", () => {
const r = resolveComboCooldownWaitDecision(
decisionInput({
computeWaitMs: () => null,
lookupLock: () => ({ reason: "rate_limit", remainingMs: 1500 }),
}) as never
);
assert.equal(r.wait, true);
assert.equal(r.waitMs, 1500 + M);
});
test("resolve: honors a LONGER upstream hint over the lock remaining (still capped)", () => {
// lock says 1s left but upstream hint says 3s → wait the longer 3s (+margin).
const r = resolveComboCooldownWaitDecision(
decisionInput({
computeWaitMs: () => 3000,
lookupLock: () => ({ reason: "rate_limit", remainingMs: 1000 }),
}) as never
);
assert.equal(r.wait, true);
assert.equal(r.waitMs, 3000 + M);
});
test("resolve: hint above the ceiling → no wait even if lock remaining is short", () => {
const r = resolveComboCooldownWaitDecision(
decisionInput({
settings: baseSettings({ maxWaitMs: 5000 }),
computeWaitMs: () => 6000,
lookupLock: () => ({ reason: "rate_limit", remainingMs: 1000 }),
}) as never
);
assert.equal(r.wait, false);
});
test("resolve: lock remaining above the ceiling → no wait (not a SHORT cooldown)", () => {
const r = resolveComboCooldownWaitDecision(
decisionInput({
settings: baseSettings({ maxWaitMs: 5000 }),
computeWaitMs: () => null,
lookupLock: () => ({ reason: "rate_limit", remainingMs: 6000 }),
}) as never
);
assert.equal(r.wait, false);
});

View File

@@ -0,0 +1,91 @@
/**
* #5005 — Per-combo editable `description` field.
*
* The routing-combo API historically had NO `description` field: the Zod
* `createComboSchema` / `updateComboSchema` stripped any `description` on save.
* This proves the schema now ACCEPTS and PRESERVES `description`, and that the
* value round-trips through the DB layer (persisted in the `data` JSON blob and
* surfaced back by GET-style reads), with no new DB column.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-combo-desc-5005-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const { createComboSchema, updateComboSchema } = await import(
"../../src/shared/validation/schemas.ts"
);
const core = await import("../../src/lib/db/core.ts");
const combosDb = await import("../../src/lib/db/combos.ts");
async function resetStorage() {
core.resetDbInstance();
if (fs.existsSync(TEST_DATA_DIR)) {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
}
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(async () => {
await resetStorage();
});
test.after(async () => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("createComboSchema preserves description instead of stripping it", () => {
const parsed = createComboSchema.parse({
name: "Described Combo",
models: [{ provider: "openai", model: "gpt-4.1" }],
description: "Routes premium traffic to GPT-4.1",
});
assert.equal(parsed.description, "Routes premium traffic to GPT-4.1");
});
test("updateComboSchema preserves description instead of stripping it", () => {
const parsed = updateComboSchema.parse({
description: "Updated description text",
});
assert.equal(parsed.description, "Updated description text");
});
test("description round-trips through createCombo and getComboById/getComboByName", async () => {
const created = await combosDb.createCombo({
name: "Desc Combo",
models: [{ provider: "openai", model: "gpt-4.1" }],
description: "My human-friendly note",
});
assert.equal(created.description, "My human-friendly note");
const byId = await combosDb.getComboById(created.id as string);
assert.ok(byId, "combo should be retrievable by id");
assert.equal(byId!.description, "My human-friendly note");
const byName = await combosDb.getComboByName("Desc Combo");
assert.ok(byName, "combo should be retrievable by name");
assert.equal(byName!.description, "My human-friendly note");
});
test("description survives an updateCombo and is listed by getCombos", async () => {
const created = await combosDb.createCombo({
name: "Update Desc Combo",
models: [{ provider: "openai", model: "gpt-4.1" }],
});
const updated = await combosDb.updateCombo(created.id as string, {
description: "Added later via PUT",
});
assert.ok(updated);
assert.equal(updated!.description, "Added later via PUT");
const all = await combosDb.getCombos();
const found = all.find((c) => c.id === created.id);
assert.ok(found, "updated combo should appear in getCombos");
assert.equal(found!.description, "Added later via PUT");
});

View File

@@ -0,0 +1,209 @@
/**
* Fusion combo strategy — parallel panel + judge synthesis.
*
* Ported from upstream decolua/9router (Daniil Schovkunov). Adds Fusion as the 16th
* combo strategy: fan the prompt out to every panel model in parallel, then a judge
* model synthesizes one final answer from all panel responses.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-combo-fusion-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "combo-fusion-test-secret";
const { handleComboChat } = await import("../../open-sse/services/combo.ts");
const noop = () => {};
const log = { info: noop, warn: noop, debug: noop, error: noop };
type Body = Record<string, unknown>;
// Minimal OpenAI-chat Response-shaped object compatible with the engine's .ok + .clone().json() surface.
function okResponse(content: string, { delayMs = 0 } = {}): Response | Promise<Response> {
const body = JSON.stringify({ choices: [{ message: { role: "assistant", content } }] });
const make = () =>
new Response(body, { status: 200, headers: { "Content-Type": "application/json" } });
return delayMs > 0 ? new Promise((r) => setTimeout(() => r(make()), delayMs)) : make();
}
function errResponse(status = 500): Response {
return new Response(JSON.stringify({ error: { message: "boom" } }), {
status,
headers: { "Content-Type": "application/json" },
});
}
function fusionCombo(models: string[], extra: Record<string, unknown> = {}) {
return {
name: "test-fusion-combo",
strategy: "fusion",
models: models.map((m) => ({ model: m })),
config: extra,
};
}
test("fusion: single-model panel answers directly (nothing to fuse)", async () => {
const calls: string[] = [];
const handleSingleModel = async (_b: Body, m: string) => {
calls.push(m);
return okResponse("solo");
};
const res = await handleComboChat({
body: { messages: [{ role: "user", content: "hi" }] },
combo: fusionCombo(["p/only"]),
handleSingleModel,
log,
settings: {},
allCombos: [],
});
assert.equal(calls.length, 1);
assert.equal(calls[0], "p/only");
assert.equal(res.status, 200);
});
test("fusion: fans out to the panel then routes a synthesis turn to the judge", async () => {
const seen: string[] = [];
const seenBodies: Body[] = [];
const handleSingleModel = async (b: Body, m: string) => {
seen.push(m);
seenBodies.push(b);
if (m === "p/judge") return okResponse("FINAL");
return okResponse(`ans-${m}`);
};
const res = await handleComboChat({
body: {
messages: [{ role: "user", content: "Q" }],
stream: true,
tools: [{ name: "x" }],
},
combo: fusionCombo(["p/a", "p/b", "p/c"], { judgeModel: "p/judge" }),
handleSingleModel,
log,
settings: {},
allCombos: [],
});
// 3 panel calls + 1 judge call.
assert.equal(seen.length, 4);
assert.deepEqual(seen.slice(0, 3).sort(), ["p/a", "p/b", "p/c"]);
assert.equal(seen[3], "p/judge");
// Panel calls are non-streaming with tools stripped.
for (let i = 0; i < 3; i++) {
const b = seenBodies[i];
assert.equal(b.stream, false, "panel call should be non-streaming");
assert.equal(b.tools, undefined, "panel call should have tools stripped");
}
// Judge call carries every panel answer + keeps the client's stream flag.
const judgeBody = seenBodies[3];
const judgeMsgs = judgeBody.messages as Array<{ role: string; content: string }>;
const judgeText = judgeMsgs[judgeMsgs.length - 1].content;
assert.match(judgeText, /ans-p\/a/);
assert.match(judgeText, /ans-p\/b/);
assert.match(judgeText, /ans-p\/c/);
assert.match(judgeText, /Source 1/);
assert.equal(judgeBody.stream, true);
assert.equal(res.status, 200);
});
test("fusion: defaults the judge to the first panel model when none is set", async () => {
const seen: string[] = [];
const handleSingleModel = async (_b: Body, m: string) => {
seen.push(m);
return okResponse(`ans-${m}`);
};
await handleComboChat({
body: { messages: [{ role: "user", content: "Q" }] },
combo: fusionCombo(["p/first", "p/second"]),
handleSingleModel,
log,
settings: {},
allCombos: [],
});
// Last call is the judge; defaults to panel[0].
assert.equal(seen[seen.length - 1], "p/first");
});
test("fusion: proceeds on quorum without waiting for a straggler (grace window)", async () => {
const handleSingleModel = async (_b: Body, m: string) => {
if (m === "p/slow") return okResponse("slow", { delayMs: 5000 });
if (m === "p/judge") return okResponse("FINAL");
return okResponse(`fast-${m}`);
};
const t0 = Date.now();
const seenBodies: Body[] = [];
const wrapped = async (b: Body, m: string) => {
seenBodies.push(b);
return handleSingleModel(b, m);
};
await handleComboChat({
body: { messages: [{ role: "user", content: "Q" }] },
combo: fusionCombo(["p/x", "p/y", "p/slow"], {
judgeModel: "p/judge",
fusionTuning: { minPanel: 2, stragglerGraceMs: 50, panelHardTimeoutMs: 10000 },
}),
handleSingleModel: wrapped,
log,
settings: {},
allCombos: [],
});
const elapsed = Date.now() - t0;
// Two fast answers reach quorum; grace is 50ms, so we never wait ~5s for p/slow.
assert.ok(elapsed < 2000, `should not wait for straggler (took ${elapsed}ms)`);
const judgeBody = seenBodies[seenBodies.length - 1];
const judgeMsgs = judgeBody.messages as Array<{ role: string; content: string }>;
const judgeText = judgeMsgs[judgeMsgs.length - 1].content;
assert.match(judgeText, /fast-p\/x/);
assert.match(judgeText, /fast-p\/y/);
assert.ok(!/slow/.test(judgeText), "straggler answer should not appear in the judge prompt");
});
test("fusion: returns the lone survivor directly when only one panel model succeeds", async () => {
const seen: string[] = [];
const handleSingleModel = async (_b: Body, m: string) => {
seen.push(m);
if (m === "p/ok") return okResponse("lone");
return errResponse(500);
};
await handleComboChat({
body: { messages: [{ role: "user", content: "Q" }] },
combo: fusionCombo(["p/ok", "p/bad"], {
judgeModel: "p/judge",
fusionTuning: { minPanel: 2, stragglerGraceMs: 50, panelHardTimeoutMs: 5000 },
}),
handleSingleModel,
log,
settings: {},
allCombos: [],
});
// No judge call — single answer means there is nothing to fuse.
assert.ok(
!seen.includes("p/judge"),
"judge should not be invoked when only one panel model survives"
);
});
test("fusion: returns 503 when the whole panel fails", async () => {
const handleSingleModel = async () => errResponse(500);
const res = await handleComboChat({
body: { messages: [{ role: "user", content: "Q" }] },
combo: fusionCombo(["p/a", "p/b"], {
fusionTuning: { minPanel: 2, stragglerGraceMs: 50, panelHardTimeoutMs: 5000 },
}),
handleSingleModel,
log,
settings: {},
allCombos: [],
});
assert.equal(res.status, 503);
});

View File

@@ -0,0 +1,132 @@
/**
* tests/unit/combo-headroom-ranking.test.ts
*
* Coverage for the PURE headroom-ranking helper
* (open-sse/services/combo/headroomRanking.ts).
*
* headroom = 1 max(util_5h, util_7d). Candidates with MORE free capacity
* (higher headroom) rank first. Saturation is INJECTED (no network), so the
* helper is fully deterministic / unit-testable. Ties preserve input order
* (stable sort).
*
* Technique borrowed from the `dario` project: prefer the connection with the
* most headroom instead of reactively filling one until it 429s.
*/
import test from "node:test";
import assert from "node:assert/strict";
const mod = await import("../../open-sse/services/combo/headroomRanking.ts");
const { computeHeadroom, rankByHeadroom } = mod;
// ─── computeHeadroom (scalar) ────────────────────────────────────────────────
test("computeHeadroom: 1 max(util5h, util7d)", () => {
// weekly is the binding (higher util) dimension here
assert.equal(computeHeadroom({ util5h: 0.2, util7d: 0.7 }), 1 - 0.7);
// 5h is the binding dimension here
assert.equal(computeHeadroom({ util5h: 0.9, util7d: 0.1 }), 1 - 0.9);
});
test("computeHeadroom: clamps utilization into [0,1] and never returns <0 or >1", () => {
// over-saturated upstream (util > 1) → headroom floored at 0
assert.equal(computeHeadroom({ util5h: 1.4, util7d: 0 }), 0);
// negative noise → treated as 0 util → full headroom
assert.equal(computeHeadroom({ util5h: -0.3, util7d: -0.1 }), 1);
});
test("computeHeadroom: missing / non-finite dimensions are treated as 0 util (generous)", () => {
// fail-open: unknown saturation must not penalize a connection
assert.equal(computeHeadroom({}), 1);
assert.equal(computeHeadroom({ util5h: Number.NaN, util7d: undefined }), 1);
assert.equal(computeHeadroom({ util5h: 0.5 }), 0.5);
});
// ─── rankByHeadroom (ordering) ───────────────────────────────────────────────
const keyOf = (c: { id: string }) => c.id;
test("rankByHeadroom: higher headroom (more free) first — A busy(0.8) vs B free(0.2)", () => {
const candidates = [{ id: "A" }, { id: "B" }];
const sat = new Map([
["A", { util5h: 0.8, util7d: 0.1 }],
["B", { util5h: 0.2, util7d: 0.1 }],
]);
const ranked = rankByHeadroom(candidates, sat, keyOf);
assert.deepEqual(
ranked.map((c) => c.id),
["B", "A"]
);
});
test("rankByHeadroom: uses max(5h,7d) — B's high weekly sinks it below A", () => {
const candidates = [{ id: "A" }, { id: "B" }];
const sat = new Map([
["A", { util5h: 0.5, util7d: 0.5 }], // headroom 0.5
["B", { util5h: 0.1, util7d: 0.95 }], // headroom 0.05 (weekly binds)
]);
const ranked = rankByHeadroom(candidates, sat, keyOf);
assert.deepEqual(
ranked.map((c) => c.id),
["A", "B"]
);
});
test("rankByHeadroom: tie → stable (preserves original input order)", () => {
const candidates = [{ id: "X" }, { id: "Y" }, { id: "Z" }];
const sat = new Map([
["X", { util5h: 0.3, util7d: 0.3 }],
["Y", { util5h: 0.3, util7d: 0.3 }],
["Z", { util5h: 0.3, util7d: 0.3 }],
]);
const ranked = rankByHeadroom(candidates, sat, keyOf);
assert.deepEqual(
ranked.map((c) => c.id),
["X", "Y", "Z"]
);
});
test("rankByHeadroom: partial tie keeps stable order within the tied band", () => {
// A and C both headroom 0.7; B headroom 0.2. Expect A, C (input order), then B.
const candidates = [{ id: "A" }, { id: "B" }, { id: "C" }];
const sat = new Map([
["A", { util5h: 0.3, util7d: 0.0 }], // 0.7
["B", { util5h: 0.8, util7d: 0.0 }], // 0.2
["C", { util5h: 0.3, util7d: 0.0 }], // 0.7
]);
const ranked = rankByHeadroom(candidates, sat, keyOf);
assert.deepEqual(
ranked.map((c) => c.id),
["A", "C", "B"]
);
});
test("rankByHeadroom: missing saturation entry → treated as full headroom (fail-open, ranks first)", () => {
const candidates = [{ id: "known" }, { id: "unknown" }];
const sat = new Map([
["known", { util5h: 0.6, util7d: 0.6 }], // headroom 0.4
// "unknown" absent → headroom 1.0 → should rank first
]);
const ranked = rankByHeadroom(candidates, sat, keyOf);
assert.deepEqual(
ranked.map((c) => c.id),
["unknown", "known"]
);
});
test("rankByHeadroom: does not mutate the input array", () => {
const candidates = [{ id: "A" }, { id: "B" }];
const original = [...candidates];
const sat = new Map([
["A", { util5h: 0.9, util7d: 0.9 }],
["B", { util5h: 0.1, util7d: 0.1 }],
]);
rankByHeadroom(candidates, sat, keyOf);
assert.deepEqual(candidates, original, "input array order must be unchanged");
});
test("rankByHeadroom: empty / single candidate is a no-op", () => {
assert.deepEqual(rankByHeadroom([], new Map(), keyOf), []);
const one = [{ id: "solo" }];
assert.deepEqual(rankByHeadroom(one, new Map(), keyOf), one);
});

View File

@@ -0,0 +1,147 @@
/**
* tests/unit/combo-headroom-strategy.test.ts
*
* Integration coverage for orderTargetsByHeadroom
* (open-sse/services/combo/quotaStrategies.ts) — the async orderer that wires
* per-connection saturation into the pure rankByHeadroom helper.
*
* Isolated from DB/network: targets carry explicit connectionIds and use a
* provider with NO registered quota fetcher, so getQuotaAwareConnectionsForTarget
* short-circuits (no getProviderConnections call). The getSaturation signal is
* injected via __setHeadroomSaturationFetcherForTests.
*/
import test from "node:test";
import assert from "node:assert/strict";
const mod = await import("../../open-sse/services/combo/quotaStrategies.ts");
const { orderTargetsByHeadroom, __setHeadroomSaturationFetcherForTests } = mod;
const silentLog = { warn: () => {} };
function target(connectionId: string, modelStr = "testprov/model-a") {
return {
kind: "model" as const,
stepId: connectionId,
executionKey: `key-${connectionId}`,
modelStr,
provider: "testprov",
providerId: "testprov",
connectionId,
weight: 1,
label: null,
};
}
test.afterEach(() => {
__setHeadroomSaturationFetcherForTests(null);
});
test("orderTargetsByHeadroom: prefers the connection with the most free capacity", async () => {
// conn-busy is at 0.8 util (5h), conn-free at 0.2 → free should come first.
__setHeadroomSaturationFetcherForTests(async (connectionId, _provider, dim) => {
const table: Record<string, { "5h": number; weekly: number }> = {
"conn-busy": { "5h": 0.8, weekly: 0.1 },
"conn-free": { "5h": 0.2, weekly: 0.1 },
};
return table[connectionId]?.[dim.window as "5h" | "weekly"] ?? 0;
});
const ordered = await orderTargetsByHeadroom(
[target("conn-busy"), target("conn-free")],
"combo-x",
silentLog
);
assert.deepEqual(
ordered.map((t) => t.connectionId),
["conn-free", "conn-busy"]
);
});
test("orderTargetsByHeadroom: weekly window can bind even when 5h is low", async () => {
// conn-a: balanced 0.5/0.5 → headroom 0.5
// conn-b: 5h great (0.1) but weekly nearly exhausted (0.95) → headroom 0.05
__setHeadroomSaturationFetcherForTests(async (connectionId, _provider, dim) => {
const table: Record<string, { "5h": number; weekly: number }> = {
"conn-a": { "5h": 0.5, weekly: 0.5 },
"conn-b": { "5h": 0.1, weekly: 0.95 },
};
return table[connectionId]?.[dim.window as "5h" | "weekly"] ?? 0;
});
const ordered = await orderTargetsByHeadroom(
[target("conn-a"), target("conn-b")],
"combo-x",
silentLog
);
assert.deepEqual(
ordered.map((t) => t.connectionId),
["conn-a", "conn-b"]
);
});
test("orderTargetsByHeadroom: equal saturation preserves priority order (stable)", async () => {
__setHeadroomSaturationFetcherForTests(async () => 0.3);
const ordered = await orderTargetsByHeadroom(
[target("c1"), target("c2"), target("c3")],
"combo-x",
silentLog
);
assert.deepEqual(
ordered.map((t) => t.connectionId),
["c1", "c2", "c3"]
);
});
test("orderTargetsByHeadroom: saturation fetcher throwing fails open (keeps order)", async () => {
__setHeadroomSaturationFetcherForTests(async () => {
throw new Error("boom");
});
const ordered = await orderTargetsByHeadroom(
[target("c1"), target("c2")],
"combo-x",
silentLog
);
// Fail-open: the orderer catches and returns the original target order.
assert.deepEqual(
ordered.map((t) => t.connectionId),
["c1", "c2"]
);
});
test("orderTargetsByHeadroom: single / empty target is a no-op without calling the fetcher", async () => {
let called = 0;
__setHeadroomSaturationFetcherForTests(async () => {
called++;
return 0;
});
assert.deepEqual(await orderTargetsByHeadroom([], "combo-x", silentLog), []);
const one = [target("solo")];
const orderedOne = await orderTargetsByHeadroom(one, "combo-x", silentLog);
assert.deepEqual(
orderedOne.map((t) => t.connectionId),
["solo"]
);
assert.equal(called, 0, "fetcher must not be called for <=1 target");
});
test("orderTargetsByHeadroom: fetches each unique connection's saturation once (5h+weekly)", async () => {
const calls: string[] = [];
__setHeadroomSaturationFetcherForTests(async (connectionId, _provider, dim) => {
calls.push(`${connectionId}:${dim.window}`);
return connectionId === "hot" ? 0.9 : 0.1;
});
const ordered = await orderTargetsByHeadroom(
[target("hot"), target("cool")],
"combo-x",
silentLog
);
assert.deepEqual(
ordered.map((t) => t.connectionId),
["cool", "hot"]
);
// Exactly one 5h + one weekly probe per unique connection (2 conns → 4 calls).
calls.sort();
assert.deepEqual(calls, ["cool:5h", "cool:weekly", "hot:5h", "hot:weekly"]);
});

View File

@@ -0,0 +1,77 @@
/**
* ccp pin health gate — drop a context-cache pin when its provider is DURABLY
* down so the session fails over instead of pounding a dead account, while
* tolerating transient cooldowns so an unstable provider does not churn the pin.
*
* Incident 2026-06-22: a session pinned to a throttled/credits-exhausted account
* stayed pinned forever (strategy bypassed, no failover) because the pin was only
* dropped when the model LEFT the combo, never on connection health.
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import { pinIsDurablyUnhealthy } from "../../open-sse/services/combo.ts";
const NOW = 1_900_000_000_000;
const opts = { backoffLevel: 2, graceMs: 20_000 };
const healthy = { testStatus: "active", backoffLevel: 0, rateLimitedUntil: null };
test("keeps pin when a healthy connection exists", () => {
assert.equal(pinIsDurablyUnhealthy("CLOSED", [healthy], NOW, opts), false);
});
test("drops pin when circuit is OPEN", () => {
assert.equal(pinIsDurablyUnhealthy("OPEN", [healthy], NOW, opts), true);
});
test("drops pin when there are no active connections", () => {
assert.equal(pinIsDurablyUnhealthy("CLOSED", [], NOW, opts), true);
});
test("drops pin when the only connection has credits exhausted (terminal)", () => {
const conn = { testStatus: "credits_exhausted", backoffLevel: 0, rateLimitedUntil: null };
assert.equal(pinIsDurablyUnhealthy("CLOSED", [conn], NOW, opts), true);
});
test("drops pin on banned/expired terminal status", () => {
assert.equal(
pinIsDurablyUnhealthy("CLOSED", [{ testStatus: "banned", backoffLevel: 0 }], NOW, opts),
true
);
assert.equal(
pinIsDurablyUnhealthy("CLOSED", [{ testStatus: "expired", backoffLevel: 0 }], NOW, opts),
true
);
});
test("drops pin once backoffLevel reaches the threshold (repeated failures)", () => {
assert.equal(
pinIsDurablyUnhealthy("CLOSED", [{ testStatus: "active", backoffLevel: 2 }], NOW, opts),
true
);
});
test("anti-flap: keeps pin on a brief transient cooldown (low backoff, short rate-limit)", () => {
const conn = {
testStatus: "active",
backoffLevel: 1,
rateLimitedUntil: new Date(NOW + 4_000).toISOString(), // 4s out — within grace
};
assert.equal(pinIsDurablyUnhealthy("CLOSED", [conn], NOW, opts), false);
});
test("drops pin on a long rate-limit window (beyond grace)", () => {
const conn = {
testStatus: "active",
backoffLevel: 0,
rateLimitedUntil: new Date(NOW + 60_000).toISOString(), // 60s out — durable
};
assert.equal(pinIsDurablyUnhealthy("CLOSED", [conn], NOW, opts), true);
});
test("keeps pin if ANY connection is usable (terminal + healthy mix)", () => {
const conns = [
{ testStatus: "credits_exhausted", backoffLevel: 0 },
healthy,
];
assert.equal(pinIsDurablyUnhealthy("CLOSED", conns, NOW, opts), false);
});

View File

@@ -0,0 +1,234 @@
/**
* TDD (integration) — quota-share combo cooldown-aware retry (Variante A),
* exercised through handleComboChat.
*
* Scenarios (modelled on model-lockout-max-cooldown.test.ts):
* 1. strategy="quota-share", single connection, model hits a 429 with a SHORT
* retry-after → the combo WAITS out the cooldown and re-dispatches; the 2nd
* pass (lock expired) returns 200 instead of propagating the 429.
* 2. A 403 (quota_exhausted, locked until midnight) → NO wait, the 403/429 is
* propagated immediately (the helper's critical exclusion).
* 3. Client abort DURING the wait → 499 "Request aborted".
* 4. strategy="priority" (non quota-share) → unchanged: the 429 is propagated
* immediately with NO wait.
* 5. comboCooldownWait disabled in settings → unchanged: 429 propagated, no wait.
*
* The waits use a real (short) cooldown so the real setTimeout in
* waitForCooldownAwareRetry elapses fast and the model lock expires naturally.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omr-combo-cooldown-wait-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-combo-cooldown-wait-secret";
const core = await import("../../src/lib/db/core.ts");
const { handleComboChat } = await import("../../open-sse/services/combo.ts");
const { clearAllModelLockouts } = await import("../../open-sse/services/accountFallback.ts");
function createLog() {
return { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} };
}
// A short transient cooldown so the real wait is fast but the lock genuinely
// expires between passes. The 429 carries a retry-after hint slightly LONGER
// than baseCooldownMs so waiting it out guarantees the lock has cleared.
const BASE_COOLDOWN_MS = 150;
const RETRY_AFTER_MS = 250;
function shortModelLockoutSettings() {
return {
modelLockout: {
enabled: true,
errorCodes: [403, 429],
baseCooldownMs: BASE_COOLDOWN_MS,
maxCooldownMs: 5000,
maxBackoffSteps: 0,
useExponentialBackoff: false,
},
};
}
function jsonResponse(status: number, body: Record<string, unknown>) {
return new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json" },
});
}
function okResponse() {
return jsonResponse(200, { id: "ok", choices: [{ message: { content: "recovered" } }] });
}
function rateLimitResponse(status: number) {
return jsonResponse(status, {
error: { message: `rate limited (${status})` },
// string ISO retry-after → computeClosestRetryAfter yields ~RETRY_AFTER_MS
retryAfter: new Date(Date.now() + RETRY_AFTER_MS).toISOString(),
});
}
function comboOf(strategy: string) {
return {
name: `qtSd/${strategy}-${Math.random().toString(16).slice(2, 8)}`,
strategy,
models: ["openai/gpt-4"],
config: { maxRetries: 0, retryDelayMs: 0, fallbackDelayMs: 0, maxSetRetries: 0 },
};
}
async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(async () => {
clearAllModelLockouts();
await resetStorage();
});
test.after(async () => {
clearAllModelLockouts();
try {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
} catch {
/* best effort */
}
});
test("quota-share: short 429 cooldown → waits and re-dispatches (2nd pass 200)", async () => {
let calls = 0;
const handleSingleModel = async () => {
calls += 1;
// 1st dispatch: transient 429 (records a rate_limit lock). 2nd dispatch
// (after the wait, lock expired): success.
return calls === 1 ? rateLimitResponse(429) : okResponse();
};
const startedAt = Date.now();
const res = await handleComboChat({
body: { model: "openai/gpt-4" },
combo: comboOf("quota-share"),
handleSingleModel,
isModelAvailable: async () => true,
log: createLog() as never,
settings: shortModelLockoutSettings(),
allCombos: null,
});
const elapsed = Date.now() - startedAt;
assert.equal(res.status, 200, "expected the retried dispatch to succeed with 200");
assert.equal(calls, 2, "expected exactly one wait+redispatch (2 upstream calls)");
assert.ok(
elapsed >= BASE_COOLDOWN_MS,
`expected to have waited out the cooldown, only ${elapsed}ms elapsed`
);
});
test("quota-share: 403 quota_exhausted → NO wait, error propagated immediately", async () => {
let calls = 0;
const handleSingleModel = async () => {
calls += 1;
return rateLimitResponse(403);
};
const startedAt = Date.now();
const res = await handleComboChat({
body: { model: "openai/gpt-4" },
combo: comboOf("quota-share"),
handleSingleModel,
isModelAvailable: async () => true,
log: createLog() as never,
settings: shortModelLockoutSettings(),
allCombos: null,
});
const elapsed = Date.now() - startedAt;
assert.notEqual(res.status, 200, "quota_exhausted must not be retried into a success");
// The real signal that the cooldown wait did NOT fire: a single upstream
// dispatch (no redispatch). The 403 lock cooldown is multi-second, so the
// wait — had it fired — would dominate the elapsed time; assert we stayed far
// below that (loose bound; the first combo dispatch pays DB/import overhead).
assert.equal(calls, 1, "quota_exhausted must NOT trigger a wait+redispatch");
assert.ok(elapsed < 1500, `quota_exhausted must not wait out a cooldown, but ${elapsed}ms elapsed`);
});
test("quota-share: client abort during the wait → 499", async () => {
const controller = new AbortController();
let calls = 0;
const handleSingleModel = async () => {
calls += 1;
// Always 429 so the loop reaches the wait; abort fires during the wait.
return rateLimitResponse(429);
};
// Abort shortly after the request starts — within the cooldown wait window.
setTimeout(() => controller.abort(), 50);
const res = await handleComboChat({
body: { model: "openai/gpt-4" },
combo: comboOf("quota-share"),
handleSingleModel,
isModelAvailable: async () => true,
log: createLog() as never,
settings: shortModelLockoutSettings(),
allCombos: null,
signal: controller.signal,
});
assert.equal(res.status, 499, "abort during the cooldown wait must return 499");
});
test("non quota-share (priority): 429 propagated immediately, NO wait", async () => {
let calls = 0;
const handleSingleModel = async () => {
calls += 1;
return rateLimitResponse(429);
};
const startedAt = Date.now();
const res = await handleComboChat({
body: { model: "openai/gpt-4" },
combo: { ...comboOf("priority"), name: "priority-combo" },
handleSingleModel,
isModelAvailable: async () => true,
log: createLog() as never,
settings: shortModelLockoutSettings(),
allCombos: null,
});
const elapsed = Date.now() - startedAt;
assert.equal(res.status, 429, "priority combo must propagate the 429 unchanged");
assert.equal(calls, 1, "priority combo must NOT wait+redispatch");
assert.ok(elapsed < 1500, `priority combo must not wait out a cooldown, but ${elapsed}ms elapsed`);
});
test("quota-share with comboCooldownWait disabled → 429 propagated, NO wait", async () => {
let calls = 0;
const handleSingleModel = async () => {
calls += 1;
return rateLimitResponse(429);
};
const res = await handleComboChat({
body: { model: "openai/gpt-4" },
combo: comboOf("quota-share"),
handleSingleModel,
isModelAvailable: async () => true,
log: createLog() as never,
settings: {
...shortModelLockoutSettings(),
resilienceSettings: { comboCooldownWait: { enabled: false } },
},
allCombos: null,
});
assert.equal(res.status, 429, "disabled feature must propagate the 429 unchanged");
assert.equal(calls, 1, "disabled feature must NOT wait+redispatch");
});

View File

@@ -154,13 +154,18 @@ test("POST /api/combos rejects composite tiers that point to unknown steps", asy
assert.equal(response.status, 400);
assert.deepEqual(body, {
error: {
message: "Invalid composite tiers",
details: [
{
field: "config.compositeTiers.tiers.primary.stepId",
message: 'stepId "step-missing" does not exist in combo.models',
},
],
code: "COMBO_003",
message: "Composite tier configuration is invalid",
category: "COMBO",
details: {
reason: "Invalid composite tiers",
details: [
{
field: "config.compositeTiers.tiers.primary.stepId",
message: 'stepId "step-missing" does not exist in combo.models',
},
],
},
},
});
});
@@ -222,13 +227,18 @@ test("PUT /api/combos rejects updates that orphan an existing composite tier ste
assert.equal(response.status, 400);
assert.deepEqual(body, {
error: {
message: "Invalid composite tiers",
details: [
{
field: "config.compositeTiers.tiers.primary.stepId",
message: 'stepId "step-primary" does not exist in combo.models',
},
],
code: "COMBO_003",
message: "Composite tier configuration is invalid",
category: "COMBO",
details: {
reason: "Invalid composite tiers",
details: [
{
field: "config.compositeTiers.tiers.primary.stepId",
message: 'stepId "step-primary" does not exist in combo.models',
},
],
},
},
});
});

View File

@@ -0,0 +1,322 @@
/**
* tests/unit/combo-session-stickiness.test.ts
*
* Unit tests for open-sse/services/combo/sessionStickiness.ts
*
* Design: saturation is injected via __setStickinessHeadroomFetcherForTests —
* no network, no DB, fully deterministic.
*
* Coverage:
* - deriveMessageHash: stable key derivation from first user message
* - applySessionStickiness: same hash → same connection while healthy
* - applySessionStickiness: saturated connection → rebind (clear + fall through)
* - applySessionStickiness: no user message → normal ordering, no crash
* - applySessionStickiness: different hashes → can map to different connections
* - applySessionStickiness: saturation fetch error → fail-open
* - recordStickyBinding / clearStickyBinding lifecycle
*/
import test from "node:test";
import assert from "node:assert/strict";
import type { HeadroomSaturation } from "../../open-sse/services/combo/headroomRanking.ts";
const mod = await import("../../open-sse/services/combo/sessionStickiness.ts");
const {
deriveMessageHash,
applySessionStickiness,
recordStickyBinding,
clearStickyBinding,
clearAllStickyBindings,
__setStickinessHeadroomFetcherForTests,
STICKINESS_HEADROOM_THRESHOLD,
} = mod;
// ─── helpers ─────────────────────────────────────────────────────────────────
function makeTarget(connectionId: string): import("../../open-sse/services/combo/types.ts").ResolvedComboTarget {
return {
kind: "model",
stepId: `step-${connectionId}`,
executionKey: `key-${connectionId}`,
modelStr: `gpt-4/${connectionId}`,
provider: "openai",
providerId: null,
connectionId,
weight: 1,
label: null,
};
}
function injectSat(sat: HeadroomSaturation | undefined): void {
__setStickinessHeadroomFetcherForTests(async (_id: string) => sat);
}
// ─── Test lifecycle ───────────────────────────────────────────────────────────
test.beforeEach(() => {
clearAllStickyBindings();
// Default: healthy connection (headroom = 0.5)
injectSat({ util5h: 0.3, util7d: 0.2 });
});
test.after(() => {
__setStickinessHeadroomFetcherForTests(null);
});
// ─── deriveMessageHash ───────────────────────────────────────────────────────
test("deriveMessageHash: returns 16-char hex for a plain user message", () => {
const hash = deriveMessageHash([{ role: "user", content: "Hello world" }]);
assert.ok(hash !== null, "hash should not be null");
assert.match(hash!, /^[a-f0-9]{16}$/);
});
test("deriveMessageHash: same content → same hash (stable)", () => {
const msgs = [{ role: "user", content: "How are you?" }];
assert.equal(deriveMessageHash(msgs), deriveMessageHash(msgs));
});
test("deriveMessageHash: different first messages → different hashes", () => {
const h1 = deriveMessageHash([{ role: "user", content: "AAA" }]);
const h2 = deriveMessageHash([{ role: "user", content: "BBB" }]);
assert.notEqual(h1, h2);
});
test("deriveMessageHash: skips assistant messages, uses first user", () => {
const msgs = [
{ role: "assistant", content: "Hi" },
{ role: "user", content: "My real first message" },
];
const direct = [{ role: "user", content: "My real first message" }];
assert.equal(deriveMessageHash(msgs), deriveMessageHash(direct));
});
test("deriveMessageHash: multi-part content array is hashed on text parts", () => {
const msgs = [
{
role: "user",
content: [
{ type: "text", text: "Hello" },
{ type: "image_url", url: "http://example.com/img.png" },
],
},
];
const hash = deriveMessageHash(msgs);
assert.ok(hash !== null);
assert.match(hash!, /^[a-f0-9]{16}$/);
// Stability
assert.equal(deriveMessageHash(msgs), hash);
});
test("deriveMessageHash: null/empty → null (fail-open)", () => {
assert.equal(deriveMessageHash(null), null);
assert.equal(deriveMessageHash([]), null);
assert.equal(deriveMessageHash(undefined), null);
// No user message
assert.equal(deriveMessageHash([{ role: "assistant", content: "Hi" }]), null);
});
// ─── applySessionStickiness — main scenarios ──────────────────────────────────
test("same message hash → same connection in repeated calls while healthy", async () => {
const targets = [makeTarget("conn-A"), makeTarget("conn-B"), makeTarget("conn-C")];
const messages = [{ role: "user", content: "First turn of conversation" }];
const hash = deriveMessageHash(messages)!;
assert.ok(hash, "hash must be derivable");
// Simulate first successful request: record sticky to conn-B
recordStickyBinding(hash, "conn-B");
// Call 1
const r1 = await applySessionStickiness(targets, messages);
assert.ok(r1.stuck, "should be stuck on first call");
assert.equal(r1.targets[0].connectionId, "conn-B", "conn-B should be first");
// Call 2 (same conversation)
const r2 = await applySessionStickiness(targets, messages);
assert.ok(r2.stuck, "should remain stuck on second call");
assert.equal(r2.targets[0].connectionId, "conn-B");
});
test("saturated connection → rebind (clear sticky, return normal ordering)", async () => {
injectSat({ util5h: 0.9, util7d: 0.9 }); // headroom = 0.1, below threshold
const targets = [makeTarget("conn-A"), makeTarget("conn-B")];
const messages = [{ role: "user", content: "Saturation test" }];
const hash = deriveMessageHash(messages)!;
recordStickyBinding(hash, "conn-A");
const result = await applySessionStickiness(targets, messages);
assert.equal(result.stuck, false, "should NOT be stuck when saturated");
// Normal order preserved (conn-A stays at index 0 — just not "stuck")
assert.equal(result.targets[0].connectionId, "conn-A");
// Binding must have been cleared — subsequent call also returns not stuck
// (even with healthy fetcher since the binding is gone)
injectSat({ util5h: 0.1, util7d: 0.1 }); // now healthy
const result2 = await applySessionStickiness(targets, messages);
assert.equal(result2.stuck, false, "binding was cleared, no stickiness on follow-up");
});
test("no user message in body → normal ordering, no crash", async () => {
const targets = [makeTarget("conn-A"), makeTarget("conn-B")];
const r1 = await applySessionStickiness(targets, null);
assert.equal(r1.stuck, false);
assert.deepEqual(
r1.targets.map((t) => t.connectionId),
["conn-A", "conn-B"]
);
const r2 = await applySessionStickiness(targets, []);
assert.equal(r2.stuck, false);
const r3 = await applySessionStickiness(targets, undefined);
assert.equal(r3.stuck, false);
});
test("different message hashes can map to different connections", async () => {
injectSat({ util5h: 0.0, util7d: 0.0 }); // full headroom
const targets = [makeTarget("conn-X"), makeTarget("conn-Y")];
const msgs1 = [{ role: "user", content: "Conversation Alpha" }];
const msgs2 = [{ role: "user", content: "Conversation Beta" }];
const hash1 = deriveMessageHash(msgs1)!;
const hash2 = deriveMessageHash(msgs2)!;
assert.notEqual(hash1, hash2, "hashes must differ");
recordStickyBinding(hash1, "conn-X");
recordStickyBinding(hash2, "conn-Y");
const r1 = await applySessionStickiness(targets, msgs1);
const r2 = await applySessionStickiness(targets, msgs2);
assert.ok(r1.stuck);
assert.ok(r2.stuck);
assert.equal(r1.targets[0].connectionId, "conn-X");
assert.equal(r2.targets[0].connectionId, "conn-Y");
});
test("saturation fetch error → fail-open (original order, no crash)", async () => {
__setStickinessHeadroomFetcherForTests(async (_id: string) => {
throw new Error("network failure");
});
const targets = [makeTarget("conn-A"), makeTarget("conn-B")];
const messages = [{ role: "user", content: "Error path" }];
const hash = deriveMessageHash(messages)!;
recordStickyBinding(hash, "conn-A");
const result = await applySessionStickiness(targets, messages);
assert.equal(result.stuck, false, "must not crash on fetcher error");
assert.deepEqual(
result.targets.map((t) => t.connectionId),
["conn-A", "conn-B"],
"original order preserved"
);
});
test("single target list → no-op, no sticky lookup performed", async () => {
const targets = [makeTarget("conn-solo")];
const messages = [{ role: "user", content: "Only one target" }];
const hash = deriveMessageHash(messages)!;
recordStickyBinding(hash, "conn-solo");
const result = await applySessionStickiness(targets, messages);
assert.equal(result.stuck, false, "no stickiness applied for single target");
assert.deepEqual(result.targets, targets);
});
test("STICKINESS_HEADROOM_THRESHOLD: connection below threshold is NOT reused", async () => {
// headroom = 1 0.9 = 0.1, clearly below threshold 0.15
injectSat({ util5h: 0.9, util7d: 0.0 });
const targets = [makeTarget("conn-A"), makeTarget("conn-B")];
const messages = [{ role: "user", content: "Below threshold" }];
const hash = deriveMessageHash(messages)!;
recordStickyBinding(hash, "conn-A");
const result = await applySessionStickiness(targets, messages);
assert.equal(result.stuck, false, "connection below threshold must not be reused");
});
test("connection just above threshold IS reused", async () => {
// headroom = 1 0.84 = 0.16 > 0.15
injectSat({ util5h: 0.84, util7d: 0.0 });
const targets = [makeTarget("conn-A"), makeTarget("conn-B")];
const messages = [{ role: "user", content: "Above threshold" }];
const hash = deriveMessageHash(messages)!;
recordStickyBinding(hash, "conn-A");
const result = await applySessionStickiness(targets, messages);
assert.ok(result.stuck, "connection just above threshold should be reused");
assert.equal(result.targets[0].connectionId, "conn-A");
});
test("sticky connection no longer in target list → clear binding, normal order", async () => {
const targets = [makeTarget("conn-X"), makeTarget("conn-Y")];
const messages = [{ role: "user", content: "Removed connection" }];
const hash = deriveMessageHash(messages)!;
// Sticky to conn-GONE which is NOT in targets
recordStickyBinding(hash, "conn-GONE");
const result = await applySessionStickiness(targets, messages);
assert.equal(result.stuck, false, "should not be stuck when connection is gone");
assert.deepEqual(
result.targets.map((t) => t.connectionId),
["conn-X", "conn-Y"]
);
});
// ─── recordStickyBinding / clearStickyBinding lifecycle ──────────────────────
test("recordStickyBinding: creates a new binding and stickiness is applied", async () => {
const targets = [makeTarget("conn-1"), makeTarget("conn-2")];
const messages = [{ role: "user", content: "Lifecycle test" }];
const hash = deriveMessageHash(messages)!;
recordStickyBinding(hash, "conn-1");
const r = await applySessionStickiness(targets, messages);
assert.ok(r.stuck);
assert.equal(r.targets[0].connectionId, "conn-1");
});
test("clearStickyBinding: removes the binding so next call is normal ordering", async () => {
const targets = [makeTarget("conn-A"), makeTarget("conn-B")];
const messages = [{ role: "user", content: "Clear test" }];
const hash = deriveMessageHash(messages)!;
recordStickyBinding(hash, "conn-B");
clearStickyBinding(hash);
const r = await applySessionStickiness(targets, messages);
assert.equal(r.stuck, false, "binding was cleared, no stickiness");
});
test("recordStickyBinding: updating to a new connectionId rebinds correctly", async () => {
const targets = [makeTarget("conn-OLD"), makeTarget("conn-NEW")];
const messages = [{ role: "user", content: "Rebind test" }];
const hash = deriveMessageHash(messages)!;
recordStickyBinding(hash, "conn-OLD");
// Rebind to conn-NEW
recordStickyBinding(hash, "conn-NEW");
const r = await applySessionStickiness(targets, messages);
assert.ok(r.stuck);
assert.equal(r.targets[0].connectionId, "conn-NEW");
});
test("messageHash is returned in result even when no binding exists", async () => {
const targets = [makeTarget("conn-A"), makeTarget("conn-B")];
const messages = [{ role: "user", content: "No binding yet" }];
const r = await applySessionStickiness(targets, messages);
assert.equal(r.stuck, false);
assert.ok(r.messageHash !== null, "hash should be derivable and returned");
assert.match(r.messageHash!, /^[a-f0-9]{16}$/);
});

View File

@@ -12,6 +12,8 @@ process.env.DATA_DIR = TEST_DATA_DIR;
const dbCore = await import("../../src/lib/db/core.ts");
const { handleComboChat } = await import("../../open-sse/services/combo.ts");
const { clearAllStickyBindings } =
await import("../../open-sse/services/combo/sessionStickiness.ts");
const { invalidateCodexQuotaCache, registerCodexConnection, registerCodexQuotaFetcher } =
await import("../../open-sse/services/codexQuotaFetcher.ts");
const { registerQuotaFetcher } = await import("../../open-sse/services/quotaPreflight.ts");
@@ -171,6 +173,10 @@ async function selectedConnectionFor(
combo: Record<string, unknown>,
options: { apiKeyAllowedConnections?: string[] | null } = {}
) {
// Isolate strategy/round-robin assertions from session stickiness (#5): this helper
// reuses the same body, so a sticky binding from a prior call would pin the connection
// and break tie-break rotation. Stickiness has its own suite (combo-session-stickiness).
clearAllStickyBindings();
const calls: Array<string | null> = [];
const response = await handleComboChat({
body: reqBodyTextArray,

View File

@@ -0,0 +1,429 @@
/**
* Tests for task-aware combo routing (port of upstream PR #2045).
*
* Coverage:
* - classifyTask: light / standard / heavy / critical levels
* - reorderByTaskWeight: floats higher-power models for heavy tasks
* - getConversationCacheKey: stable per conversation, distinct per thread
* - getOrSetConversationAffinityIndex: affinity pin + TTL behaviour
* - isTaskRoutingStrategy: gate for task-aware strategies
* - Guard: non-task-aware strategies are unaffected (identity path)
*/
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import {
classifyTask,
getConversationCacheKey,
getOrSetConversationAffinityIndex,
isTaskRoutingStrategy,
reorderByTaskWeight,
scoreModelForTask,
TASK_LEVEL_WEIGHT,
comboConversationAffinity,
clearConversationAffinity,
} from "../../open-sse/services/taskAwareRouting.ts";
import type { ResolvedComboTarget } from "../../open-sse/services/combo/types.ts";
// ── Helpers ───────────────────────────────────────────────────────────────────
function makeTarget(modelStr: string, idx = 0): ResolvedComboTarget {
const [provider = "", model = modelStr] = modelStr.includes("/")
? modelStr.split("/")
: ["", modelStr];
return {
kind: "model" as const,
stepId: `step-${idx}`,
executionKey: `exec-${idx}`,
modelStr,
provider,
providerId: null,
connectionId: null,
weight: 1,
label: null,
};
}
function makeBody(overrides: Record<string, unknown> = {}): Record<string, unknown> {
return { messages: [{ role: "user", content: "Hello" }], ...overrides };
}
// ── classifyTask ──────────────────────────────────────────────────────────────
describe("classifyTask", () => {
it("classifies empty body as light (all signals tiny → default-light path)", () => {
const t = classifyTask({});
// Empty body: 0 chars, 0 messages, 0 tools, 0 output tokens → light
assert.equal(t.level, "light");
assert.equal(t.weight, TASK_LEVEL_WEIGHT.light);
});
it("classifies a simple short greeting as light", () => {
const t = classifyTask({ messages: [{ role: "user", content: "hi there" }], max_tokens: 100 });
assert.equal(t.level, "light");
assert.equal(t.weight, TASK_LEVEL_WEIGHT.light);
});
it("classifies a translate request as light", () => {
const t = classifyTask({
messages: [{ role: "user", content: "translate this short sentence" }],
max_tokens: 256,
});
assert.equal(t.level, "light");
});
it("classifies a quick rewrite request as light", () => {
const t = classifyTask({
messages: [{ role: "user", content: "quick rewrite this sentence to be more concise" }],
max_tokens: 300,
});
assert.equal(t.level, "light");
});
it("classifies long conversation + big prompt as heavy (two signals)", () => {
// 20 messages (1 signal) + prompt >= 24000 chars (1 signal) = 2 → heavy
const bigContent = "context ".repeat(3500); // ~28000 chars
const messages = [
{ role: "user", content: bigContent },
...Array.from({ length: 20 }, (_, i) => ({
role: i % 2 === 0 ? "assistant" : "user",
content: "turn " + i,
})),
];
const t = classifyTask({ messages });
assert.equal(t.level, "heavy");
assert.equal(t.weight, TASK_LEVEL_WEIGHT.heavy);
});
it("classifies high reasoning effort as heavy", () => {
const t = classifyTask({
messages: [{ role: "user", content: "analyze this code" }],
reasoning_effort: "high",
});
assert.equal(t.level, "heavy");
});
it("classifies huge context (>= 100k chars) as critical", () => {
const t = classifyTask({
messages: [{ role: "user", content: "x".repeat(120_000) }],
});
assert.equal(t.level, "critical");
assert.equal(t.weight, TASK_LEVEL_WEIGHT.critical);
});
it("classifies huge output request (>= 32768 tokens) as critical", () => {
const t = classifyTask({
messages: [{ role: "user", content: "write a comprehensive report" }],
max_tokens: 40_000,
});
assert.equal(t.level, "critical");
});
it("classifies security+tools+effort as critical", () => {
const t = classifyTask({
messages: [
{
role: "user",
content: `Find a critical bug bounty attack chain for RCE or supply chain impact.\n${"context ".repeat(2000)}`,
},
],
tools: [{ name: "read" }, { name: "grep" }, { name: "web" }, { name: "bash" }],
reasoning_effort: "high",
max_tokens: 12_000,
});
assert.equal(t.level, "critical");
});
it("classifies moderate prompt without signals as standard (must exceed light thresholds)", () => {
// To be classified standard (not light), prompt must either: be > 4000 chars,
// have > 3 messages, tools, or not match a light keyword — "explain briefly" would be light.
// A prompt with "analyze" keyword + 5000 chars crosses into standard.
const t = classifyTask({
messages: [
{
role: "user",
content: "Please analyze how TCP works in detail.\n" + "context ".repeat(500),
},
{ role: "assistant", content: "TCP uses..." },
{ role: "user", content: "And what about UDP?" },
{ role: "assistant", content: "UDP is..." },
],
});
assert.equal(t.level, "standard");
});
it("returns reasons array for each classification", () => {
const t = classifyTask({
messages: [{ role: "user", content: "x".repeat(120_000) }],
});
assert.ok(Array.isArray(t.reasons), "reasons should be an array");
assert.ok(t.reasons.length > 0, "critical task should have at least one reason");
});
});
// ── reorderByTaskWeight ───────────────────────────────────────────────────────
describe("reorderByTaskWeight", () => {
it("returns same reference for single-target list", () => {
const targets = [makeTarget("anthropic/claude-haiku-4.5", 0)];
const task = classifyTask({ messages: [{ role: "user", content: "hello" }] });
const out = reorderByTaskWeight(targets, task, new Set());
assert.strictEqual(out, targets);
});
it("returns same reference for empty list", () => {
const targets: ResolvedComboTarget[] = [];
const task = classifyTask({});
const out = reorderByTaskWeight(targets, task, new Set());
assert.strictEqual(out, targets);
});
it("routes light tasks to lighter models (haiku before opus)", () => {
const targets = [
makeTarget("anthropic/claude-opus-4.6", 0),
makeTarget("anthropic/claude-haiku-4.5", 1),
];
const task = classifyTask({
messages: [{ role: "user", content: "quick rewrite this sentence" }],
max_tokens: 300,
});
const out = reorderByTaskWeight(targets, task, new Set());
assert.equal(out[0].modelStr, "anthropic/claude-haiku-4.5");
});
it("routes critical tasks to stronger models (opus before haiku)", () => {
const targets = [
makeTarget("anthropic/claude-haiku-4.5", 0),
makeTarget("anthropic/claude-opus-4.6", 1),
];
const task = classifyTask({
messages: [
{
role: "user",
content: `Security review for account takeover and cross-tenant RCE impact.\n${"code ".repeat(3000)}`,
},
],
tools: [{ name: "read" }, { name: "grep" }, { name: "bash" }, { name: "web" }],
reasoning_effort: "high",
});
const required = new Set(["reasoning"]);
const out = reorderByTaskWeight(targets, task, required);
assert.equal(out[0].modelStr, "anthropic/claude-opus-4.6");
assert.ok(
scoreModelForTask(out[0].modelStr, task, required) >=
scoreModelForTask(out[1].modelStr, task, required),
"first model should score >= second"
);
});
it("keeps hard-cap vision model first even for light tasks", () => {
// anthropic/claude-haiku-4.5 has vision; deepseek-chat does not
const targets = [
makeTarget("deepseek/deepseek-chat", 0),
makeTarget("anthropic/claude-haiku-4.5", 1),
];
const task = classifyTask({ messages: [{ role: "user", content: "what is in this image?" }] });
const out = reorderByTaskWeight(targets, task, new Set(["vision"]));
assert.equal(out[0].modelStr, "anthropic/claude-haiku-4.5");
});
it("never drops any targets", () => {
const targets = [
makeTarget("anthropic/claude-opus-4.6", 0),
makeTarget("anthropic/claude-haiku-4.5", 1),
makeTarget("deepseek/deepseek-chat", 2),
];
const task = classifyTask({ messages: [{ role: "user", content: "x".repeat(120_000) }] });
const out = reorderByTaskWeight(targets, task, new Set());
assert.equal(out.length, 3);
});
it("is stable: ties preserve original order", () => {
// Two identical models should not swap positions
const targets = [
makeTarget("anthropic/claude-haiku-4.5", 0),
makeTarget("anthropic/claude-haiku-4.5", 1),
];
const task = classifyTask({});
const out = reorderByTaskWeight(targets, task, new Set());
assert.equal(out[0].stepId, "step-0");
assert.equal(out[1].stepId, "step-1");
});
});
// ── isTaskRoutingStrategy ─────────────────────────────────────────────────────
describe("isTaskRoutingStrategy", () => {
it("returns true for task-aware strategy names", () => {
for (const name of ["smart", "task", "task-aware", "task_aware", "auto"]) {
assert.ok(isTaskRoutingStrategy(name), `Expected ${name} to be task-routing`);
}
});
it("is case-insensitive", () => {
assert.ok(isTaskRoutingStrategy("SMART"));
assert.ok(isTaskRoutingStrategy("Task-Aware"));
});
it("returns false for standard strategy names", () => {
for (const name of [
"priority",
"fallback",
"round-robin",
"weighted",
"random",
"fill-first",
"p2c",
"least-used",
"cost-optimized",
"reset-aware",
"reset-window",
"context-optimized",
"headroom",
"lkgp",
"context-relay",
]) {
assert.ok(!isTaskRoutingStrategy(name), `Expected ${name} to NOT be task-routing`);
}
});
it("returns false for null/undefined/empty", () => {
assert.ok(!isTaskRoutingStrategy(null));
assert.ok(!isTaskRoutingStrategy(undefined));
assert.ok(!isTaskRoutingStrategy(""));
});
});
// ── getConversationCacheKey ───────────────────────────────────────────────────
describe("getConversationCacheKey", () => {
it("returns null for null input", () => {
assert.equal(getConversationCacheKey(null as unknown as Record<string, unknown>), null);
});
it("returns a string for a body with messages", () => {
const key = getConversationCacheKey({
messages: [
{ role: "system", content: "You are helpful" },
{ role: "user", content: "hi" },
],
});
assert.equal(typeof key, "string");
assert.ok((key as string).length > 0);
});
it("is stable across calls for the same conversation start", () => {
const body = {
messages: [
{ role: "system", content: "You are a helpful assistant" },
{ role: "user", content: "Hello, how are you?" },
],
};
const k1 = getConversationCacheKey(body);
const k2 = getConversationCacheKey(body);
assert.equal(k1, k2);
});
it("differs for different first messages", () => {
const k1 = getConversationCacheKey({
messages: [{ role: "user", content: "Explain TCP" }],
});
const k2 = getConversationCacheKey({
messages: [{ role: "user", content: "Explain UDP" }],
});
assert.notEqual(k1, k2);
});
it("same key even when later messages are appended (conversation affinity)", () => {
const systemMsg = { role: "system", content: "You are a coding assistant" };
const firstUser = { role: "user", content: "What is a pointer?" };
const k1 = getConversationCacheKey({ messages: [systemMsg, firstUser] });
const k2 = getConversationCacheKey({
messages: [
systemMsg,
firstUser,
{ role: "assistant", content: "A pointer stores an address..." },
{ role: "user", content: "Can you give an example?" },
],
});
assert.equal(k1, k2);
});
it("prefers explicit conversation_id when present", () => {
const k1 = getConversationCacheKey({
conversation_id: "thread-abc-123",
messages: [{ role: "user", content: "hello" }],
});
const k2 = getConversationCacheKey({ conversation_id: "thread-abc-123" });
assert.equal(k1, k2);
assert.notEqual(k1, null);
});
it("different conversation_ids produce different keys", () => {
const k1 = getConversationCacheKey({ conversation_id: "thread-1" });
const k2 = getConversationCacheKey({ conversation_id: "thread-2" });
assert.notEqual(k1, k2);
});
});
// ── Conversation affinity (getOrSetConversationAffinityIndex) ─────────────────
describe("conversation affinity", () => {
before(() => clearConversationAffinity());
after(() => clearConversationAffinity());
it("returns currentIndex for a new conversation key", () => {
const idx = getOrSetConversationAffinityIndex("combo-x", "conv-aaa", 2);
assert.equal(idx, 2);
});
it("returns the pinned index for the same conversation key", () => {
clearConversationAffinity();
getOrSetConversationAffinityIndex("combo-y", "conv-bbb", 0);
// Second call should return the pinned index (0), not a new currentIndex (1)
const idx = getOrSetConversationAffinityIndex("combo-y", "conv-bbb", 1);
assert.equal(idx, 0);
});
it("different conversation keys get independent pins", () => {
clearConversationAffinity();
const i1 = getOrSetConversationAffinityIndex("combo-z", "conv-c1", 0);
const i2 = getOrSetConversationAffinityIndex("combo-z", "conv-c2", 1);
assert.equal(i1, 0);
assert.equal(i2, 1);
// Both pins should stay
assert.equal(getOrSetConversationAffinityIndex("combo-z", "conv-c1", 99), 0);
assert.equal(getOrSetConversationAffinityIndex("combo-z", "conv-c2", 99), 1);
});
it("clearConversationAffinity clears only the named combo prefix", () => {
clearConversationAffinity();
getOrSetConversationAffinityIndex("combo-a", "conv-1", 0);
getOrSetConversationAffinityIndex("combo-b", "conv-1", 1);
clearConversationAffinity("combo-a");
// combo-a entry should be gone
const sizeAfter = comboConversationAffinity.size;
assert.equal(sizeAfter, 1);
});
});
// ── Guard: non-task-aware strategy leaves targets unchanged ───────────────────
describe("non-task-aware strategy guard", () => {
it("reorderByTaskWeight is still safe to call externally but only wired for task strategies", () => {
// The function itself is pure — what matters is that combo.ts only calls it
// when isTaskRoutingStrategy() is true. Verify that explicitly.
const nonTaskStrategies = ["priority", "fallback", "round-robin", "cost-optimized"];
for (const strategy of nonTaskStrategies) {
assert.ok(!isTaskRoutingStrategy(strategy), `${strategy} should not trigger task routing`);
}
});
it("reorderByTaskWeight on standard task with equal-power models returns same reference", () => {
// If all scores are equal, no reordering → returns same array reference
const targets = [makeTarget("anthropic/claude-haiku-4.5", 0)];
const task = classifyTask({});
const out = reorderByTaskWeight(targets, task, new Set());
assert.strictEqual(out, targets);
});
});

View File

@@ -0,0 +1,101 @@
// tests/unit/combo/auto-status-penalty-4540.test.ts
// Regression for #4540: an exhausted provider connection (e.g. credits_exhausted /
// rate_limited with no numeric quota fetcher) used to score IDENTICALLY to a healthy
// one — quotaRemaining defaulted to 100 and testStatus never entered scoring — so auto
// routing kept picking dead providers. With the quota-preflight hard cutoff DISABLED
// (the default), we must NOT hard-block such a candidate (that would surface a misleading
// "below quota cutoff" 429); instead a SOFT status penalty must rank it strictly BELOW
// an otherwise-identical healthy candidate.
import { test } from "node:test";
import assert from "node:assert/strict";
import { scoreAutoTargets } from "../../../open-sse/services/combo/autoStrategy.ts";
import type {
AutoProviderCandidate,
ResolvedComboTarget,
} from "../../../open-sse/services/combo/types.ts";
import type { ScoringWeights } from "../../../open-sse/services/autoCombo/scoring.ts";
// Quota-weighted only so the two candidates would otherwise tie at quotaRemaining=100.
const quotaOnlyWeights: ScoringWeights = {
quota: 1,
health: 0,
costInv: 0,
latencyInv: 0,
taskFit: 0,
stability: 0,
tierPriority: 0,
tierAffinity: 0,
specificityMatch: 0,
contextAffinity: 0,
resetWindowAffinity: 0,
connectionDensity: 0,
};
function target(provider: string, model: string, connectionId: string): ResolvedComboTarget {
return {
kind: "model",
stepId: `${provider}-${model}-${connectionId}`,
executionKey: `${provider}/${model}@${connectionId}`,
modelStr: `${provider}/${model}`,
provider,
providerId: null,
connectionId,
} as ResolvedComboTarget;
}
function candidate(
provider: string,
model: string,
connectionId: string,
overrides: Partial<AutoProviderCandidate> = {}
): AutoProviderCandidate {
return {
provider,
model,
stepId: `${provider}-${model}-${connectionId}`,
executionKey: `${provider}/${model}@${connectionId}`,
modelStr: `${provider}/${model}`,
connectionId,
quotaRemaining: 100,
quotaTotal: 100,
circuitBreakerState: "CLOSED",
costPer1MTokens: 1,
p95LatencyMs: 1000,
latencyStdDev: 10,
errorRate: 0,
resetWindowAffinity: 0.5,
connectionPoolSize: 1,
...overrides,
} as AutoProviderCandidate;
}
test("#4540: exhausted (statusPenalty) candidate scores strictly BELOW an identical healthy one without being hard-blocked", () => {
const targets = [target("dead", "m", "dead-conn"), target("healthy", "m", "healthy-conn")];
const ranked = scoreAutoTargets(
targets,
[
// Exhausted connection with no numeric quota fetcher: quotaRemaining stays 100,
// but the connection terminal status flags a soft penalty (NOT a hard block).
candidate("dead", "m", "dead-conn", { statusPenalty: true }),
candidate("healthy", "m", "healthy-conn"),
],
"coding",
quotaOnlyWeights
);
// Soft penalty: NOT hard-blocked — both candidates are still in the pool.
assert.equal(ranked.length, 2, "soft penalty must not drop the exhausted candidate");
const dead = ranked.find((e) => e.target.provider === "dead");
const healthy = ranked.find((e) => e.target.provider === "healthy");
assert.ok(dead && healthy, "both candidates present");
// The fix: exhausted scores STRICTLY less than the identical healthy one (was a tie).
assert.ok(
(dead!.score as number) < (healthy!.score as number),
`exhausted score (${dead!.score}) must be strictly < healthy score (${healthy!.score})`
);
// And ranking puts healthy first.
assert.equal(ranked[0]?.target.provider, "healthy");
});

View File

@@ -0,0 +1,46 @@
// tests/unit/combo/effective-max-concurrency.test.ts
// Unit test for effectiveMaxConcurrency — the pure resolver that turns a
// connection's per-account concurrency cap (provider_connections.max_concurrent)
// into the semaphore's maxConcurrency for a round-robin combo target, falling
// back to the combo-level default when the connection has no positive cap.
//
// Runner: node:test + assert/strict (matches the sibling combo predicate tests).
import { test, describe } from "node:test";
import assert from "node:assert/strict";
import { effectiveMaxConcurrency } from "../../../open-sse/services/combo/comboPredicates.ts";
describe("effectiveMaxConcurrency", () => {
test("a positive per-connection cap wins over the fallback", () => {
assert.equal(effectiveMaxConcurrency(1, 3), 1, "GLM/MiniMax-style cap of 1 must be honored");
assert.equal(effectiveMaxConcurrency(5, 3), 5, "a cap higher than the default is still used");
});
test("null cap → fallback (no per-connection limit configured)", () => {
assert.equal(effectiveMaxConcurrency(null, 3), 3);
});
test("undefined cap → fallback", () => {
assert.equal(effectiveMaxConcurrency(undefined, 7), 7);
});
test("zero cap → fallback (0 means 'no limit', not 'block everything')", () => {
assert.equal(effectiveMaxConcurrency(0, 3), 3);
});
test("negative cap → fallback (defensive)", () => {
assert.equal(effectiveMaxConcurrency(-2, 4), 4);
});
test("non-finite cap → fallback (defensive)", () => {
assert.equal(effectiveMaxConcurrency(Number.NaN, 4), 4);
assert.equal(effectiveMaxConcurrency(Number.POSITIVE_INFINITY, 4), 4);
});
test("a non-integer positive cap is floored to a whole slot count", () => {
assert.equal(
effectiveMaxConcurrency(2.9, 3),
2,
"fractional caps floor to whole concurrency slots"
);
});
});

View File

@@ -0,0 +1,132 @@
/**
* tests/unit/combo/quota-share-concurrency.test.ts
*
* FASE 2.1: the per-connection concurrency slot for quota-share combos. These
* tests pin the contract of acquireQuotaShareConcurrencySlot against the real
* semaphore module: no limit when there is no cap, a stable connection-scoped
* key, genuine serialization (a second request WAITS until the first releases),
* and fail-open behavior when the queue is saturated.
*/
import test from "node:test";
import assert from "node:assert/strict";
import * as semaphore from "../../../open-sse/services/rateLimitSemaphore.ts";
import {
quotaShareConcurrencyKey,
acquireQuotaShareConcurrencySlot,
} from "../../../open-sse/services/combo/quotaShareConcurrency.ts";
const noopLog = { warn: () => {} };
function target(connectionId: string) {
return {
connectionId,
modelStr: "p/m",
executionKey: "p/m",
provider: "p",
stepId: "s",
label: "p/m",
} as never;
}
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
test("quotaShareConcurrencyKey is stable and connection-scoped", () => {
assert.equal(quotaShareConcurrencyKey("abc"), "qsconn:abc");
assert.equal(quotaShareConcurrencyKey("abc"), quotaShareConcurrencyKey("abc"));
assert.notEqual(quotaShareConcurrencyKey("a"), quotaShareConcurrencyKey("b"));
});
test("no slot when cap is null (no per-connection limit → unchanged behavior)", async () => {
semaphore.resetAll();
const release = await acquireQuotaShareConcurrencySlot(
target("c1"),
null,
{ queueTimeoutMs: 50, maxQueueSize: 10 },
noopLog
);
assert.equal(release, null);
});
test("no slot when cap <= 0", async () => {
semaphore.resetAll();
assert.equal(
await acquireQuotaShareConcurrencySlot(
target("c1"),
0,
{ queueTimeoutMs: 50, maxQueueSize: 10 },
noopLog
),
null
);
});
test("no slot when connectionId is empty", async () => {
semaphore.resetAll();
const release = await acquireQuotaShareConcurrencySlot(
target(""),
1,
{ queueTimeoutMs: 50, maxQueueSize: 10 },
noopLog
);
assert.equal(release, null);
});
test("acquires a slot when a positive cap is set", async () => {
semaphore.resetAll();
const release = await acquireQuotaShareConcurrencySlot(
target("c1"),
1,
{ queueTimeoutMs: 50, maxQueueSize: 10 },
noopLog
);
assert.equal(typeof release, "function");
release!();
});
test("cap=1 serializes: a second concurrent request WAITS until the first releases", async () => {
semaphore.resetAll();
const opts = { queueTimeoutMs: 1000, maxQueueSize: 10 };
const r1 = await acquireQuotaShareConcurrencySlot(target("c1"), 1, opts, noopLog);
assert.equal(typeof r1, "function", "first request acquires the only slot");
let secondResolved = false;
const p2 = acquireQuotaShareConcurrencySlot(target("c1"), 1, opts, noopLog).then((r) => {
secondResolved = true;
return r;
});
await wait(60);
assert.equal(
secondResolved,
false,
"second request is still queued while the first holds the slot"
);
r1!(); // release the first
const r2 = await p2;
assert.equal(secondResolved, true, "second request resolves only after the first releases");
assert.equal(typeof r2, "function", "second request then acquires the freed slot");
r2!();
});
test("fail-open: a saturated queue proceeds without a slot (null), never blocks", async () => {
semaphore.resetAll();
const opts = { queueTimeoutMs: 1000, maxQueueSize: 0 };
const r1 = await acquireQuotaShareConcurrencySlot(target("c1"), 1, opts, noopLog);
assert.equal(typeof r1, "function", "first request acquires");
const r2 = await acquireQuotaShareConcurrencySlot(target("c1"), 1, opts, noopLog);
assert.equal(r2, null, "queue full → fail-open null (availability never worsened)");
r1!();
});
test("different connections have independent gates (no cross-contention)", async () => {
semaphore.resetAll();
const opts = { queueTimeoutMs: 50, maxQueueSize: 0 };
const r1 = await acquireQuotaShareConcurrencySlot(target("c1"), 1, opts, noopLog);
const r2 = await acquireQuotaShareConcurrencySlot(target("c2"), 1, opts, noopLog);
assert.equal(typeof r1, "function");
assert.equal(typeof r2, "function", "a different connection has its own slot");
r1!();
r2!();
});

View File

@@ -0,0 +1,45 @@
/**
* Security regression (#4694 sibling-path gate parity): the compression
* run-telemetry route MUST authenticate before disclosing telemetry, mirroring
* its sibling `settings/compression/route.ts`. A behavioral 401 test would need
* full DB/settings bootstrap (isAuthenticated reads settings), so this is a
* source guard that fails if the auth gate is removed or reordered after the
* data call — the same wiring-guard pattern used elsewhere in the suite.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROUTE = join(
__dirname,
"../../src/app/api/settings/compression/run-telemetry/route.ts"
);
test("run-telemetry GET imports isAuthenticated from the shared auth util", () => {
const src = readFileSync(ROUTE, "utf8");
assert.match(src, /import\s*\{[^}]*\bisAuthenticated\b[^}]*\}\s*from\s*["']@\/shared\/utils\/apiAuth["']/);
});
test("run-telemetry GET gates on auth and returns 401 before reading telemetry", () => {
const src = readFileSync(ROUTE, "utf8");
const authIdx = src.indexOf("isAuthenticated(request)");
const unauthorizedIdx = src.search(/status:\s*401/);
const dataIdx = src.indexOf("getCompressionRunTelemetrySummary(");
assert.ok(authIdx > 0, "must call isAuthenticated(request)");
assert.ok(unauthorizedIdx > 0, "must return a 401 on failed auth");
assert.ok(dataIdx > 0, "must call getCompressionRunTelemetrySummary");
// The auth check and its 401 must come BEFORE the telemetry read.
assert.ok(authIdx < dataIdx, "auth check must precede the telemetry read");
assert.ok(unauthorizedIdx < dataIdx, "401 response must precede the telemetry read");
});
test("run-telemetry GET receives the request object (so auth can read it)", () => {
const src = readFileSync(ROUTE, "utf8");
assert.match(src, /export\s+async\s+function\s+GET\s*\(\s*request\s*:/);
});

View File

@@ -0,0 +1,59 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
extractPreservedBlocks,
restorePreservedBlocks,
} from "../../../open-sse/services/compression/preservation.ts";
// Regression guard for #4795: a single unmatched `$` followed by a run of
// consecutive backslashes (e.g. Windows paths pasted into a prompt) triggered
// catastrophic backtracking in the `math_inline` built-in pattern, pinning one
// CPU core at 100% and freezing the event loop. The alternation
// `(?:\\.|[^$\n])` was ambiguous: a backslash could be consumed either by the
// escape branch `\\.` or by the catch-all `[^$\n]`, so a run of N backslashes
// could be tiled exponentially many ways before the trailing `$` anchor failed.
describe("preservation ReDoS guard (#4795)", () => {
it("does not hang on a `$` followed by many consecutive backslashes (no closing `$`)", () => {
// Without the fix this blows up exponentially (N=38 already exceeds 8s).
const evil = "$x" + "\\".repeat(200) + "y";
const start = Date.now();
const { text } = extractPreservedBlocks(evil);
const elapsed = Date.now() - start;
assert.ok(
elapsed < 1000,
`extractPreservedBlocks must not backtrack catastrophically (took ${elapsed}ms)`
);
// The pathological input is not valid inline math, so it stays untouched.
assert.equal(text, evil, "non-math text must be returned unchanged");
});
it("does not hang on Windows-path-style payloads with backslashes after a `$`", () => {
const evil =
"$C:\\Users\\Alpha\\Net\\DESKTOP\\" + "sub\\".repeat(120) + "no-closing-dollar";
const start = Date.now();
extractPreservedBlocks(evil);
assert.ok(Date.now() - start < 1000, "Windows-path payload must resolve quickly");
});
it("still preserves legitimate inline math", () => {
const text = "The identity $E=mc^2$ and $\\alpha + \\beta$ are classics.";
const { text: extracted, blocks } = extractPreservedBlocks(text);
const mathBlocks = blocks.filter((b) => b.kind === "math_inline");
assert.equal(mathBlocks.length, 2, "both inline-math spans must be preserved");
assert.ok(!extracted.includes("E=mc^2"), "math content must be replaced by a placeholder");
assert.equal(
restorePreservedBlocks(extracted, blocks),
text,
"round-trip must reproduce the original text"
);
});
it("preserves inline math that contains an escaped dollar", () => {
const text = "Price math: $a \\$ b$ end.";
const { blocks } = extractPreservedBlocks(text);
assert.ok(
blocks.some((b) => b.kind === "math_inline" && b.content === "$a \\$ b$"),
"inline math with an escaped dollar must still be captured via the `\\.` branch"
);
});
});

View File

@@ -0,0 +1,108 @@
/**
* TDD regression for #4559: RTK compression over-truncates tool-result content.
*
* A user reported that when a tool returns a file's contents (e.g. a ~147-line
* text/code file via a Read tool), RTK's default caps (maxLinesPerResult: 120,
* maxCharsPerResult: 12000) drop the middle and most lines are silently
* suppressed. Disabling compression OR the RTK "tool results" toggle fixes it;
* OpenRouter (no OmniRoute) is unaffected.
*
* Root cause: the final `smartTruncate` hard-cap in processRtkText fires for ANY
* tool result over 120 lines — including document/file reads — even though RTK's
* intent is repetitive *command output* (npm install logs, make, docker logs),
* NOT prose/code file reads.
*
* Fix: when the content is NOT a recognized repetitive command/log output
* (detection.type === "unknown" with no detected command — i.e. a document-style
* read), RTK must NOT apply the line/char truncation that drops the middle.
* Genuine repetitive logs (which detect as a known command type) are unaffected,
* preserving RTK's value there.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { processRtkText } from "../../../open-sse/services/compression/engines/rtk/index.ts";
// A ~147-line mixed prose/code file read — every line is distinct content, NOT
// repetitive log noise. No shell-command prefix, so RTK detects it as "unknown".
function buildFileRead(): string {
const lines: string[] = [];
lines.push("# Module: payment processor");
lines.push("");
lines.push("This module reconciles charges against the ledger.");
lines.push("");
lines.push("import { Decimal } from './decimal';");
lines.push("import { Ledger } from './ledger';");
lines.push("");
for (let i = 0; i < 70; i++) {
lines.push(`export function step${i}(input: Input${i}): Result${i} {`);
lines.push(` // reconcile bucket ${i} against the prior settlement window`);
lines.push(` const value = computeBucket${i}(input.amount, input.currency);`);
lines.push(` const settled = applyDiscount${i}(value, input.coupon);`);
lines.push(` return { id: ${i}, value: settled, settledAt: input.timestamp${i} };`);
lines.push("}");
lines.push("");
}
return lines.join("\n");
}
describe("RTK file-read tool result — #4559 no over-truncation", () => {
it("does not drop the middle of a 147-line document/file read", () => {
const fileRead = buildFileRead();
const totalLines = fileRead.split("\n").length;
assert.ok(totalLines > 120, `fixture should exceed default cap; got ${totalLines}`);
// Default RTK tool-result path (applyToToolResults defaults true; this is the
// text path the Anthropic/OpenAI tool_result blocks feed into).
const result = processRtkText(fileRead, {
config: { maxLinesPerResult: 120, maxCharsPerResult: 12000 },
});
// The hard-cap truncation must NOT have fired for a document/file read.
assert.ok(
!result.techniquesUsed.includes("rtk-truncate"),
`file-read content must not be hard-cap truncated; techniquesUsed: ${result.techniquesUsed.join(
", "
)}`
);
// The truncation marker must be absent and the middle content retained.
assert.ok(
!result.text.includes("[rtk:truncated"),
"no truncation marker should be injected into a file read"
);
// Sample lines from the head, middle, and tail must all survive.
assert.ok(result.text.includes("step0("), "head content survives");
assert.ok(result.text.includes("step35("), "middle content survives");
assert.ok(result.text.includes("step69("), "tail content survives");
// Output retains the vast majority of the lines (not dropped below a
// reasonable threshold).
const outLines = result.text.split("\n").length;
assert.ok(
outLines >= totalLines * 0.9,
`expected to retain >=90% of lines; kept ${outLines}/${totalLines}`
);
});
it("still truncates genuine repetitive command output (RTK value preserved)", () => {
// npm install-style repetitive log lines, with a detected command so RTK
// recognizes it as repetitive output worth truncating.
const noise = Array.from(
{ length: 300 },
(_, i) => `added package-${i}@1.0.0 to node_modules`
).join("\n");
const input = `$ npm install\n${noise}`;
const result = processRtkText(input, {
command: "npm install",
config: { maxLinesPerResult: 120, maxCharsPerResult: 12000 },
});
assert.ok(
result.compressed,
"repetitive npm install output should still be compressed"
);
});
});

View File

@@ -0,0 +1,96 @@
// Port of decolua/9router#779: GitHub Copilot Agent Mode & Antigravity "Invalid Argument" parity.
//
// Upstream symptom: VS Code GitHub Copilot Chat (in Agent mode) requests
// `maxOutputTokens` values well above what the Antigravity Cloud Code backend
// will accept — the backend rejects the call with HTTP 400 "Invalid Argument"
// even though the request envelope is otherwise valid. The same trigger is
// repeatedly observed in upstream issue reports and in OmniRoute connection
// cooldowns spiking on Copilot traffic.
//
// Fix: hard-cap `generationConfig.maxOutputTokens` to MAX_ANTIGRAVITY_OUTPUT_TOKENS
// (16384) so any oversized client request is silently shrunk to a value the
// upstream accepts. Smaller values are left untouched, and the cap applies
// independently of the thinkingBudget bump logic that already lives in
// `applyAntigravityGenerationDefaults`.
import test from "node:test";
import assert from "node:assert/strict";
import {
MAX_ANTIGRAVITY_OUTPUT_TOKENS,
__test_applyAntigravityGenerationDefaults as applyAntigravityGenerationDefaults,
} from "../../open-sse/executors/antigravity.ts";
test("MAX_ANTIGRAVITY_OUTPUT_TOKENS is fixed at 16384 (upstream-accepted ceiling)", () => {
assert.equal(MAX_ANTIGRAVITY_OUTPUT_TOKENS, 16384);
});
test("Copilot-style oversized maxOutputTokens is clamped to 16384 to avoid Antigravity 400 'Invalid Argument'", () => {
// Reproduces the upstream-rejected envelope: VS Code GitHub Copilot Chat in
// Agent mode commonly requests 32K65K output tokens.
const request: Record<string, unknown> = {
generationConfig: {
maxOutputTokens: 65536,
},
};
applyAntigravityGenerationDefaults(request);
const gc = request.generationConfig as Record<string, unknown>;
assert.equal(gc.maxOutputTokens, MAX_ANTIGRAVITY_OUTPUT_TOKENS);
});
test("maxOutputTokens at or below the cap is left untouched", () => {
const request: Record<string, unknown> = {
generationConfig: {
maxOutputTokens: 8192,
},
};
applyAntigravityGenerationDefaults(request);
const gc = request.generationConfig as Record<string, unknown>;
assert.equal(gc.maxOutputTokens, 8192);
});
test("maxOutputTokens exactly at the cap is left untouched (boundary)", () => {
const request: Record<string, unknown> = {
generationConfig: {
maxOutputTokens: MAX_ANTIGRAVITY_OUTPUT_TOKENS,
},
};
applyAntigravityGenerationDefaults(request);
const gc = request.generationConfig as Record<string, unknown>;
assert.equal(gc.maxOutputTokens, MAX_ANTIGRAVITY_OUTPUT_TOKENS);
});
test("Cap is applied even when no generationConfig is provided initially", () => {
// The defaults helper synthesises generationConfig — the cap must not crash
// and must leave a well-formed object behind.
const request: Record<string, unknown> = {};
applyAntigravityGenerationDefaults(request);
const gc = request.generationConfig as Record<string, unknown>;
assert.equal(typeof gc, "object");
// No maxOutputTokens was requested — the cap must not invent one.
assert.equal(gc.maxOutputTokens, undefined);
});
test("Cap interacts safely with thinkingBudget bump: bump still wins when budget exceeds tokens, then the cap still clamps the bumped value", () => {
// thinkingBudget bumps maxOutputTokens to floor(budget)+1 when it exceeds
// the requested ceiling; if the bump itself overshoots the AG cap, the cap
// must still apply.
const request: Record<string, unknown> = {
generationConfig: {
maxOutputTokens: 1000,
thinkingConfig: { thinkingBudget: 20000 },
},
};
applyAntigravityGenerationDefaults(request);
const gc = request.generationConfig as Record<string, unknown>;
assert.equal(gc.maxOutputTokens, MAX_ANTIGRAVITY_OUTPUT_TOKENS);
});

View File

@@ -0,0 +1,93 @@
/**
* Regression test ported from decolua/9router#1536 (follow-up to #663).
*
* GitHub Copilot's `/responses` endpoint only serves OpenAI (gpt/codex) models —
* Gemini and Claude models reject with HTTP 400
* "[github/<model>] [400]: model <model> does not support Responses API."
* (unsupported_api_for_model). They must therefore never be routed to
* `/responses`, no matter what `targetFormat` a registry entry (or any future
* misconfiguration) declares for them.
*
* OmniRoute's GithubExecutor decides the endpoint inside `buildUrl()` purely
* from `getModelTargetFormat("gh", model)`. The registry today (`open-sse/
* config/providers/registry/github/index.ts`) correctly omits
* `targetFormat: "openai-responses"` on every Claude/Gemini entry — but that is
* a single-line away from regressing (the file's own comments on lines 60, 74,
* 80 warn against it). This test pins the defensive invariant: even if a
* Claude/Gemini variant ends up tagged `openai-responses`, the executor must
* still build the chat/completions URL.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { GithubExecutor } from "../../open-sse/executors/github.ts";
import { PROVIDER_MODELS } from "../../open-sse/config/providerModels.ts";
const CHAT_URL = "https://api.githubcopilot.com/chat/completions";
const RESPONSES_URL = "https://api.githubcopilot.com/responses";
describe("GithubExecutor — Gemini/Claude must never hit /responses (port 9router#1536)", () => {
it("routes registered Claude/Gemini Copilot models to chat/completions", () => {
const exec = new GithubExecutor();
for (const id of [
"claude-haiku-4.5",
"claude-sonnet-4.5",
"claude-sonnet-4.6",
"claude-opus-4.6",
"claude-opus-4.7",
"claude-opus-4-5-20251101",
"gemini-3.1-pro-preview",
"gemini-3-flash-preview",
]) {
assert.equal(exec.buildUrl(id, false), CHAT_URL, `${id} must route to chat/completions`);
}
});
it("still uses chat/completions if a Claude/Gemini model is wrongly tagged openai-responses", () => {
const exec = new GithubExecutor();
const ghModels = PROVIDER_MODELS["gh"];
assert.ok(Array.isArray(ghModels), "gh provider models registry must exist");
const claude = ghModels.find((m: any) => m.id === "claude-sonnet-4.6");
const gemini = ghModels.find((m: any) => m.id === "gemini-3.1-pro-preview");
assert.ok(claude && gemini, "claude-sonnet-4.6 and gemini-3.1-pro-preview must be registered");
const originalClaude = claude.targetFormat;
const originalGemini = gemini.targetFormat;
try {
// Simulate a future misconfiguration. The guard must still hold.
(claude as any).targetFormat = "openai-responses";
(gemini as any).targetFormat = "openai-responses";
assert.equal(exec.buildUrl("claude-sonnet-4.6", false), CHAT_URL);
assert.equal(exec.buildUrl("gemini-3.1-pro-preview", false), CHAT_URL);
} finally {
(claude as any).targetFormat = originalClaude;
(gemini as any).targetFormat = originalGemini;
}
});
it("still routes registered OpenAI/codex Copilot models to /responses", () => {
const exec = new GithubExecutor();
for (const id of ["gpt-5-mini", "gpt-5.3-codex", "gpt-5.4-mini", "gpt-5.4"]) {
assert.equal(exec.buildUrl(id, false), RESPONSES_URL, `${id} must route to /responses`);
}
});
it("is case-insensitive when guarding (defensive against upper/mixed-case ids)", () => {
const exec = new GithubExecutor();
const ghModels = PROVIDER_MODELS["gh"];
const claude = ghModels.find((m: any) => m.id === "claude-sonnet-4.6");
const original = claude.targetFormat;
try {
(claude as any).targetFormat = "openai-responses";
// Look up by the same id (registry is case-sensitive on lookup) but with a
// mixed-case path through the guard. We rebuild with the registered id;
// the guard normalizes before substring check, so it must still detect.
assert.equal(exec.buildUrl("claude-sonnet-4.6", false), CHAT_URL);
} finally {
(claude as any).targetFormat = original;
}
});
});

View File

@@ -0,0 +1,29 @@
import test from "node:test";
import assert from "node:assert/strict";
test("runOmniRouteCli: missing command returns error", async () => {
const { getCopilotTool } = await import("../../src/lib/copilot/tools.ts");
const tool = getCopilotTool("runOmniRouteCli");
assert.ok(tool);
const result = await tool.handler({});
assert.equal(result, "Please provide a command to execute.");
});
test("runOmniRouteCli: empty command returns error", async () => {
const { getCopilotTool } = await import("../../src/lib/copilot/tools.ts");
const tool = getCopilotTool("runOmniRouteCli");
assert.ok(tool);
const result = await tool.handler({ command: "" });
assert.equal(result, "Please provide a command to execute.");
});
test("runOmniRouteCli: returns CLI-not-found when omniroute unavailable", async () => {
const { getCopilotTool } = await import("../../src/lib/copilot/tools.ts");
const tool = getCopilotTool("runOmniRouteCli");
assert.ok(tool);
const result = await tool.handler({ command: "health" });
assert.ok(
result.includes("omniroute CLI not found in PATH"),
`Expected CLI-not-found message, got: ${result}`
);
});

View File

@@ -0,0 +1,156 @@
/**
* TDD test for per-endpoint custom system prompt injection (port of upstream #2063).
*
* Validates that injectCustomSystemPrompt():
* - Appends the custom prompt to an existing system message (string content)
* - Appends the custom prompt to an existing system message (array content)
* - Creates a new system message when none exists
* - Injects into Claude-style `system` field (string)
* - Injects into Claude-style `system` field (array)
* - Does NOT inject when prompt is empty string
* - Does NOT inject when body has _skipSystemPrompt flag
* - Leaves body unchanged when prompt is falsy
*
* Also validates settings defaults include customSystemPromptEnabled / customSystemPrompt.
*/
import test from "node:test";
import assert from "node:assert/strict";
const { injectCustomSystemPrompt } = await import(
"../../open-sse/services/systemPrompt.ts"
);
// ─── injectCustomSystemPrompt ────────────────────────────────────────────────
test("injectCustomSystemPrompt: appends to existing string system message", () => {
const body = {
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "Hello" },
],
};
const result = injectCustomSystemPrompt(body, "Always respond formally.");
const sysMsg = result.messages.find((m) => m.role === "system");
assert.ok(sysMsg, "system message must exist");
assert.ok(
typeof sysMsg.content === "string" && sysMsg.content.includes("You are a helpful assistant."),
"original content preserved"
);
assert.ok(
typeof sysMsg.content === "string" && sysMsg.content.includes("Always respond formally."),
"custom prompt appended"
);
});
test("injectCustomSystemPrompt: appends to existing array-content system message", () => {
const body = {
messages: [
{ role: "system", content: [{ type: "text", text: "Base instructions." }] },
{ role: "user", content: "Hi" },
],
};
const result = injectCustomSystemPrompt(body, "Speak like a pirate.");
const sysMsg = result.messages.find((m) => m.role === "system");
assert.ok(sysMsg, "system message must exist");
assert.ok(Array.isArray(sysMsg.content), "content must remain array");
const texts = (sysMsg.content as Array<{ type: string; text: string }>).map((c) => c.text);
assert.ok(texts.includes("Base instructions."), "original block preserved");
assert.ok(texts.includes("Speak like a pirate."), "custom prompt block appended");
});
test("injectCustomSystemPrompt: creates system message when none exists", () => {
const body = {
messages: [{ role: "user", content: "What is 2+2?" }],
};
const result = injectCustomSystemPrompt(body, "Think step by step.");
assert.equal(result.messages[0].role, "system", "system message prepended");
assert.equal(result.messages[0].content, "Think step by step.", "prompt is the content");
assert.equal(result.messages[1].role, "user", "user message preserved after system");
});
test("injectCustomSystemPrompt: injects into Claude-style string system field", () => {
const body = {
system: "You are Claude.",
messages: [{ role: "user", content: "Hello" }],
};
const result = injectCustomSystemPrompt(body, "Be concise.");
assert.ok(typeof result.system === "string", "system field still a string");
assert.ok(result.system.includes("You are Claude."), "original system preserved");
assert.ok(result.system.includes("Be concise."), "custom prompt appended to system");
});
test("injectCustomSystemPrompt: injects into Claude-style array system field", () => {
const body = {
system: [{ type: "text", text: "You are Claude." }],
messages: [{ role: "user", content: "Hello" }],
};
const result = injectCustomSystemPrompt(body, "Be concise.");
assert.ok(Array.isArray(result.system), "system field still an array");
const texts = (result.system as Array<{ type: string; text: string }>).map((c) => c.text);
assert.ok(texts.includes("You are Claude."), "original system block preserved");
assert.ok(texts.includes("Be concise."), "custom prompt block appended");
});
test("injectCustomSystemPrompt: no-op when prompt is empty string", () => {
const body = {
messages: [{ role: "user", content: "Hello" }],
};
const result = injectCustomSystemPrompt(body, "");
assert.deepEqual(result, body, "body unchanged when prompt is empty");
});
test("injectCustomSystemPrompt: no-op when body has _skipSystemPrompt flag", () => {
const body = {
_skipSystemPrompt: true,
messages: [{ role: "user", content: "Hello" }],
};
const result = injectCustomSystemPrompt(body as Record<string, unknown>, "Be formal.");
assert.deepEqual(result, body, "body unchanged when _skipSystemPrompt is set");
});
test("injectCustomSystemPrompt: does not mutate input body (immutable)", () => {
const body = {
messages: [
{ role: "system", content: "Original." },
{ role: "user", content: "Hi" },
],
};
const original = JSON.stringify(body);
injectCustomSystemPrompt(body, "New instruction.");
assert.equal(JSON.stringify(body), original, "input body not mutated");
});
// ─── Settings defaults ───────────────────────────────────────────────────────
// Verify that getSettings() returns the expected defaults for the new keys.
// Uses an in-memory DB (DATA_DIR set to a temp path so no production DB is touched).
import { tmpdir } from "node:os";
import { join } from "node:path";
import { mkdirSync } from "node:fs";
const tmpDir = join(tmpdir(), `omniroute-test-${Date.now()}`);
mkdirSync(tmpDir, { recursive: true });
process.env["DATA_DIR"] = tmpDir;
test("settings defaults include customSystemPromptEnabled=false and customSystemPrompt=''", async (t) => {
// Dynamic import after setting DATA_DIR to avoid polluting other tests
const { resetDbInstance } = await import("../../src/lib/db/core.ts");
const { getSettings } = await import("../../src/lib/db/settings.ts");
const settings = await getSettings();
assert.equal(
settings.customSystemPromptEnabled,
false,
"customSystemPromptEnabled default is false"
);
assert.equal(
settings.customSystemPrompt,
"",
"customSystemPrompt default is empty string"
);
t.after(() => {
resetDbInstance();
});
});

View File

@@ -0,0 +1,109 @@
import test from "node:test";
import assert from "node:assert/strict";
const { getModelCatalogSourceLabel, normalizeModelCatalogSource } = await import(
"../../src/shared/utils/modelCatalogSearch.ts"
);
// Ported from upstream PR decolua/9router#2018 (Hamsa_M):
// custom (openai-/anthropic-compatible) providers in the combo model-select modal
// now dynamically fetch their model catalog from the provider's upstream `/models`
// endpoint, merge it with alias/fallback/custom models (deduped by id) and tag the
// fetched entries with an "auto" source badge.
test('the "auto" source normalizes to itself and renders the "Auto" badge', () => {
// Before the port the modal had no way to tag dynamically-fetched models, so an
// unknown source fell through to "system" / "Built-in", which is misleading.
assert.equal(normalizeModelCatalogSource("auto"), "auto");
assert.equal(getModelCatalogSourceLabel("auto"), "Auto");
});
test('"auto" must not collide with the "auto-sync" import alias', () => {
// `auto-sync` is an existing synced-import alias and must keep mapping to imported.
assert.equal(normalizeModelCatalogSource("auto-sync"), "imported");
assert.equal(getModelCatalogSourceLabel("auto-sync"), "Imported");
});
// Mirrors the modal's merge step: alias models win, fetched ("auto") models fill the
// gaps, deduped by id against alias + fallback + custom entries.
function mergeFetchedModels(
nodePrefix: string,
nodeModels: Array<{ id: string }>,
fallbackEntries: Array<{ id: string }>,
customEntries: Array<{ id: string }>,
fetched: Array<Record<string, string>>
) {
const fetchedEntries = fetched
.map((m) => {
const id = m.id || m.slug || m.model || m.name;
return {
id,
name: m.name || m.displayName || id,
value: `${nodePrefix}/${id}`,
isFetched: true,
source: "auto",
};
})
.filter(
(fm) =>
fm.id &&
!nodeModels.some((nm) => nm.id === fm.id) &&
!fallbackEntries.some((fbm) => fbm.id === fm.id) &&
!customEntries.some((cm) => cm.id === fm.id)
);
return [...nodeModels, ...fallbackEntries, ...customEntries, ...fetchedEntries];
}
test("fetched models merge with alias models, deduping by id", () => {
const nodeModels = [{ id: "gpt-4", name: "GPT-4", value: "p/gpt-4" }];
const fetched = [
{ id: "gpt-4", name: "GPT-4 Turbo" }, // duplicate of an alias → dropped
{ id: "gpt-3.5", name: "GPT-3.5" }, // new → kept, tagged auto
];
const merged = mergeFetchedModels("p", nodeModels, [], [], fetched) as Array<{
id: string;
source?: string;
}>;
assert.equal(merged.length, 2);
assert.deepEqual(
merged.map((m) => m.id).sort(),
["gpt-3.5", "gpt-4"]
);
const auto = merged.find((m) => m.id === "gpt-3.5");
assert.equal(auto?.source, "auto");
// The pre-existing alias entry keeps its original (non-auto) identity.
assert.equal(merged.find((m) => m.id === "gpt-4")?.source, undefined);
});
test("fetched ids fall back across id/slug/model/name keys", () => {
const merged = mergeFetchedModels(
"p",
[],
[],
[],
[{ slug: "llama-3" }, { model: "mixtral" }, { name: "qwen" }]
) as Array<{ id: string; value: string }>;
assert.deepEqual(
merged.map((m) => m.id).sort(),
["llama-3", "mixtral", "qwen"]
);
assert.equal(merged.find((m) => m.id === "llama-3")?.value, "p/llama-3");
});
test("fetched entries are deduped against fallback and custom models too", () => {
const merged = mergeFetchedModels(
"p",
[],
[{ id: "fb-model" }],
[{ id: "custom-model" }],
[{ id: "fb-model" }, { id: "custom-model" }, { id: "brand-new" }]
) as Array<{ id: string; source?: string }>;
// Only the genuinely-new model survives as an auto entry.
const autoEntries = merged.filter((m) => m.source === "auto");
assert.deepEqual(
autoEntries.map((m) => m.id),
["brand-new"]
);
});

View File

@@ -146,6 +146,81 @@ test("database log settings mirror the runtime pipeline toggle", async () => {
assert.equal(databaseSettings.getUserDatabaseSettings().logs.callLogPipelineEnabled, true);
});
test("database optimization settings apply SQLite cache size immediately", () => {
const current = databaseSettings.getUserDatabaseSettings();
databaseSettings.updateDatabaseSettings({
optimization: {
...current.optimization,
autoVacuumMode: core.getAutoVacuumMode(),
pageSize: 4096,
cacheSize: 16384,
},
});
const db = core.getDbInstance();
const stored = db
.prepare(
"SELECT value FROM key_value WHERE namespace = 'databaseSettings' AND key = 'optimization.cacheSize'"
)
.get() as { value: string } | undefined;
assert.equal(db.pragma("cache_size", { simple: true }), -16384);
assert.equal(JSON.parse(stored?.value ?? "null"), 16384);
assert.equal(databaseSettings.getUserDatabaseSettings().optimization.cacheSize, 16384);
});
test("database optimization settings apply SQLite page size immediately", () => {
const current = databaseSettings.getUserDatabaseSettings();
databaseSettings.updateDatabaseSettings({
optimization: {
...current.optimization,
autoVacuumMode: core.getAutoVacuumMode(),
pageSize: 8192,
cacheSize: 16384,
},
});
assert.equal(core.getDbInstance().pragma("page_size", { simple: true }), 8192);
assert.equal(databaseSettings.getUserDatabaseSettings().optimization.pageSize, 8192);
});
test("database optimization cache size is applied when the DB is reopened", () => {
const db = core.getDbInstance();
db.prepare(
"INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('databaseSettings', ?, ?)"
).run("optimization.cacheSize", JSON.stringify(32768));
core.resetDbInstance();
const reopened = core.getDbInstance();
assert.equal(reopened.pragma("cache_size", { simple: true }), -32768);
});
test("database optimization rejects negative cache size through the API", async () => {
const current = databaseSettings.getUserDatabaseSettings();
const response = await databaseSettingsRoute.PATCH(
makeJsonRequest("PATCH", {
optimization: {
...current.optimization,
cacheSize: -2000,
},
}) as never
);
assert.equal(response.status, 400);
});
test("database settings reader normalizes legacy negative cache size to the positive default", () => {
const db = core.getDbInstance();
db.prepare(
"INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('databaseSettings', ?, ?)"
).run("optimization.cacheSize", JSON.stringify(-2000));
assert.equal(databaseSettings.getUserDatabaseSettings().optimization.cacheSize, 16384);
});
test("purgeDetailedLogs deletes request_detail_logs", async () => {
const db = core.getDbInstance();
db.prepare("INSERT INTO request_detail_logs (id, timestamp, duration_ms) VALUES (?, ?, ?)").run(

View File

@@ -0,0 +1,70 @@
// Characterization of the db/core.ts column-mapping split (god-file decomposition): the pure
// snake_case ↔ camelCase row helpers moved into db/caseMapping.ts. Behavior-preserving move — these
// locks pin the conversion semantics (boolean coercion for known flag columns, JSON parsing for
// providerSpecificData + `_json` TEXT columns, null-stripping) and that core.ts still re-exports them
// for the historical `from "./core"` import surface.
import { test } from "node:test";
import assert from "node:assert/strict";
const M = await import("../../src/lib/db/caseMapping.ts");
const CORE = await import("../../src/lib/db/core.ts");
test("module exposes the five mapping helpers", () => {
for (const name of ["toSnakeCase", "toCamelCase", "objToSnake", "rowToCamel", "cleanNulls"]) {
assert.equal(typeof (M as Record<string, unknown>)[name], "function", `missing ${name}`);
}
});
test("core.ts re-exports the same helpers (historical surface preserved)", () => {
for (const name of ["toSnakeCase", "toCamelCase", "objToSnake", "rowToCamel", "cleanNulls"]) {
assert.equal((CORE as Record<string, unknown>)[name], (M as Record<string, unknown>)[name]);
}
});
test("toSnakeCase / toCamelCase round-trip column names", () => {
assert.equal(M.toSnakeCase("maxRequestsPerDay"), "max_requests_per_day");
assert.equal(M.toCamelCase("max_requests_per_day"), "maxRequestsPerDay");
});
test("objToSnake converts keys, passes non-objects through", () => {
assert.deepEqual(M.objToSnake({ providerId: "x", isActive: 1 }), {
provider_id: "x",
is_active: 1,
});
assert.equal(M.objToSnake(null), null);
assert.equal(M.objToSnake("str"), "str");
});
test("rowToCamel coerces known boolean flags from 0/1", () => {
const out = M.rowToCamel({ is_active: 1, proxy_enabled: 0, per_key_proxy_enabled: 1 });
assert.equal(out?.isActive, true);
assert.equal(out?.proxyEnabled, false);
assert.equal(out?.perKeyProxyEnabled, true);
assert.equal(M.rowToCamel(null), null);
});
test("rowToCamel parses providerSpecificData JSON string", () => {
const out = M.rowToCamel({ provider_specific_data: '{"a":1}' });
assert.deepEqual(out?.providerSpecificData, { a: 1 });
// invalid JSON falls back to the raw string
const bad = M.rowToCamel({ provider_specific_data: "not json" });
assert.equal(bad?.providerSpecificData, "not json");
});
test("rowToCamel unwraps `_json` columns to the base key", () => {
const out = M.rowToCamel({ quota_window_thresholds_json: '{"hi":80}' });
assert.deepEqual(out?.quotaWindowThresholds, { hi: 80 });
assert.equal("quotaWindowThresholdsJson" in (out ?? {}), false);
// NULL `_json` column normalizes the base key to null
const nul = M.rowToCamel({ quota_window_thresholds_json: null });
assert.equal(nul?.quotaWindowThresholds, null);
});
test("cleanNulls drops null/undefined, keeps falsy-but-present values", () => {
assert.deepEqual(M.cleanNulls({ a: 0, b: "", c: null, d: undefined, e: false }), {
a: 0,
b: "",
e: false,
});
assert.deepEqual(M.cleanNulls(null), {});
});

View File

@@ -20,6 +20,7 @@ process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const poolsDb = await import("../../src/lib/db/quotaPools.ts");
const { getDbInstance } = core;
async function resetStorage() {
core.resetDbInstance();
@@ -260,3 +261,61 @@ test("allocation stores optional capValue and capUnit correctly", () => {
assert.equal(alloc.capValue, 1000);
assert.equal(alloc.capUnit, "requests");
});
// ---------------------------------------------------------------------------
// Guard A (issue #10): corrupted/unknown policy in the DB must be normalized to
// the most restrictive policy ('hard') at the read boundary — never trusted via
// `row.policy as Policy`. A garbage policy reaching the fair-share engine would
// fall through every switch case and silently ALLOW (fail-OPEN).
//
// The schema has CHECK (policy IN ('hard','soft','burst')), so we bypass it with
// PRAGMA ignore_check_constraints to simulate a legacy/corrupted row.
// ---------------------------------------------------------------------------
test("rowToAllocation normalizes an unknown DB policy to 'hard' (Guard A)", () => {
const pool = poolsDb.createPool({
connectionId: "c-guardA",
name: "Corrupt Policy Pool",
allocations: [{ apiKeyId: "k-corrupt", weight: 100, policy: "soft" }],
});
// Inject a corrupted policy directly, bypassing the CHECK constraint.
const db = getDbInstance() as unknown as {
pragma: (s: string) => unknown;
prepare: (sql: string) => { run: (...p: unknown[]) => unknown };
};
db.pragma("ignore_check_constraints = ON");
db.prepare("UPDATE quota_allocations SET policy = ? WHERE pool_id = ? AND api_key_id = ?").run(
"bogus-policy",
pool.id,
"k-corrupt"
);
db.pragma("ignore_check_constraints = OFF");
// Read through the domain module — the unknown policy must become 'hard'.
const found = poolsDb.getPool(pool.id)!;
const alloc = found.allocations.find((a) => a.apiKeyId === "k-corrupt")!;
assert.equal(alloc.policy, "hard", "unknown DB policy must be normalized to 'hard'");
// Same expectation via listAllocationsForApiKey (the other read path).
const list = poolsDb.listAllocationsForApiKey("k-corrupt");
assert.equal(list.length, 1);
assert.equal(list[0].allocation.policy, "hard");
});
test("rowToAllocation preserves valid policies unchanged (Guard A regression)", () => {
const pool = poolsDb.createPool({
connectionId: "c-guardA-valid",
name: "Valid Policy Pool",
allocations: [
{ apiKeyId: "k-hard", weight: 34, policy: "hard" },
{ apiKeyId: "k-soft", weight: 33, policy: "soft" },
{ apiKeyId: "k-burst", weight: 33, policy: "burst" },
],
});
const found = poolsDb.getPool(pool.id)!;
assert.equal(found.allocations.find((a) => a.apiKeyId === "k-hard")!.policy, "hard");
assert.equal(found.allocations.find((a) => a.apiKeyId === "k-soft")!.policy, "soft");
assert.equal(found.allocations.find((a) => a.apiKeyId === "k-burst")!.policy, "burst");
});

View File

@@ -0,0 +1,65 @@
// Characterization of the db/core.ts schema-column split (god-file decomposition): the idempotent
// ALTER-TABLE column reconcilers + table introspection helpers moved into db/schemaColumns.ts. These
// run an in-memory SQLite db through the helpers to lock the observable behavior: ensure* adds missing
// columns and is safe to re-run; hasTable/hasColumn/getTableColumns/quoteIdentifier introspect.
import { test } from "node:test";
import assert from "node:assert/strict";
import { tryOpenSync } from "../../src/lib/db/adapters/driverFactory.ts";
import {
ensureUsageHistoryColumns,
hasColumn,
hasTable,
quoteIdentifier,
getTableColumns,
} from "../../src/lib/db/schemaColumns.ts";
function openMemoryDb() {
// Synchronous in-memory adapter — no DATA_DIR / file handles to clean up.
const db = tryOpenSync(":memory:");
assert.ok(db, "expected a synchronous sqlite adapter for :memory:");
return db!;
}
test("quoteIdentifier escapes embedded double quotes", () => {
assert.equal(quoteIdentifier("plain"), '"plain"');
assert.equal(quoteIdentifier('we"ird'), '"we""ird"');
});
test("hasTable / hasColumn / getTableColumns introspect a live table", () => {
const db = openMemoryDb();
try {
db.exec("CREATE TABLE usage_history (id INTEGER PRIMARY KEY, model TEXT)");
assert.equal(hasTable(db, "usage_history"), true);
assert.equal(hasTable(db, "does_not_exist"), false);
assert.equal(hasColumn(db, "usage_history", "model"), true);
assert.equal(hasColumn(db, "usage_history", "nope"), false);
assert.deepEqual(getTableColumns(db, "usage_history").sort(), ["id", "model"]);
} finally {
db.close?.();
}
});
test("ensureUsageHistoryColumns adds missing columns and is idempotent", () => {
const db = openMemoryDb();
try {
db.exec("CREATE TABLE usage_history (id INTEGER PRIMARY KEY, model TEXT)");
assert.equal(hasColumn(db, "usage_history", "service_tier"), false);
ensureUsageHistoryColumns(db);
for (const col of [
"success",
"latency_ms",
"ttft_ms",
"error_code",
"service_tier",
"combo_strategy",
]) {
assert.equal(hasColumn(db, "usage_history", col), true, `expected ${col} after ensure`);
}
// Re-running must not throw (columns already present) — idempotency.
assert.doesNotThrow(() => ensureUsageHistoryColumns(db));
} finally {
db.close?.();
}
});

View File

@@ -0,0 +1,104 @@
// SSRF regression guard for the Deno Deploy relay worker (PR #4643 follow-up).
//
// The generated edge worker reads an attacker-controlled `x-relay-path` header
// and previously appended it to the (validated) `x-relay-target` origin via
// string concatenation: `fetch(target.replace(/\/$/, "") + relayPath)`. That
// lets the caller smuggle a NEW host past the allow/SSRF check using userinfo
// (`@`), a backslash, or a protocol-relative path:
// - x-relay-target: https://api.anthropic.com (passes the host guard)
// - x-relay-path: //evil.com/x → fetches https://evil.com/x
// - x-relay-path: /x@evil.com → userinfo confusion
// - x-relay-path: \evil.com → backslash host confusion
//
// The fix resolves the path against the validated target with `new URL()` and
// re-checks that the resolved host/credentials still match the target. The
// path-resolution decision is exported as the pure `resolveRelayTarget()` so it
// can be unit-tested directly (the worker body — a Deno-runtime string — embeds
// the SAME function source, asserted below by diffing the generated worker).
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
resolveRelayTarget,
__buildRelayWorkerForTest,
} from "../../src/app/api/settings/proxy/deno-deploy/route";
const TARGET = "https://api.anthropic.com";
describe("resolveRelayTarget — SSRF-safe path join", () => {
it("accepts a legitimate absolute path and keeps the validated host", () => {
const r = resolveRelayTarget(TARGET, "/v1/foo");
assert.equal(r.ok, true, "a normal path must pass");
if (r.ok) {
assert.equal(r.url, "https://api.anthropic.com/v1/foo");
}
});
it("accepts a path with a query string", () => {
const r = resolveRelayTarget(TARGET, "/v1/messages?beta=true");
assert.equal(r.ok, true);
if (r.ok) {
assert.equal(r.url, "https://api.anthropic.com/v1/messages?beta=true");
}
});
it("rejects a protocol-relative path that swaps the host (//evil.com/x)", () => {
const r = resolveRelayTarget(TARGET, "//evil.com/x");
assert.equal(r.ok, false, "//evil.com must NOT be allowed to change the host");
if (!r.ok) assert.equal(r.status, 403);
});
it("rejects a path containing userinfo (/x@evil.com)", () => {
const r = resolveRelayTarget(TARGET, "/x@evil.com");
assert.equal(r.ok, false, "@ in the path must be rejected");
if (!r.ok) assert.equal(r.status, 403);
});
it("rejects a backslash host-confusion path (\\evil.com)", () => {
const r = resolveRelayTarget(TARGET, "\\evil.com");
assert.equal(r.ok, false, "backslash must be rejected");
if (!r.ok) assert.equal(r.status, 403);
});
it("rejects a value that does not start with '/'", () => {
const r = resolveRelayTarget(TARGET, "evil.com/x");
assert.equal(r.ok, false, "a path must start with '/'");
if (!r.ok) assert.equal(r.status, 403);
});
it("rejects an embedded-credentials path that resolves a different host", () => {
// Classic userinfo trick: everything before '@' becomes userinfo, the real
// host is after it.
const r = resolveRelayTarget(TARGET, "/@evil.com/path");
assert.equal(r.ok, false);
if (!r.ok) assert.equal(r.status, 403);
});
it("rejects backslash-prefixed protocol-relative (\\\\evil.com)", () => {
const r = resolveRelayTarget(TARGET, "\\\\evil.com/x");
assert.equal(r.ok, false);
if (!r.ok) assert.equal(r.status, 403);
});
});
describe("buildRelayWorker — generated worker has no string-concat SSRF hole", () => {
it("does not concatenate the target with the raw relay path", () => {
const worker = __buildRelayWorkerForTest("deadbeefcafe");
// The vulnerable pattern was: target.replace(/\/$/, "") + relayPath
assert.ok(
!/\.replace\(\s*\/\\?\/\$\/\s*,\s*["']{2}\s*\)\s*\+\s*relayPath/.test(worker),
"worker must not append relayPath to target by string concatenation"
);
assert.ok(
!worker.includes('+ relayPath'),
"worker must not contain `+ relayPath` string concatenation"
);
});
it("embeds the resolveRelayTarget guard into the worker body", () => {
const worker = __buildRelayWorkerForTest("deadbeefcafe");
assert.ok(
worker.includes("resolveRelayTarget"),
"the worker must call the shared resolveRelayTarget guard"
);
});
});

View File

@@ -1,6 +1,8 @@
import { describe, it, afterEach } from "node:test";
import assert from "node:assert/strict";
import type { Dispatcher } from "undici";
import {
__createRoundRobinDispatcherForTest,
__getDefaultDispatcherOptionsForTest,
__getProxyDispatcherOptionsForTest,
getDefaultDispatcherConnectionLimit,
@@ -39,7 +41,10 @@ describe("#4580 direct dispatcher options", () => {
});
it("connection limit honors OMNIROUTE_DIRECT_DISPATCHER_CONNECTIONS", () => {
assert.equal(getDefaultDispatcherConnectionLimit({ OMNIROUTE_DIRECT_DISPATCHER_CONNECTIONS: "8" }), 8);
assert.equal(
getDefaultDispatcherConnectionLimit({ OMNIROUTE_DIRECT_DISPATCHER_CONNECTIONS: "8" }),
8
);
});
it("connection limit clamps invalid values to the default", () => {
@@ -49,4 +54,30 @@ describe("#4580 direct dispatcher options", () => {
);
assert.equal(getDefaultDispatcherConnectionLimit({}), 32);
});
it("fans out direct requests across independent dispatcher pools", () => {
const calls: number[] = [];
const dispatchers = [0, 1, 2].map(
(index) =>
({
dispatch() {
calls.push(index);
return true;
},
close() {},
destroy() {},
}) as unknown as Dispatcher
);
const dispatcher = __createRoundRobinDispatcherForTest(dispatchers);
const dispatchOptions = {
origin: "https://chatgpt.com",
path: "/backend-api/codex/responses",
method: "POST",
} as unknown as Parameters<Dispatcher["dispatch"]>[0];
const handler = {} as Parameters<Dispatcher["dispatch"]>[1];
for (let i = 0; i < 7; i++) dispatcher.dispatch(dispatchOptions, handler);
assert.deepEqual(calls, [0, 1, 2, 0, 1, 2, 0]);
});
});

View File

@@ -6,6 +6,7 @@ const { createErrorResponse, createErrorResponseFromUnknown } =
await import("../../src/lib/api/errorResponse.ts");
const { getAccountDisplayName, getProviderDisplayName } =
await import("../../src/lib/display/names.ts");
const { resolveProviderName } = await import("../../src/lib/display/useProviderNodeMap.ts");
test("toJsonErrorPayload: preserves upstream error objects that already have error payloads", () => {
const payload = {
@@ -201,6 +202,32 @@ test("getAccountDisplayName: respects priority order and fallback", () => {
assert.equal(getAccountDisplayName(null), "Unknown Account");
});
test("resolveProviderName: returns user-given name from nodeMap for custom provider", () => {
const nodeMap = new Map([
["openai-compatible-chat-abc123", { name: "My LLM", prefix: "my-llm" }],
]);
assert.equal(
resolveProviderName("openai-compatible-chat-abc123", nodeMap),
"My LLM",
"should return the user-given name when present in the map"
);
// Falls back to prefix when name is blank
const mapNoName = new Map([
["openai-compatible-chat-abc123", { name: null, prefix: "my-prefix" }],
]);
assert.equal(resolveProviderName("openai-compatible-chat-abc123", mapNoName), "my-prefix");
// Falls back to de-UUIDed label when no node entry exists
assert.equal(
resolveProviderName("openai-compatible-chat-02669115-2545-4896-b003-cb4dac09d441", new Map()),
"Compatible (openai)"
);
// Falls back gracefully for unknown ids not in the map
assert.equal(resolveProviderName("plain-provider", nodeMap), "plain-provider");
// Handles null/undefined id
assert.equal(resolveProviderName(null, nodeMap), "Unknown Provider");
assert.equal(resolveProviderName(undefined, null), "Unknown Provider");
});
test("getProviderDisplayName: prefers node metadata and simplifies compatible IDs", () => {
assert.equal(
getProviderDisplayName("openai-compatible-chat-02669115-2545-4896-b003-cb4dac09d441", {

View File

@@ -188,10 +188,10 @@ test("CodexExecutor.buildHeaders binds workspace ids and disables SSE accept for
assert.equal(standardHeaders.Authorization, "Bearer codex-token");
assert.equal(standardHeaders.Accept, "text/event-stream");
assert.equal(standardHeaders["chatgpt-account-id"], "workspace-1");
assert.equal(standardHeaders.Version, "0.132.0");
assert.equal(standardHeaders.Version, "0.142.0");
assert.equal(standardHeaders["Openai-Beta"], "responses=experimental");
assert.equal(standardHeaders["X-Codex-Beta-Features"], "responses_websockets");
assert.equal(standardHeaders["User-Agent"], "codex-cli/0.132.0 (Windows 10.0.26200; x64)");
assert.equal(standardHeaders["User-Agent"], "codex-cli/0.142.0 (Windows 10.0.26200; x64)");
assert.equal(compactHeaders.Accept, "application/json");
});
@@ -200,13 +200,13 @@ test("CodexExecutor.buildHeaders honors safe env overrides for Version and User-
await withEnv(
{
CODEX_CLIENT_VERSION: "0.132.0",
CODEX_CLIENT_VERSION: "0.142.0",
CODEX_USER_AGENT: undefined,
},
() => {
const headers = executor.buildHeaders({ accessToken: "codex-token" }, true);
assert.equal(headers.Version, "0.132.0");
assert.equal(headers["User-Agent"], "codex-cli/0.132.0 (Windows 10.0.26200; x64)");
assert.equal(headers.Version, "0.142.0");
assert.equal(headers["User-Agent"], "codex-cli/0.142.0 (Windows 10.0.26200; x64)");
}
);
@@ -217,7 +217,7 @@ test("CodexExecutor.buildHeaders honors safe env overrides for Version and User-
},
() => {
const headers = executor.buildHeaders({ accessToken: "codex-token" }, true);
assert.equal(headers.Version, "0.132.0");
assert.equal(headers.Version, "0.142.0");
assert.equal(headers["User-Agent"], "custom-codex/9.9.9");
}
);

View File

@@ -71,6 +71,33 @@ test("GithubExecutor.buildUrl keeps GitHub Claude Opus 4.6 on /chat/completions"
assert.equal(url, "https://api.githubcopilot.com/chat/completions");
});
test("GithubExecutor.buildUrl routes unlisted Codex models to /responses (9router#102)", () => {
// Copilot Codex models advertise supported_endpoints: ["/responses"]. When such
// a model isn't in the curated gh registry, getModelTargetFormat returns null and
// the request fell through to /chat/completions -> upstream 400 "model <id> is not
// accessible via the /chat/completions endpoint". Any *-codex id must route to
// /responses regardless of whether it's explicitly registered.
const executor = new GithubExecutor();
for (const model of [
"gpt-5-codex",
"gpt-5.1-codex",
"gpt-5.1-codex-mini",
"gpt-5.1-codex-max",
"gpt-5.2-codex",
]) {
assert.equal(
executor.buildUrl(model, true),
"https://api.githubcopilot.com/responses",
`${model} must route to /responses`
);
}
// Non-codex unlisted models keep the chat/completions default.
assert.equal(
executor.buildUrl("some-random-chat-model", true),
"https://api.githubcopilot.com/chat/completions"
);
});
test("GithubExecutor.transformRequest injects JSON response instructions for Claude and strips reasoning fields", () => {
const executor = new GithubExecutor();
const body = {

View File

@@ -0,0 +1,121 @@
// Unit tests for stripUnsupportedParams: per-(provider,model) strip of params
// the upstream rejects (HTTP 400). Port from 9router#7ae9fff6 (fixes #1748).
//
// Rule coverage:
// 1. claude-opus-4 series: temperature deprecated → Anthropic 400.
// 2. github + gpt-5.4: temperature unsupported.
// 3. github + Claude (except opus/sonnet 4.6): thinking + reasoning_effort rejected.
import { test } from "node:test";
import assert from "node:assert/strict";
import {
stripUnsupportedParams,
__STRIP_RULES_FOR_TEST,
} from "../../open-sse/translator/paramSupport.ts";
test("stripUnsupportedParams: drops temperature for claude-opus-4 models (any provider)", () => {
const body = { model: "claude-opus-4-20250514", temperature: 0.7, max_tokens: 100 };
stripUnsupportedParams("anthropic", "claude-opus-4-20250514", body);
assert.equal(body.temperature, undefined, "temperature must be stripped");
assert.equal(body.max_tokens, 100, "other params must survive");
assert.equal(body.model, "claude-opus-4-20250514", "model must not be touched");
});
test("stripUnsupportedParams: drops temperature for claude-opus-4-1 (case insensitive)", () => {
const body: Record<string, unknown> = { temperature: 0.5 };
stripUnsupportedParams("anthropic", "CLAUDE-OPUS-4-1-20250805", body);
assert.equal(body.temperature, undefined);
});
test("stripUnsupportedParams: keeps temperature for claude-sonnet-4 (only opus-4 affected)", () => {
const body: Record<string, unknown> = { temperature: 0.7 };
stripUnsupportedParams("anthropic", "claude-sonnet-4-20250514", body);
assert.equal(body.temperature, 0.7, "sonnet-4 still accepts temperature");
});
test("stripUnsupportedParams: keeps temperature for claude-opus-3 (regression guard)", () => {
const body: Record<string, unknown> = { temperature: 0.7 };
stripUnsupportedParams("anthropic", "claude-3-opus-20240229", body);
assert.equal(body.temperature, 0.7);
});
test("stripUnsupportedParams: github + gpt-5.4 strips temperature", () => {
const body: Record<string, unknown> = { temperature: 1, max_completion_tokens: 200 };
stripUnsupportedParams("github", "gpt-5.4", body);
assert.equal(body.temperature, undefined);
assert.equal(body.max_completion_tokens, 200);
});
test("stripUnsupportedParams: github + gpt-5 (non-5.4) keeps temperature", () => {
const body: Record<string, unknown> = { temperature: 1 };
stripUnsupportedParams("github", "gpt-5", body);
assert.equal(body.temperature, 1);
});
test("stripUnsupportedParams: github + Claude strips thinking + reasoning_effort", () => {
const body: Record<string, unknown> = {
thinking: { type: "enabled" },
reasoning_effort: "high",
temperature: 0.5,
};
stripUnsupportedParams("github", "claude-3-5-sonnet", body);
assert.equal(body.thinking, undefined, "Copilot rejects Claude-style thinking");
assert.equal(body.reasoning_effort, undefined);
assert.equal(body.temperature, 0.5, "non-targeted params survive");
});
test("stripUnsupportedParams: github + claude opus 4.6 KEEPS thinking + reasoning_effort", () => {
const body: Record<string, unknown> = {
thinking: { type: "enabled" },
reasoning_effort: "high",
};
stripUnsupportedParams("github", "claude-opus-4.6", body);
assert.equal(body.reasoning_effort, "high", "opus-4.6 supports reasoning_effort");
// Note: thinking survives too on opus-4.6 per upstream rule.
assert.deepEqual(body.thinking, { type: "enabled" });
});
test("stripUnsupportedParams: github + claude sonnet 4.6 KEEPS reasoning_effort", () => {
const body: Record<string, unknown> = { reasoning_effort: "low" };
stripUnsupportedParams("github", "claude-sonnet-4.6", body);
assert.equal(body.reasoning_effort, "low");
});
test("stripUnsupportedParams: non-github provider + Claude does NOT strip thinking", () => {
// The github-Claude rule is provider-scoped.
const body: Record<string, unknown> = { thinking: { type: "enabled" } };
stripUnsupportedParams("anthropic", "claude-3-5-sonnet", body);
assert.deepEqual(body.thinking, { type: "enabled" });
});
test("stripUnsupportedParams: only deletes when the key is present (never adds undefined)", () => {
const body: Record<string, unknown> = { max_tokens: 50 };
stripUnsupportedParams("anthropic", "claude-opus-4", body);
// No `temperature` key should be introduced.
assert.equal("temperature" in body, false);
});
test("stripUnsupportedParams: returns the same body reference (in-place mutation)", () => {
const body: Record<string, unknown> = { temperature: 0.7 };
const out = stripUnsupportedParams("anthropic", "claude-opus-4", body);
assert.equal(out, body);
});
test("stripUnsupportedParams: null/undefined body is a no-op", () => {
assert.equal(stripUnsupportedParams("anthropic", "claude-opus-4", null), null);
assert.equal(stripUnsupportedParams("anthropic", "claude-opus-4", undefined), undefined);
});
test("stripUnsupportedParams: missing model is a no-op", () => {
const body: Record<string, unknown> = { temperature: 0.7 };
stripUnsupportedParams("anthropic", "", body);
assert.equal(body.temperature, 0.7);
});
test("STRIP_RULES is non-empty and every rule has a drop list", () => {
assert.ok(__STRIP_RULES_FOR_TEST.length > 0);
for (const rule of __STRIP_RULES_FOR_TEST) {
assert.ok(Array.isArray(rule.drop) && rule.drop.length > 0);
assert.ok(typeof rule.match === "function" || rule.match instanceof RegExp);
}
});

View File

@@ -0,0 +1,54 @@
// Regression test: `pattern` must be preserved in antigravity tool schemas.
//
// Upstream: decolua/9router @ f6c2f7ca / Fixes decolua/9router#1368.
//
// Tools such as glob/grep/file-search rely on a `pattern` constraint on
// string parameters. Antigravity (Gemini-derived) DOES accept `pattern`,
// so removing it for that surface drops critical tool semantics and
// produces upstream 400s / wrong-tool-call behavior.
//
// Two assertions, mirroring how the field is consumed downstream:
// 1) `pattern` must NOT live in the unsupported keyword set used by the
// antigravity schema cleaner.
// 2) End-to-end, `cleanJSONSchemaForAntigravity` must preserve a `pattern`
// constraint placed on a string property (typical glob/grep tool).
import test from "node:test";
import assert from "node:assert/strict";
import {
GEMINI_UNSUPPORTED_SCHEMA_KEYS,
cleanJSONSchemaForAntigravity,
} from "../../open-sse/translator/helpers/geminiHelper.ts";
test("GEMINI_UNSUPPORTED_SCHEMA_KEYS does not strip `pattern` for antigravity", () => {
assert.equal(
GEMINI_UNSUPPORTED_SCHEMA_KEYS.has("pattern"),
false,
"`pattern` is supported by antigravity and must be preserved on tool schemas"
);
});
test("cleanJSONSchemaForAntigravity preserves `pattern` on glob/grep-style tool schema", () => {
const schema = {
type: "object",
properties: {
glob: {
type: "string",
description: "Glob pattern to match files",
pattern: "^[A-Za-z0-9_\\-/*.]+$",
},
},
required: ["glob"],
};
const cleaned = cleanJSONSchemaForAntigravity(schema) as {
properties: { glob: { pattern?: string } };
};
assert.equal(
cleaned.properties.glob.pattern,
"^[A-Za-z0-9_\\-/*.]+$",
"`pattern` constraint on a string property must survive antigravity cleaning"
);
});

View File

@@ -0,0 +1,214 @@
/**
* #4569 — Google Flow video generation helpers.
*
* Covers the pure transformation logic (param normalization, Veo submit-body
* construction, operation-name parsing, LRO result parsing, credential
* resolution) and a source guard asserting the wire endpoint stays isolated and
* flagged for live validation (Hard Rule #18).
*/
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import {
GOOGLE_FLOW_CREDENTIAL_PROVIDER,
GOOGLE_FLOW_HOST,
buildGoogleFlowSubmitBody,
normalizeFlowVideoParams,
parseFlowOperationName,
parseFlowOperationResult,
resolveFlowAccessToken,
resolveFlowProjectId,
resolveVideoCredentialProvider,
} from "../../open-sse/handlers/videoGeneration/googleFlow.ts";
import { getVideoProvider, parseVideoModel } from "../../open-sse/config/videoRegistry.ts";
const __dirname = dirname(fileURLToPath(import.meta.url));
test("normalizeFlowVideoParams: defaults sampleCount to 1 and stringifies prompt", () => {
const p = normalizeFlowVideoParams({ prompt: "a cat surfing" });
assert.equal(p.prompt, "a cat surfing");
assert.equal(p.sampleCount, 1);
assert.equal(p.aspectRatio, undefined);
assert.equal(p.durationSeconds, undefined);
});
test("normalizeFlowVideoParams: reads snake_case OpenAI fields", () => {
const p = normalizeFlowVideoParams({
prompt: "x",
aspect_ratio: "16:9",
duration: 8,
n: 2,
negative_prompt: "blurry",
resolution: "1080p",
});
assert.equal(p.aspectRatio, "16:9");
assert.equal(p.durationSeconds, 8);
assert.equal(p.sampleCount, 2);
assert.equal(p.negativePrompt, "blurry");
assert.equal(p.resolution, "1080p");
});
test("normalizeFlowVideoParams: reads camelCase native fields", () => {
const p = normalizeFlowVideoParams({ prompt: "x", aspectRatio: "9:16", durationSeconds: 4 });
assert.equal(p.aspectRatio, "9:16");
assert.equal(p.durationSeconds, 4);
});
test("normalizeFlowVideoParams: treats ratio-shaped size as aspectRatio, ignores pixel size", () => {
assert.equal(normalizeFlowVideoParams({ prompt: "x", size: "16:9" }).aspectRatio, "16:9");
assert.equal(normalizeFlowVideoParams({ prompt: "x", size: "1024x1024" }).aspectRatio, undefined);
});
test("normalizeFlowVideoParams: rejects non-positive / non-finite numbers", () => {
const p = normalizeFlowVideoParams({ prompt: "x", n: 0, duration: -5 });
assert.equal(p.sampleCount, 1);
assert.equal(p.durationSeconds, undefined);
});
test("buildGoogleFlowSubmitBody: emits documented Veo instances/parameters shape", () => {
const body = buildGoogleFlowSubmitBody({
prompt: "a dog",
aspectRatio: "16:9",
durationSeconds: 6,
sampleCount: 1,
negativePrompt: "ugly",
resolution: "720p",
});
assert.deepEqual(body.instances, [{ prompt: "a dog" }]);
assert.equal(body.parameters.sampleCount, 1);
assert.equal(body.parameters.aspectRatio, "16:9");
assert.equal(body.parameters.durationSeconds, 6);
assert.equal(body.parameters.negativePrompt, "ugly");
assert.equal(body.parameters.resolution, "720p");
});
test("buildGoogleFlowSubmitBody: omits absent optional parameters", () => {
const body = buildGoogleFlowSubmitBody({ prompt: "p", sampleCount: 3 });
assert.deepEqual(body.parameters, { sampleCount: 3 });
assert.ok(!("aspectRatio" in body.parameters));
assert.ok(!("durationSeconds" in body.parameters));
});
test("parseFlowOperationName: handles {name} and {operation:{name}} and rejects junk", () => {
assert.equal(parseFlowOperationName({ name: "operations/abc" }), "operations/abc");
assert.equal(parseFlowOperationName({ operation: { name: "operations/xyz" } }), "operations/xyz");
assert.equal(parseFlowOperationName({}), null);
assert.equal(parseFlowOperationName(null), null);
assert.equal(parseFlowOperationName({ name: "" }), null);
});
test("parseFlowOperationResult: not done → keep polling", () => {
assert.deepEqual(parseFlowOperationResult({ done: false }), { done: false });
assert.deepEqual(parseFlowOperationResult({}), { done: false });
});
test("parseFlowOperationResult: done with base64 video (documented shape)", () => {
const r = parseFlowOperationResult({
done: true,
response: { videos: [{ bytesBase64Encoded: "AAAA" }] },
});
assert.equal(r.done, true);
assert.equal(r.base64, "AAAA");
assert.equal(r.format, "mp4");
assert.equal(r.error, undefined);
});
test("parseFlowOperationResult: done with gcsUri/uri video", () => {
assert.equal(
parseFlowOperationResult({ done: true, response: { videos: [{ gcsUri: "gs://b/v.mp4" }] } }).url,
"gs://b/v.mp4"
);
assert.equal(
parseFlowOperationResult({ done: true, response: { videos: [{ uri: "https://x/v.mp4" }] } }).url,
"https://x/v.mp4"
);
});
test("parseFlowOperationResult: done with alternate generateVideoResponse shape", () => {
const r = parseFlowOperationResult({
done: true,
response: {
generateVideoResponse: { generatedSamples: [{ video: { uri: "https://x/sample.mp4" } }] },
},
});
assert.equal(r.url, "https://x/sample.mp4");
});
test("parseFlowOperationResult: done with error surfaces the message", () => {
const r = parseFlowOperationResult({ done: true, error: { message: "quota exceeded" } });
assert.equal(r.done, true);
assert.equal(r.error, "quota exceeded");
});
test("parseFlowOperationResult: done but empty → explicit no-video error", () => {
const r = parseFlowOperationResult({ done: true, response: {} });
assert.equal(r.done, true);
assert.match(r.error ?? "", /no video/i);
});
test("parseFlowOperationResult: null/empty array elements do not crash (defensive)", () => {
// Upstream returning a null element must not throw a TypeError (gemini-code-assist #4769).
for (const response of [
{ videos: [null] },
{ videos: [undefined] },
{ videos: [{}] },
{ generateVideoResponse: { generatedSamples: [null] } },
{ generateVideoResponse: { generatedSamples: [{ video: null }] } },
{ generateVideoResponse: { generatedSamples: [{}] } },
]) {
const r = parseFlowOperationResult({ done: true, response });
assert.equal(r.done, true);
assert.match(r.error ?? "", /no video/i);
assert.equal(r.base64, undefined);
assert.equal(r.url, undefined);
}
});
test("resolveFlowProjectId: direct, providerSpecificData, and missing", () => {
assert.equal(resolveFlowProjectId({ projectId: "proj-1" }), "proj-1");
assert.equal(resolveFlowProjectId({ providerSpecificData: { projectId: "proj-2" } }), "proj-2");
assert.equal(resolveFlowProjectId({}), null);
assert.equal(resolveFlowProjectId(null), null);
});
test("resolveFlowAccessToken: prefers accessToken, falls back to apiKey", () => {
assert.equal(resolveFlowAccessToken({ accessToken: "tok" }), "tok");
assert.equal(resolveFlowAccessToken({ apiKey: "key" }), "key");
assert.equal(resolveFlowAccessToken({}), null);
});
test("videoRegistry: googleflow provider is registered with oauth + google-flow format", () => {
const provider = getVideoProvider("googleflow");
assert.ok(provider, "googleflow provider must exist");
assert.equal(provider.format, "google-flow");
assert.equal(provider.authType, "oauth");
assert.ok(provider.models.length > 0, "must expose at least one Veo model");
});
test("videoRegistry: parseVideoModel resolves googleflow/<model> and its alias", () => {
const parsed = parseVideoModel("googleflow/veo-3.1-generate");
assert.equal(parsed.provider, "googleflow");
assert.equal(parsed.model, "veo-3.1-generate");
const aliased = parseVideoModel("flow/veo-3.1-generate");
assert.equal(aliased.provider, "googleflow");
});
test("resolveVideoCredentialProvider: googleflow reuses antigravity OAuth, others unchanged", () => {
assert.equal(resolveVideoCredentialProvider("googleflow"), "antigravity");
assert.equal(GOOGLE_FLOW_CREDENTIAL_PROVIDER, "antigravity");
assert.equal(resolveVideoCredentialProvider("vertex"), "vertex");
assert.equal(resolveVideoCredentialProvider("kie"), "kie");
});
test("source guard: wire endpoint stays isolated + flagged for live validation (Rule #18)", () => {
assert.match(GOOGLE_FLOW_HOST, /aisandbox-pa\.googleapis\.com/);
const src = readFileSync(
join(__dirname, "../../open-sse/handlers/videoGeneration/googleFlow.ts"),
"utf8"
);
assert.match(src, /PENDING LIVE VALIDATION/, "wire format must be flagged for HAR validation");
});

View File

@@ -4,11 +4,32 @@
import test from "node:test";
import assert from "node:assert/strict";
import dns from "node:dns";
import { callVisionModel, type VisionModelConfig } from "@/lib/guardrails/visionBridgeHelpers";
// Store original fetch
const originalFetch = globalThis.fetch;
// Stub DNS for fetchRemoteImage's GHSA-cmhj-wh2f-9cgx DNS-rebinding guard
// (assertHostnameResolvesPublic in src/shared/network/remoteImageFetch.ts).
// These tests mock globalThis.fetch with example.com hosts that don't actually
// resolve in CI; the call path (callVisionModel -> fetchRemoteImageAsDataUri)
// does not expose a way to inject a `lookup` stub through to fetchRemoteImage,
// so we monkey-patch dns.promises.lookup with a pass-through public-IP
// resolver. Node --test runs each test file in its own process, so this
// rebinding does not leak across files.
const originalDnsLookup = dns.promises.lookup;
(dns.promises as { lookup: unknown }).lookup = (async (
_hostname: string,
options?: { all?: boolean }
) => {
const record = { address: "203.0.113.1", family: 4 };
return options && options.all ? [record] : record;
}) as typeof dns.promises.lookup;
process.on("exit", () => {
(dns.promises as { lookup: unknown }).lookup = originalDnsLookup;
});
test("callVisionModel returns description on success", async () => {
// Mock global fetch
const mockResponse = {

View File

@@ -0,0 +1,138 @@
// #1998 (upstream) — adaptBodyForCompression must use the full translator pair
// (openaiResponsesToOpenAIRequest → compress → openaiToOpenAIResponsesRequest) when
// body.input is in Responses format, so that:
// (a) function_call_output items with non-string `output` (JSON objects) are properly
// serialised and included in the compression pass (upstream bug: simple helper skips
// them because hasTextContent() returns false for object content), and
// (b) body.input stays Responses-shaped after the full round-trip.
import { describe, it, after } from "node:test";
import assert from "node:assert/strict";
import { adaptBodyForCompression } from "../../open-sse/services/compression/bodyAdapter.ts";
describe("adaptBodyForCompression openai-responses format (#1998)", () => {
after(() => {
// No DB touched.
});
it("includes function_call_output with object output in the compression pass", () => {
// The simple responsesItemToMessage() helper only sets content = item.output.
// When output is a JSON object (not a string), hasTextContent() returns false
// and the item is excluded from messages entirely — not compressable.
// The fix: use openaiResponsesToOpenAIRequest which JSON.stringifies object output
// so the text is available for compression engines.
const body: Record<string, unknown> = {
model: "gpt-5",
input: [
{
type: "function_call",
call_id: "c1",
name: "bash",
arguments: JSON.stringify({ command: "ls -la" }),
},
{
type: "function_call_output",
call_id: "c1",
// Object output — NOT a string. Simple helper loses this.
output: { result: "file1.ts file2.ts ".repeat(50) },
},
],
};
const adapter = adaptBodyForCompression(body);
// After the fix, the adapter must recognise the function_call_output as
// compressable and include it in the messages array.
assert.ok(adapter.adapted, "adapter must be adapted (not a pass-through)");
const messages = (adapter.body as Record<string, unknown>).messages as Array<Record<string, unknown>>;
assert.ok(Array.isArray(messages) && messages.length > 0, "messages must be non-empty");
// At least one tool/assistant message must be present for compression engines.
const hasToolMsg = messages.some((m) => m.role === "tool" || m.role === "assistant");
assert.ok(hasToolMsg, "messages must include a tool/assistant entry from the function_call pair");
});
it("keeps body.input Responses-shaped after compress/restore round-trip for type:message items", () => {
// Verify the canonical behaviour: a Responses body.input with type:message items
// is correctly round-tripped through the adapter and restore.
const body: Record<string, unknown> = {
model: "gpt-5",
input: [
{
type: "message",
role: "user",
content: [{ type: "input_text", text: "a long original message ".repeat(20) }],
},
],
};
const adapter = adaptBodyForCompression(body);
assert.ok(adapter.adapted, "adapter must be adapted for Responses body.input");
// Simulate compression: keep messages unchanged (no-op compression).
const compressedBody = { ...(adapter.body as Record<string, unknown>) };
const restored = adapter.restore(compressedBody);
// body.input must be an array of Responses items (not raw OpenAI messages).
assert.ok(Array.isArray(restored.input), "restored.input must be an array");
assert.equal((restored.input as unknown[]).length, 1, "restored.input must have one item");
const item = (restored.input as Array<Record<string, unknown>>)[0];
assert.equal(item.type, "message", "restored item must keep type:message");
assert.equal(item.role, "user", "restored item must keep role:user");
assert.ok(
Array.isArray(item.content),
"restored item.content must stay as an array (Responses format), not a string"
);
// Content must still carry the Responses input_text structure.
const firstContentPart = (item.content as Array<Record<string, unknown>>)[0];
assert.equal(
firstContentPart.type,
"input_text",
"content part must preserve type:input_text"
);
assert.ok(
typeof firstContentPart.text === "string" && firstContentPart.text.length > 0,
"content part must preserve the text value"
);
});
it("round-trips function_call_output with string output (preserves call_id and output field)", () => {
// Ensure string output is correctly restored in output field (not content field).
const body: Record<string, unknown> = {
model: "gpt-5",
input: [
{
type: "function_call",
call_id: "c2",
name: "read_file",
arguments: JSON.stringify({ path: "/foo.ts" }),
},
{
type: "function_call_output",
call_id: "c2",
output: "large file contents ".repeat(30),
},
],
};
const adapter = adaptBodyForCompression(body);
assert.ok(adapter.adapted, "adapter must be adapted");
// Simulate no-op compression.
const compressedBody = { ...(adapter.body as Record<string, unknown>) };
const restored = adapter.restore(compressedBody);
assert.ok(Array.isArray(restored.input), "restored.input must be an array");
const items = restored.input as Array<Record<string, unknown>>;
assert.equal(items.length, 2, "both function_call and function_call_output must be present");
const outputItem = items.find((i) => i.type === "function_call_output") as Record<string, unknown>;
assert.ok(outputItem, "function_call_output item must be in restored.input");
assert.equal(outputItem.call_id, "c2", "call_id must be preserved");
assert.ok(
typeof outputItem.output === "string" && outputItem.output.length > 0,
"output field must be present and non-empty"
);
});
});

View File

@@ -0,0 +1,27 @@
// Regression guard for #4596: the home provider-topology card (ReactFlow connection
// map) vanished for installs that never persisted `showProviderTopologyOnHome`.
// HomePageClient initialized the flag to `false` and only flipped it on an explicit
// boolean from /api/settings, so an absent (undefined) setting kept the card hidden
// forever — while AppearanceTab still showed the toggle as ON (`!== false`).
// The card defaults ON; only an explicit `false` hides it.
import { test } from "node:test";
import assert from "node:assert/strict";
import { shouldShowProviderTopologyOnHome } from "../../src/app/(dashboard)/dashboard/homeAppearance.ts";
test("absent setting (undefined) shows the topology card — the #4596 regression", () => {
assert.equal(shouldShowProviderTopologyOnHome(undefined), true);
});
test("explicit false hides the topology card (respects opt-out)", () => {
assert.equal(shouldShowProviderTopologyOnHome(false), false);
});
test("explicit true shows the topology card", () => {
assert.equal(shouldShowProviderTopologyOnHome(true), true);
});
test("non-boolean truthy/absent values keep the default-on behavior", () => {
// null (e.g. JSON null) and missing keys must not hide the card — only `false` does.
assert.equal(shouldShowProviderTopologyOnHome(null), true);
});

View File

@@ -0,0 +1,80 @@
/**
* Regression test — ported from decolua/9router commit 047fdc89:
* "fix(image): prevent compatible nodes from shadowing provider aliases"
*
* A user-defined openai-compatible provider node with `prefix: "cf"` must NOT
* shadow the built-in `cloudflare-ai` provider (alias `cf`). Cloudflare image
* routes like `cf/@cf/black-forest-labs/...` must keep resolving to the
* built-in cloudflare-ai provider, regardless of any compatible-node prefix
* collision.
*
* Inverse case: a non-reserved compatible-node prefix (e.g. `oct`) must still
* resolve to the compatible node. The fix is a reserved-prefix guard — it must
* not break user-defined prefixes that don't collide with built-in aliases.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(
path.join(os.tmpdir(), "omniroute-image-compat-shadow-")
);
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const { getModelInfo } = await import("../../src/sse/services/model.ts");
test.before(async () => {
await providersDb.createProviderNode({
id: "openai-compatible-cf-collision",
type: "openai-compatible",
name: "Compatible CF Collision",
prefix: "cf",
baseUrl: "https://compatible.test/v1",
chatPath: "/v1/chat/completions",
modelsPath: "/v1/models",
});
await providersDb.createProviderNode({
id: "openai-compatible-oct-passthrough",
type: "openai-compatible",
name: "Compatible OCT",
prefix: "oct",
baseUrl: "https://compatible-oct.test/v1",
chatPath: "/v1/chat/completions",
modelsPath: "/v1/models",
});
});
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("compatible node with prefix=cf must NOT shadow the built-in cloudflare-ai alias", async () => {
const info = (await getModelInfo("cf/@cf/black-forest-labs/flux-2-klein-9b")) as {
provider?: string;
model?: string;
};
assert.equal(
info.provider,
"cloudflare-ai",
"cf/ must keep routing to the built-in cloudflare-ai provider (not the compatible node)"
);
assert.equal(info.model, "@cf/black-forest-labs/flux-2-klein-9b");
});
test("non-reserved compatible-node prefix (oct) still routes to the compatible node", async () => {
const info = (await getModelInfo("oct/gpt-image-1")) as {
provider?: string;
model?: string;
};
assert.equal(
info.provider,
"openai-compatible-oct-passthrough",
"user-defined prefixes that don't collide with built-in aliases must still resolve to the compatible node"
);
assert.equal(info.model, "gpt-image-1");
});

View File

@@ -1,11 +1,34 @@
import test from "node:test";
import assert from "node:assert/strict";
import dns from "node:dns";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
process.env.DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-images-"));
// Stub DNS for fetchRemoteImage's GHSA-cmhj-wh2f-9cgx DNS-rebinding guard
// (assertHostnameResolvesPublic in src/shared/network/remoteImageFetch.ts).
// Several image-handler tests (Fal AI URL->b64 normalization, BFL polling
// with base64 input images, NanoBanana polling with URL->b64 conversion)
// mock globalThis.fetch with example.com URLs that don't resolve in CI; the
// handler invokes fetchRemoteImage without exposing a `lookup` injection
// point, so we monkey-patch dns.promises.lookup to always return a public IP
// so the rebinding guard passes and the test exercises the mocked fetch
// behaviour as intended. Node --test runs each file in its own process, so
// this rebinding does not leak across files.
const originalDnsLookup = dns.promises.lookup;
(dns.promises as { lookup: unknown }).lookup = (async (
_hostname: string,
options?: { all?: boolean }
) => {
const record = { address: "203.0.113.1", family: 4 };
return options && options.all ? [record] : record;
}) as typeof dns.promises.lookup;
process.on("exit", () => {
(dns.promises as { lookup: unknown }).lookup = originalDnsLookup;
});
const { IMAGE_PROVIDERS, parseImageModel, getAllImageModels } =
await import("../../open-sse/config/imageRegistry.ts");
const { handleImageGeneration } = await import("../../open-sse/handlers/imageGeneration.ts");

View File

@@ -0,0 +1,96 @@
// SSRF regression guard for kiro region — GHSA-6mwv-4mrm-5p3m.
// Without the assertValidAwsRegion guard, a malicious region value
// would be interpolated directly into upstream URLs like
// `https://oidc.${region}.amazonaws.com/token`, allowing the caller to
// redirect the proxy to arbitrary hosts (file://, 127.0.0.1, EC2 metadata, etc).
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
AWS_REGION_PATTERN,
assertValidAwsRegion,
} from "../../src/lib/oauth/constants/oauth";
import { KiroService } from "../../src/lib/oauth/services/kiro";
describe("AWS_REGION_PATTERN", () => {
it("accepts canonical AWS regions", () => {
for (const r of [
"us-east-1",
"us-west-2",
"eu-west-1",
"ap-southeast-2",
"ca-central-1",
"sa-east-1",
"me-south-1",
"af-south-1",
"eu-central-2",
]) {
assert.ok(AWS_REGION_PATTERN.test(r), `expected ${r} to match`);
}
});
it("rejects SSRF-shaped values", () => {
for (const bad of [
"127.0.0.1",
"169.254.169.254",
"localhost",
"evil.example.com",
"us-east-1.evil.com",
"us-east-1/../foo",
"us-east-1#frag",
"us-east-1?x=1",
"file:///etc/passwd",
"http://internal",
"",
"US-EAST-1", // wrong case
"us_east_1",
"us-east-",
"-east-1",
"us--east-1",
]) {
assert.ok(!AWS_REGION_PATTERN.test(bad), `expected ${bad!} to be rejected`);
}
});
});
describe("assertValidAwsRegion", () => {
it("returns the region when valid", () => {
assert.equal(assertValidAwsRegion("us-east-1"), "us-east-1");
});
it("throws on non-string", () => {
assert.throws(() => assertValidAwsRegion(undefined as unknown as string));
assert.throws(() => assertValidAwsRegion(null as unknown as string));
assert.throws(() => assertValidAwsRegion(123 as unknown as string));
});
it("throws on invalid region", () => {
assert.throws(() => assertValidAwsRegion("127.0.0.1"));
assert.throws(() => assertValidAwsRegion("evil.com"));
assert.throws(() => assertValidAwsRegion(""));
});
});
describe("KiroService SSRF guard", () => {
const svc = new KiroService();
it("registerClient rejects malicious region", async () => {
await assert.rejects(() => svc.registerClient("127.0.0.1"));
await assert.rejects(() => svc.registerClient("evil.example.com"));
});
it("startDeviceAuthorization rejects malicious region", async () => {
await assert.rejects(() =>
svc.startDeviceAuthorization("cid", "csec", "https://x", "169.254.169.254")
);
});
it("pollDeviceToken rejects malicious region", async () => {
await assert.rejects(() =>
svc.pollDeviceToken("cid", "csec", "dc", "127.0.0.1")
);
});
it("validateImportToken rejects malicious region before fetching", async () => {
await assert.rejects(() => svc.validateImportToken("aorAAAAAGfoo", "127.0.0.1"));
});
});

View File

@@ -0,0 +1,238 @@
/**
* Kiro (AWS CodeWhisperer) tool-result compression — ported from upstream
* decolua/9router#1194 (zanuartri).
*
* The upstream PR added Kiro-format support directly inside RTK. OmniRoute
* lifts that capability to `bodyAdapter`, so the Kiro envelope
* (`conversationState.history[].userInputMessage.userInputMessageContext.toolResults`)
* flattens to OpenAI-shape `role:"tool"` messages — every compression engine
* (RTK, lite, aggressive, etc.) automatically benefits, not just RTK.
*
* Error tool results (status === "error") are skipped to preserve diagnostics
* (matches upstream behavior).
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { applyRtkCompression } from "../../open-sse/services/compression/engines/rtk/index.ts";
import { adaptBodyForCompression } from "../../open-sse/services/compression/bodyAdapter.ts";
function buildKiroBodyWithBuildOutput(): Record<string, unknown> {
const noisyNpmOutput = [
"npm warn deprecated har-validator@5.1.5: this library is no longer supported",
"npm warn deprecated uuid@3.4.0: uuid@10 and below is no longer supported",
"npm warn deprecated request@2.88.2: request has been deprecated",
"npm warn deprecated inflight@1.0.6: This module is not supported",
"npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported",
"npm warn deprecated rimraf@2.7.1: Rimraf versions prior to v4 are no longer supported",
"",
"added 47 packages, and audited 48 packages in 13s",
"",
"3 packages are looking for funding",
" run `npm fund` for details",
"",
"4 vulnerabilities (2 moderate, 2 critical)",
"",
"Some issues need review, and may require choosing",
"a different dependency.",
"",
"Run `npm audit` for details.",
].join("\n");
return {
conversationState: {
chatTriggerType: "MANUAL",
conversationId: "test-port-1194",
currentMessage: {
userInputMessage: {
content: "Install express",
modelId: "claude-sonnet-4.5",
userInputMessageContext: {
toolResults: [
{
toolUseId: "tool_1",
status: "success",
content: [{ text: noisyNpmOutput }],
},
],
},
},
},
history: [],
},
};
}
describe("Kiro tool-result compression (port of decolua/9router#1194)", () => {
it("adapts a Kiro conversationState body to messages with the tool-result text exposed", () => {
const body = buildKiroBodyWithBuildOutput();
const adapter = adaptBodyForCompression(body);
const messages = (adapter.body.messages ?? []) as Array<{
role?: string;
content?: unknown;
}>;
assert.ok(adapter.adapted, "Kiro body must be flagged as adapted");
assert.ok(
messages.some((m) => m.role === "tool" && typeof m.content === "string"),
"adapter must surface tool-result text as a role:tool message"
);
});
it("compresses tool-result text inside Kiro currentMessage and writes the rewritten text back", () => {
const body = buildKiroBodyWithBuildOutput();
const before = (
(body.conversationState as Record<string, unknown>).currentMessage as Record<string, unknown>
).userInputMessage as {
userInputMessageContext: { toolResults: Array<{ content: Array<{ text: string }> }> };
};
const originalLen = before.userInputMessageContext.toolResults[0].content[0].text.length;
const result = applyRtkCompression(body);
assert.equal(result.compressed, true, "RTK must compress noisy build output");
assert.ok(result.stats, "stats must be returned");
assert.ok(
result.stats!.compressedTokens < result.stats!.originalTokens,
"compressedTokens must drop"
);
const restored = (
(result.body as Record<string, unknown>).conversationState as Record<string, unknown>
).currentMessage as {
userInputMessage: {
userInputMessageContext: { toolResults: Array<{ content: Array<{ text: string }> }> };
};
};
const afterLen = restored.userInputMessage.userInputMessageContext.toolResults[0].content[0]
.text.length;
assert.ok(
afterLen < originalLen,
`Kiro tool-result text must shrink (before=${originalLen}, after=${afterLen})`
);
});
it("compresses tool-result text inside Kiro history[] entries", () => {
// Reuse the noisy npm-install fixture (already proven to trigger a build-output
// filter in the currentMessage test) so this test exercises the history[] path
// without depending on a separate filter (cargo, etc.) being registered.
const compilingLines: string[] = [
"npm warn deprecated har-validator@5.1.5: this library is no longer supported",
"npm warn deprecated uuid@3.4.0: uuid@10 and below is no longer supported",
"npm warn deprecated request@2.88.2: request has been deprecated",
"npm warn deprecated inflight@1.0.6: This module is not supported",
"npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported",
"npm warn deprecated rimraf@2.7.1: Rimraf versions prior to v4 are no longer supported",
"",
"added 47 packages, and audited 48 packages in 13s",
"",
"3 packages are looking for funding",
" run `npm fund` for details",
"",
"4 vulnerabilities (2 moderate, 2 critical)",
"",
"Run `npm audit` for details.",
];
const body: Record<string, unknown> = {
conversationState: {
chatTriggerType: "MANUAL",
conversationId: "test-history",
currentMessage: {
userInputMessage: {
content: "What happened?",
modelId: "claude-sonnet-4.5",
},
},
history: [
{
userInputMessage: {
content: "Run npm install",
modelId: "claude-sonnet-4.5",
userInputMessageContext: {
toolResults: [
{
toolUseId: "tool_2",
status: "success",
content: [{ text: compilingLines.join("\n") }],
},
],
},
},
},
],
},
};
const originalLen = compilingLines.join("\n").length;
const result = applyRtkCompression(body);
assert.equal(result.compressed, true, "history tool result must compress");
const state = (result.body as Record<string, unknown>).conversationState as {
history: Array<{
userInputMessage: {
userInputMessageContext: { toolResults: Array<{ content: Array<{ text: string }> }> };
};
}>;
};
const afterLen = state.history[0].userInputMessage.userInputMessageContext.toolResults[0]
.content[0].text.length;
assert.ok(
afterLen < originalLen,
`history tool-result text must shrink (before=${originalLen}, after=${afterLen})`
);
});
it("preserves error tool results (status === 'error') without rewriting their text", () => {
const errorText =
"npm error code E404\nnpm error 404 Not Found - GET https://registry.npmjs.org/invalid-package";
const body: Record<string, unknown> = {
conversationState: {
chatTriggerType: "MANUAL",
conversationId: "test-error",
currentMessage: {
userInputMessage: {
content: "Install invalid-package",
modelId: "claude-sonnet-4.5",
userInputMessageContext: {
toolResults: [
{
toolUseId: "tool_err",
status: "error",
content: [{ text: errorText }],
},
],
},
},
},
history: [],
},
};
const result = applyRtkCompression(body);
const after = (
(result.body as Record<string, unknown>).conversationState as {
currentMessage: {
userInputMessage: {
userInputMessageContext: { toolResults: Array<{ content: Array<{ text: string }> }> };
};
};
}
).currentMessage.userInputMessage.userInputMessageContext.toolResults[0].content[0].text;
assert.equal(after, errorText, "error tool-result text must be preserved byte-for-byte");
});
it("handles a malformed Kiro body without crashing", () => {
const malformed: Array<Record<string, unknown>> = [
{ conversationState: null },
{ conversationState: {} },
{ conversationState: { history: null, currentMessage: null } },
{ conversationState: { history: "not-an-array" } },
];
for (const body of malformed) {
const result = applyRtkCompression(body);
assert.ok(result, "must not throw on malformed Kiro body");
// No tool-result text → engine reports no compression
assert.equal(result.compressed, false);
}
});
});

View File

@@ -0,0 +1,74 @@
/**
* Custom vision models in the LLM selector / `/v1/models` catalog
* (port of upstream decolua/9router 5e5e78d3 — "fix: show custom vision
* models in LLM selector and model list").
*
* Upstream stored an `imageToText` kind on user-added custom models and the
* selector hid them from the default LLM list. OmniRoute already keeps custom
* chat models in the LLM list, but the equivalent gap is the `supportsVision`
* flag on a custom model: the catalog used to only set `capabilities.vision`
* when the model id matched the conservative `isVisionModelId` heuristic
* (`pixtral`, `llava`, `qwen-vl`, …). A user who registered a custom chat
* model called e.g. `my-vision-llm` and explicitly checked "vision-capable"
* still saw no `capabilities.vision` in `/v1/models`, so the LLM selector and
* downstream routing treated it as text-only.
*
* This file pins the OmniRoute-flavoured behaviour:
* • a custom chat model with `supportsVision: true` MUST surface
* `capabilities: { vision: true }` via the catalog helper, regardless of
* whether the model id matches the static vision heuristic.
* • the existing id-based heuristic (`isVisionModelId`) still works.
* • when neither signal is present, no vision fields are emitted (no false
* positives — same conservative guard #4071 / #4072).
*/
import test from "node:test";
import assert from "node:assert/strict";
const catalog = await import("../../src/app/api/v1/models/catalog.ts");
type VisionFields = { capabilities: { vision: true } } | null;
type Helper = (entry: unknown, ...candidateIds: Array<string | null | undefined>) => VisionFields;
const getCustomVisionCapabilityFields = (
catalog as unknown as { getCustomVisionCapabilityFields?: Helper }
).getCustomVisionCapabilityFields;
test("custom vision helper is exported from the catalog module", () => {
assert.equal(
typeof getCustomVisionCapabilityFields,
"function",
"expected getCustomVisionCapabilityFields to be exported from src/app/api/v1/models/catalog.ts"
);
});
test("supportsVision flag on a custom model surfaces capabilities.vision (LLM selector unblock)", () => {
const helper = getCustomVisionCapabilityFields as Helper;
const entry = { id: "my-vision-llm", supportsVision: true };
const fields = helper(entry, "openai-compat/my-vision-llm", "my-vision-llm");
assert.ok(fields, "supportsVision=true must yield vision fields");
assert.equal(fields?.capabilities.vision, true);
});
test("id-based isVisionModelId heuristic still works when supportsVision is unset", () => {
const helper = getCustomVisionCapabilityFields as Helper;
const entry = { id: "pixtral-12b" };
const fields = helper(entry, "openai-compat/pixtral-12b", "pixtral-12b");
assert.ok(fields, "well-known vision model id must yield vision fields");
assert.equal(fields?.capabilities.vision, true);
});
test("no signal → no vision fields (avoids #4071-style false positives)", () => {
const helper = getCustomVisionCapabilityFields as Helper;
const entry = { id: "plain-text-llm" };
const fields = helper(entry, "openai-compat/plain-text-llm", "plain-text-llm");
assert.equal(fields, null);
});
test("explicit supportsVision=false is respected (text-only override)", () => {
const helper = getCustomVisionCapabilityFields as Helper;
// Even if the id matches the heuristic, an explicit false flag wins so the
// user can downgrade a mis-classified model.
const entry = { id: "pixtral-12b", supportsVision: false };
const fields = helper(entry, "alias/pixtral-12b", "pixtral-12b");
assert.equal(fields, null);
});

View File

@@ -0,0 +1,140 @@
/**
* tests/unit/migration-107-quota-share-strategy.test.ts
*
* Migration 107 flips the routing strategy of the existing auto-minted qtSd/ combos
* from the legacy "fill-first" to the dedicated "quota-share" engine (Fase 3 #9),
* WITHOUT touching combos already on quota-share or any user-authored combo.
*
* The migration is applied in isolation against an in-memory DB seeded with a mix
* of combos, so the assertions pin the exact WHERE scope (name LIKE 'qtSd/%' AND
* strategy = 'fill-first') and prove the other JSON fields are preserved.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import Database from "better-sqlite3";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const MIGRATION_107 = fs.readFileSync(
path.join(__dirname, "../../src/lib/db/migrations/107_quota_combos_quota_share_strategy.sql"),
"utf-8"
);
interface TestDb {
exec: (sql: string) => unknown;
prepare: (sql: string) => {
run: (...p: unknown[]) => unknown;
get: (...p: unknown[]) => Record<string, unknown> | undefined;
};
close: () => void;
}
function makeDb(): TestDb {
const db = new Database(":memory:") as unknown as TestDb;
db.exec(
`CREATE TABLE combos (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
data TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);`
);
return db;
}
function insertCombo(db: TestDb, id: string, name: string, strategy: string): void {
const data = JSON.stringify({
name,
strategy,
models: [{ kind: "model", model: "p/m", weight: 100 }],
isHidden: name.startsWith("qtSd/"),
});
db.prepare(
"INSERT INTO combos (id, name, data, created_at, updated_at) VALUES (?, ?, ?, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z')"
).run(id, name, data);
}
function strategyOf(db: TestDb, name: string): string | null {
const row = db
.prepare("SELECT json_extract(data, '$.strategy') AS s FROM combos WHERE name = ?")
.get(name);
return (row?.s as string | undefined) ?? null;
}
function dataOf(db: TestDb, name: string): Record<string, unknown> {
const row = db.prepare("SELECT data FROM combos WHERE name = ?").get(name);
return JSON.parse(row!.data as string) as Record<string, unknown>;
}
test("107 flips stale qtSd/ fill-first combos to quota-share", () => {
const db = makeDb();
insertCombo(db, "c1", "qtSd/grp/deepseek/deepseek-v4-flash", "fill-first");
insertCombo(db, "c2", "qtSd/grp/claude/claude-opus-4-8", "fill-first");
db.exec(MIGRATION_107);
assert.equal(strategyOf(db, "qtSd/grp/deepseek/deepseek-v4-flash"), "quota-share");
assert.equal(strategyOf(db, "qtSd/grp/claude/claude-opus-4-8"), "quota-share");
db.close();
});
test("107 preserves the other JSON fields (only strategy changes)", () => {
const db = makeDb();
insertCombo(db, "c1", "qtSd/grp/glm/glm-5", "fill-first");
db.exec(MIGRATION_107);
const data = dataOf(db, "qtSd/grp/glm/glm-5");
assert.equal(data.strategy, "quota-share");
assert.equal(data.isHidden, true, "isHidden must be preserved");
assert.deepEqual(
data.models,
[{ kind: "model", model: "p/m", weight: 100 }],
"models array must be preserved"
);
db.close();
});
test("107 leaves qtSd/ combos already on quota-share untouched", () => {
const db = makeDb();
insertCombo(db, "c1", "qtSd/grp/glm/glm-4.6", "quota-share");
db.exec(MIGRATION_107);
assert.equal(strategyOf(db, "qtSd/grp/glm/glm-4.6"), "quota-share");
db.close();
});
test("107 never touches user-authored (non-qtSd/) combos", () => {
const db = makeDb();
insertCombo(db, "c1", "my-coding-combo", "fill-first");
insertCombo(db, "c2", "team/fast", "fill-first");
db.exec(MIGRATION_107);
assert.equal(
strategyOf(db, "my-coding-combo"),
"fill-first",
"a user's fill-first combo must be preserved"
);
assert.equal(strategyOf(db, "team/fast"), "fill-first");
db.close();
});
test("107 is idempotent (second run is a no-op)", () => {
const db = makeDb();
insertCombo(db, "c1", "qtSd/grp/minimax/MiniMax-M2.7", "fill-first");
db.exec(MIGRATION_107);
const afterFirst = dataOf(db, "qtSd/grp/minimax/MiniMax-M2.7");
db.exec(MIGRATION_107);
const afterSecond = dataOf(db, "qtSd/grp/minimax/MiniMax-M2.7");
assert.equal(strategyOf(db, "qtSd/grp/minimax/MiniMax-M2.7"), "quota-share");
assert.deepEqual(afterFirst, afterSecond, "second run must not change the row");
db.close();
});

View File

@@ -1,8 +1,28 @@
import test from "node:test";
import assert from "node:assert/strict";
import dns from "node:dns";
import { handleImageGeneration } from "../../open-sse/handlers/imageGeneration.ts";
// Stub DNS for fetchRemoteImage's GHSA-cmhj-wh2f-9cgx DNS-rebinding guard
// (assertHostnameResolvesPublic in src/shared/network/remoteImageFetch.ts).
// The b64_json test mocks globalThis.fetch with an example.com URL that
// doesn't resolve in CI; the handler invokes fetchRemoteImage without
// exposing a `lookup` injection point, so we monkey-patch dns.promises.lookup
// to always return a public IP so the rebinding guard passes and the test
// exercises the mocked fetch behaviour as intended.
const originalDnsLookup = dns.promises.lookup;
(dns.promises as { lookup: unknown }).lookup = (async (
_hostname: string,
options?: { all?: boolean }
) => {
const record = { address: "203.0.113.1", family: 4 };
return options && options.all ? [record] : record;
}) as typeof dns.promises.lookup;
process.on("exit", () => {
(dns.promises as { lookup: unknown }).lookup = originalDnsLookup;
});
test("handleImageGeneration(nanobanana): async submit+poll returns URL payload", async () => {
const originalFetch = globalThis.fetch;
let pollCount = 0;

View File

@@ -232,3 +232,22 @@ test("next-intl webpack hook preserves caller config and filters known extractor
false
);
});
test("optimizePackageImports excludes the internal @omniroute/open-sse workspace (build-OOM guard)", async () => {
// Regression guard: adding the internal `@omniroute/open-sse` workspace to
// optimizePackageImports makes Next.js resolve its entire barrel at build
// time, driving the webpack production pass into a heap runaway that OOM'd
// even at 28 GB. optimizePackageImports is for EXTERNAL barrel libs only.
const { default: nextConfig } = await loadNextConfig("optimize-pkg-imports");
const list = nextConfig.experimental?.optimizePackageImports ?? [];
assert.ok(Array.isArray(list), "optimizePackageImports should be an array");
assert.ok(
!list.includes("@omniroute/open-sse"),
"do NOT add the internal @omniroute/open-sse workspace to optimizePackageImports — it OOMs the production build"
);
// The intended external barrel libs must remain optimized.
for (const lib of ["lucide-react", "date-fns", "next-intl"]) {
assert.ok(list.includes(lib), `expected external barrel lib ${lib} to stay optimized`);
}
});

View File

@@ -28,6 +28,7 @@ const {
CLAUDE_CONFIG,
CLINE_CONFIG,
CODEX_CONFIG,
CODEBUDDY_CN_CONFIG,
CURSOR_CONFIG,
GEMINI_CONFIG,
GITHUB_CONFIG,
@@ -66,6 +67,7 @@ const EXPECTED_PROVIDER_KEYS = [
"cline",
"windsurf",
"devin-cli",
"codebuddy-cn",
];
const EXPECTED_CONFIG_BY_PROVIDER = {
@@ -87,6 +89,7 @@ const EXPECTED_CONFIG_BY_PROVIDER = {
windsurf: WINDSURF_CONFIG,
"devin-cli": WINDSURF_CONFIG,
trae: TRAE_CONFIG,
"codebuddy-cn": CODEBUDDY_CN_CONFIG,
};
const REQUIRED_FIELDS_BY_PROVIDER = {
@@ -399,29 +402,21 @@ test("custom Google OAuth callbacks preserve the requested callback path and que
});
test("custom Google OAuth credentials switch IPv6 loopback callbacks to public base URL", () => {
const redirectUri = resolveBrowserOAuthRedirectUri(
"gemini-cli",
"http://[::1]:20128/callback",
{
OMNIROUTE_PUBLIC_BASE_URL: "https://omniroute.example.com",
GEMINI_OAUTH_CLIENT_ID: "custom-gemini.apps.googleusercontent.com",
GEMINI_OAUTH_CLIENT_SECRET: "custom-gemini-secret",
}
);
const redirectUri = resolveBrowserOAuthRedirectUri("gemini-cli", "http://[::1]:20128/callback", {
OMNIROUTE_PUBLIC_BASE_URL: "https://omniroute.example.com",
GEMINI_OAUTH_CLIENT_ID: "custom-gemini.apps.googleusercontent.com",
GEMINI_OAUTH_CLIENT_SECRET: "custom-gemini-secret",
});
assert.equal(redirectUri, "https://omniroute.example.com/callback");
});
test("custom Google OAuth callbacks default root loopback paths to callback path", () => {
const redirectUri = resolveBrowserOAuthRedirectUri(
"antigravity",
"http://127.0.0.1:20128",
{
NEXT_PUBLIC_BASE_URL: "https://omniroute.example.com",
ANTIGRAVITY_OAUTH_CLIENT_ID: "custom-antigravity.apps.googleusercontent.com",
ANTIGRAVITY_OAUTH_CLIENT_SECRET: "custom-antigravity-secret",
}
);
const redirectUri = resolveBrowserOAuthRedirectUri("antigravity", "http://127.0.0.1:20128", {
NEXT_PUBLIC_BASE_URL: "https://omniroute.example.com",
ANTIGRAVITY_OAUTH_CLIENT_ID: "custom-antigravity.apps.googleusercontent.com",
ANTIGRAVITY_OAUTH_CLIENT_SECRET: "custom-antigravity-secret",
});
assert.equal(redirectUri, "https://omniroute.example.com/callback");
});

View File

@@ -0,0 +1,38 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { filterToOpenAIFormat } from "../../open-sse/translator/helpers/openaiHelper.ts";
// Ported from upstream decolua/9router#1011 — many OpenAI-compatible providers
// reject the `developer` role (introduced by OpenAI Responses API). Normalize to
// `system` so passthrough requests keep working downstream.
test("filterToOpenAIFormat normalizes developer role to system", () => {
const body = {
messages: [
{ role: "developer", content: "You are helpful" },
{ role: "user", content: "Hi" },
],
};
const out = filterToOpenAIFormat(body);
assert.equal(out.messages[0].role, "system");
assert.equal(out.messages[0].content, "You are helpful");
assert.equal(out.messages[1].role, "user");
});
test("filterToOpenAIFormat normalizes developer role with array content", () => {
const body = {
messages: [
{
role: "developer",
content: [{ type: "text", text: "Be concise" }],
},
],
};
const out = filterToOpenAIFormat(body);
assert.equal(out.messages[0].role, "system");
assert.deepEqual(out.messages[0].content, [{ type: "text", text: "Be concise" }]);
});

View File

@@ -0,0 +1,94 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
const { OpencodeExecutor } = await import("../../open-sse/executors/opencode.ts");
/**
* Regression test for upstream decolua/9router#1099 (issue #1543):
*
* OpenCode is a meta-provider that proxies multi-turn chats to thinking-mode
* upstreams (DeepSeek V4 Flash, Kimi, MiniMax, ...). Those upstreams require
* `reasoning_content` to be echoed back on every assistant message in the
* conversation history; otherwise they return:
*
* 400 Bad Request — reasoning_content must be passed back
*
* Standard OpenAI clients do not preserve `reasoning_content` across turns, so
* OpencodeExecutor.transformRequest must inject a non-empty placeholder on any
* assistant message that lacks it when routing to a thinking-mode model.
*/
describe("OpencodeExecutor — DeepSeek reasoning_content injection (#1099)", () => {
const executor = new OpencodeExecutor("opencode-zen");
function buildBody(extraAssistant: Record<string, unknown>) {
return {
model: "oc/deepseek-v4-flash-free",
stream: true,
messages: [
{ role: "user", content: "hi" },
{ role: "assistant", content: "previous answer", ...extraAssistant },
{ role: "user", content: "follow up" },
],
};
}
it("injects reasoning_content on an assistant message that lacks it (deepseek model)", () => {
const out = executor.transformRequest(
"oc/deepseek-v4-flash-free",
buildBody({}),
true,
{ apiKey: "test" } as never
);
const assistant = out.messages.find((m: { role: string }) => m.role === "assistant");
assert.equal(typeof assistant.reasoning_content, "string");
assert.ok(
assistant.reasoning_content.length > 0,
"expected non-empty reasoning_content placeholder injected"
);
});
it("preserves an existing non-empty reasoning_content instead of overwriting", () => {
const original = "prior chain of thought";
const out = executor.transformRequest(
"oc/deepseek-v4-flash-free",
buildBody({ reasoning_content: original }),
true,
{ apiKey: "test" } as never
);
const assistant = out.messages.find((m: { role: string }) => m.role === "assistant");
assert.equal(assistant.reasoning_content, original);
});
it("injects for kimi-family models routed through opencode", () => {
const body = {
model: "oc/kimi-k2-thinking",
stream: false,
messages: [
{ role: "user", content: "hi" },
{ role: "assistant", content: "answer" },
],
};
const out = executor.transformRequest("oc/kimi-k2-thinking", body, false, {
apiKey: "test",
} as never);
const assistant = out.messages.find((m: { role: string }) => m.role === "assistant");
assert.equal(typeof assistant.reasoning_content, "string");
assert.ok(assistant.reasoning_content.length > 0);
});
it("does not inject for non-thinking opencode models (no false positives)", () => {
const body = {
model: "oc/gpt-5",
stream: false,
messages: [
{ role: "user", content: "hi" },
{ role: "assistant", content: "answer" },
],
};
const out = executor.transformRequest("oc/gpt-5", body, false, {
apiKey: "test",
} as never);
const assistant = out.messages.find((m: { role: string }) => m.role === "assistant");
assert.equal(assistant.reasoning_content, undefined);
});
});

View File

@@ -0,0 +1,45 @@
/**
* OpenCode Go model catalog alignment with the official Go docs.
*
* Port of decolua/9router 8efacc114 (thanks @nguyenha935): the official Go API
* advertises `glm-5.2` and routes Kimi chat traffic through `kimi-k2.7-code`
* (the live API rejects the plain `kimi-k2.7` alias for `/chat/completions`
* even though the public docs example uses it). OmniRoute previously shipped
* the older registry without these IDs.
*/
import test from "node:test";
import assert from "node:assert/strict";
const { opencode_goProvider } = await import(
"../../open-sse/config/providers/registry/opencode/go/index.ts"
);
function modelIds(): string[] {
return (opencode_goProvider.models ?? []).map((m) => m.id);
}
test("opencode-go advertises glm-5.2 (official Go endpoint addition)", () => {
assert.ok(
modelIds().includes("glm-5.2"),
`expected glm-5.2 in opencode-go catalog, got: ${modelIds().join(", ")}`
);
});
test("opencode-go advertises kimi-k2.7-code (live API rejects plain kimi-k2.7 for chat)", () => {
assert.ok(
modelIds().includes("kimi-k2.7-code"),
`expected kimi-k2.7-code in opencode-go catalog, got: ${modelIds().join(", ")}`
);
});
test("opencode-go preserves the pre-existing minimax-m3 and qwen routing via targetFormat=claude", () => {
// Routing through the /messages endpoint is OmniRoute's declarative
// equivalent of upstream's MESSAGES_FORMAT_MODELS set; the alignment
// change must not regress this.
const byId = new Map(
(opencode_goProvider.models ?? []).map((m) => [m.id, m as Record<string, unknown>])
);
assert.equal(byId.get("minimax-m3")?.targetFormat, "claude");
assert.equal(byId.get("qwen3.7-max")?.targetFormat, "claude");
});

View File

@@ -0,0 +1,168 @@
import { describe, it, beforeEach, afterEach, before, after } from "node:test";
import assert from "node:assert";
import net from "node:net";
import { OpencodeExecutor } from "../../open-sse/executors/opencode.ts";
import { resolveProxyForRequest } from "../../open-sse/utils/proxyFetch.ts";
/**
* #4954 — "OpenCode Free" exposes per-account proxy + multi-account rotation in
* the UI (NoAuthAccountCard persists providerSpecificData.fingerprints +
* providerSpecificData.accountProxies), but the executor ignored them entirely:
* every request egressed direct and never rotated. These tests pin the wiring:
*
* 1. A request for an account that has a configured proxy must egress THROUGH
* that proxy (resolveProxyForRequest reports source "context", not "direct").
* 2. On a 429 the executor must rotate to the NEXT account (and its proxy).
*
* The dispatch layer is mocked by stubbing globalThis.fetch — exactly what the
* proxy context wraps — and we observe the proxy that resolveProxyForRequest sees
* for the in-flight request, mirroring the mimocode proxy integration test. Two
* throwaway local TCP listeners stand in for the proxies so runWithProxyContext's
* fast-fail reachability probe passes without a live SOCKS/HTTP proxy.
*/
const log = { debug() {}, info() {}, warn() {}, error() {} };
const ACCOUNT_A = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const ACCOUNT_B = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
// Two local listeners so the proxy fast-fail reachability check succeeds. The
// proxy host/port are observable in the egress context — that is what asserts the
// per-account proxy is honored (was always "direct" before #4954).
let serverA: net.Server;
let serverB: net.Server;
let portA = 0;
let portB = 0;
function listen(server: net.Server): Promise<number> {
return new Promise((resolve) => {
server.listen(0, "127.0.0.1", () => {
resolve((server.address() as net.AddressInfo).port);
});
});
}
before(async () => {
serverA = net.createServer((s) => s.destroy());
serverB = net.createServer((s) => s.destroy());
portA = await listen(serverA);
portB = await listen(serverB);
});
after(() => {
serverA?.close();
serverB?.close();
});
function credentialsWithProxies() {
return {
apiKey: null,
accessToken: null,
connectionId: "noauth",
providerSpecificData: {
fingerprints: [ACCOUNT_A, ACCOUNT_B],
accountProxies: [
{ fingerprint: ACCOUNT_A, proxy: { type: "http", host: "127.0.0.1", port: portA } },
{ fingerprint: ACCOUNT_B, proxy: { type: "http", host: "127.0.0.1", port: portB } },
],
},
} as any;
}
describe("OpencodeExecutor per-account proxy + rotation (#4954)", () => {
let originalFetch: typeof globalThis.fetch;
let observed: Array<{ source: string; host: string | null; port: string | null }>;
beforeEach(() => {
originalFetch = globalThis.fetch;
observed = [];
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
/** Record the proxy context resolved for each dispatch, then return `status`. */
function installFetchStub(statuses: number[]) {
let call = 0;
globalThis.fetch = (async (input: any) => {
const url = typeof input === "string" ? input : input?.url || String(input);
const resolved = resolveProxyForRequest(url);
let host: string | null = null;
let port: string | null = null;
try {
if (resolved.proxyUrl) {
const u = new URL(resolved.proxyUrl);
host = u.hostname;
port = u.port;
}
} catch {
host = resolved.proxyUrl;
}
observed.push({ source: resolved.source, host, port });
const status = statuses[Math.min(call, statuses.length - 1)];
call++;
return new Response(JSON.stringify({ ok: status === 200 }), {
status,
headers: { "Content-Type": "application/json" },
});
}) as typeof globalThis.fetch;
}
it("dispatches through the selected account's proxy (not direct)", async () => {
const exec = new OpencodeExecutor("opencode-zen");
installFetchStub([200]);
const result = await exec.execute({
model: "grok-code",
body: { messages: [{ role: "user", content: "hi" }], stream: false },
stream: false,
signal: null,
credentials: credentialsWithProxies(),
log,
});
assert.strictEqual((result as any).response.status, 200);
assert.ok(observed.length >= 1, "at least one dispatch happened");
const first = observed[0];
assert.strictEqual(
first.source,
"context",
`expected proxy-context egress, got source="${first.source}" (was always "direct" before #4954)`
);
assert.strictEqual(first.host, "127.0.0.1", "egress must use a configured proxy host");
assert.ok(
first.port === String(portA) || first.port === String(portB),
`expected one of the configured proxy ports, got "${first.port}"`
);
});
it("rotates to the next account (and its proxy) on a 429", async () => {
const exec = new OpencodeExecutor("opencode-zen");
// first account → 429, second account → 200
installFetchStub([429, 200]);
const result = await exec.execute({
model: "grok-code",
body: { messages: [{ role: "user", content: "hi" }], stream: false },
stream: false,
signal: null,
credentials: credentialsWithProxies(),
log,
});
assert.strictEqual((result as any).response.status, 200, "final response should succeed");
assert.ok(observed.length >= 2, "should have retried on a second account after 429");
const ports = observed.map((p) => p.port);
assert.ok(ports.includes(String(portA)), "first attempt should use account A's proxy");
assert.ok(ports.includes(String(portB)), "rotated attempt should use account B's proxy");
assert.notStrictEqual(
observed[0].port,
observed[1].port,
"rotation must switch to a different account/proxy"
);
for (const p of observed) {
assert.strictEqual(p.source, "context", "every dispatch must egress through a proxy context");
}
});
});

View File

@@ -0,0 +1,184 @@
/**
* tests/unit/ops-scripts.test.ts
*
* Contract tests for the incident-runbook ops scripts under bin/ (referenced by
* docs/INCIDENT_RESPONSE.md and docs/PERF_BUDGETS.md):
* rollback.sh · snapshot-data.sh · restore-data.sh · restore-policies.sh ·
* cold-start-bench.sh
*
* These scripts touch deploys and the SQLite store, so the suite pins down the
* safety contract rather than full ops behavior: every script is executable
* bash with strict mode, prints usage on --help, the restore commands refuse to
* run without a snapshot id, and a snapshot→restore round-trip works while the
* non-interactive TTY guard blocks an unattended destructive restore.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const ROOT = path.resolve(import.meta.dirname, "..", "..");
const BIN = path.join(ROOT, "bin");
const SCRIPTS = [
"rollback.sh",
"snapshot-data.sh",
"restore-data.sh",
"restore-policies.sh",
"cold-start-bench.sh",
];
const hasSqlite3 = spawnSync("sqlite3", ["--version"], { stdio: "ignore" }).status === 0;
/** Run a bin/ script with a NON-tty stdin (so the TTY guard engages). */
function runScript(script: string, args: string[], env: Record<string, string> = {}) {
return spawnSync("bash", [path.join(BIN, script), ...args], {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env, ...env },
});
}
describe("ops runbook scripts (bin/*.sh)", () => {
it("every script exists, is executable, and uses bash + strict mode", () => {
for (const s of SCRIPTS) {
const p = path.join(BIN, s);
assert.ok(fs.existsSync(p), `${s} is missing`);
assert.ok(fs.statSync(p).mode & 0o111, `${s} is not executable (chmod +x)`);
const body = fs.readFileSync(p, "utf8");
assert.ok(body.startsWith("#!/usr/bin/env bash"), `${s} missing bash shebang`);
assert.ok(body.includes("set -euo pipefail"), `${s} missing 'set -euo pipefail'`);
}
});
it("the shared helper and every script pass `bash -n` (syntax check)", () => {
for (const s of [...SCRIPTS, "_ops-common.sh"]) {
const r = spawnSync("bash", ["-n", path.join(BIN, s)], { encoding: "utf8" });
assert.equal(r.status, 0, `${s} has a syntax error: ${r.stderr}`);
}
});
it("--help exits 0 with a usage banner for every script", () => {
for (const s of SCRIPTS) {
const r = runScript(s, ["--help"]);
assert.equal(r.status, 0, `${s} --help exited ${r.status}: ${r.stderr}`);
assert.match(r.stdout, /Usage:/, `${s} --help printed no usage banner`);
}
});
it("restore scripts refuse to run without a snapshot id", () => {
for (const s of ["restore-data.sh", "restore-policies.sh"]) {
const r = runScript(s, []);
assert.notEqual(r.status, 0, `${s} should fail without an id`);
assert.match(r.stderr, /snapshot id required/, `${s} wrong error: ${r.stderr}`);
}
});
it("snapshot → restore-data round-trips, and the TTY guard blocks unattended restores", async () => {
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "omni-ops-"));
try {
const Database = (await import("better-sqlite3")).default;
const dbPath = path.join(dataDir, "storage.sqlite");
let db = new Database(dbPath);
db.exec(
"CREATE TABLE api_keys (id TEXT PRIMARY KEY, name TEXT);" +
"INSERT INTO api_keys VALUES ('k1','orig');"
);
db.close();
const env = { DATA_DIR: dataDir };
const snap = runScript("snapshot-data.sh", ["--label", "test"], env);
assert.equal(snap.status, 0, `snapshot failed: ${snap.stderr}`);
const id = snap.stdout.trim();
assert.ok(id, "snapshot id not printed on stdout");
assert.ok(
fs.existsSync(path.join(dataDir, "db_backups", `snapshot_${id}`, "storage.sqlite")),
"snapshot dir not created"
);
// Mutate the live DB so a successful restore is observable.
db = new Database(dbPath);
db.exec("UPDATE api_keys SET name='changed' WHERE id='k1';");
db.close();
// Guard: non-interactive restore WITHOUT --yes must refuse (nothing destroyed).
const blocked = runScript("restore-data.sh", [id], env);
assert.notEqual(blocked.status, 0, "restore without --yes should be blocked");
assert.match(blocked.stderr, /TTY/, `expected TTY guard, got: ${blocked.stderr}`);
db = new Database(dbPath);
assert.equal(
(db.prepare("SELECT name FROM api_keys WHERE id='k1'").get() as { name: string }).name,
"changed",
"blocked restore must not have changed the DB"
);
db.close();
// With --yes it reverts to the snapshot.
const ok = runScript("restore-data.sh", [id, "--yes"], env);
assert.equal(ok.status, 0, `restore failed: ${ok.stderr}`);
db = new Database(dbPath);
assert.equal(
(db.prepare("SELECT name FROM api_keys WHERE id='k1'").get() as { name: string }).name,
"orig",
"restore did not revert the row"
);
db.close();
} finally {
fs.rmSync(dataDir, { recursive: true, force: true });
}
});
it(
"restore-policies replaces only api_key* tables, preserving other tables",
{ skip: hasSqlite3 ? false : "sqlite3 CLI not installed" },
async () => {
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "omni-pol-"));
try {
const Database = (await import("better-sqlite3")).default;
const dbPath = path.join(dataDir, "storage.sqlite");
let db = new Database(dbPath);
db.exec(
"CREATE TABLE api_keys (id TEXT PRIMARY KEY, name TEXT);" +
"INSERT INTO api_keys VALUES ('k1','orig');" +
"CREATE TABLE sessions (id TEXT PRIMARY KEY);" +
"INSERT INTO sessions VALUES ('s-old');"
);
db.close();
const env = { DATA_DIR: dataDir };
const id = runScript("snapshot-data.sh", [], env).stdout.trim();
assert.ok(id, "snapshot id not printed");
// Change BOTH a policy table and a non-policy table after the snapshot.
db = new Database(dbPath);
db.exec(
"UPDATE api_keys SET name='changed' WHERE id='k1';" +
"INSERT INTO sessions VALUES ('s-new');"
);
db.close();
const r = runScript("restore-policies.sh", [id, "--yes"], env);
assert.equal(r.status, 0, `restore-policies failed: ${r.stderr}`);
db = new Database(dbPath);
// Policy table reverted…
assert.equal(
(db.prepare("SELECT name FROM api_keys WHERE id='k1'").get() as { name: string }).name,
"orig",
"api_keys policy was not restored"
);
// …non-policy table left intact (live usage not rewound).
assert.equal(
(db.prepare("SELECT COUNT(*) c FROM sessions").get() as { c: number }).c,
2,
"non-policy table must not be touched by restore-policies"
);
db.close();
} finally {
fs.rmSync(dataDir, { recursive: true, force: true });
}
}
);
});

View File

@@ -0,0 +1,50 @@
// Regression test for perplexity API key validation.
//
// Perplexity deprecated the unversioned `/models` endpoint (returns 404), so
// our default validation probe — which derives `<baseUrl>/models` from the
// perplexity registry entry via `addModelsSuffix` — would always fail to
// confirm a valid key, falling through to the chat-completions probe and
// often misclassifying live keys as "Invalid". Inspired by upstream
// 9router fix (see commit message); we port it OmniRoute-style by
// declaring an explicit `modelsUrl` on the perplexity registry entry.
import { describe, it } from "node:test";
import assert from "node:assert";
describe("perplexity registry — key validation models endpoint", () => {
it("declares a modelsUrl pointing at /v1/models (not the deprecated /models)", async () => {
const { getRegistryEntry } = await import(
"../../open-sse/config/providerRegistry.ts"
);
const entry = getRegistryEntry("perplexity");
assert.ok(entry, "perplexity must be registered in the execution registry");
assert.equal(entry.format, "openai");
assert.ok(
typeof entry.modelsUrl === "string" && entry.modelsUrl.length > 0,
"perplexity registry entry must declare an explicit modelsUrl so key " +
"validation does not hit the deprecated <baseUrl>/models endpoint"
);
assert.equal(
entry.modelsUrl,
"https://api.perplexity.ai/v1/models",
"perplexity modelsUrl must point at the versioned /v1/models endpoint " +
"(unversioned /models was deprecated and now returns 404)"
);
});
it("does not derive a /models URL that ends in /chat/completions/models", async () => {
const { getRegistryEntry } = await import(
"../../open-sse/config/providerRegistry.ts"
);
const entry = getRegistryEntry("perplexity");
assert.ok(entry?.modelsUrl, "modelsUrl required");
assert.ok(
!entry.modelsUrl.includes("/chat/completions"),
"modelsUrl must not include /chat/completions"
);
assert.ok(
entry.modelsUrl.endsWith("/v1/models"),
"modelsUrl must end with /v1/models"
);
});
});

View File

@@ -0,0 +1,38 @@
import test from "node:test";
import assert from "node:assert/strict";
import { PollinationsExecutor } from "../../open-sse/executors/pollinations.ts";
// #3981: Pollinations rejects (HTTP 400 "messages must contain the word 'json'")
// any request with jsonMode=true whose messages don't mention "json". Setting
// jsonMode unconditionally broke every normal chat request. jsonMode must only
// be enabled when the caller actually requested JSON output.
test("#3981 transformRequest does NOT force jsonMode for a normal request", () => {
const executor = new PollinationsExecutor();
const body: any = {
messages: [{ role: "user", content: "Hello there" }],
};
const out = executor.transformRequest("openai", body, true, null);
assert.notEqual(out.jsonMode, true);
});
test("#3981 transformRequest enables jsonMode when response_format json_object is requested", () => {
const executor = new PollinationsExecutor();
const body: any = {
messages: [{ role: "user", content: "Return json" }],
response_format: { type: "json_object" },
};
const out = executor.transformRequest("openai", body, true, null);
assert.equal(out.jsonMode, true);
});
test("#3981 transformRequest enables jsonMode when response_format json_schema is requested", () => {
const executor = new PollinationsExecutor();
const body: any = {
messages: [{ role: "user", content: "Return structured json" }],
response_format: { type: "json_schema", json_schema: { name: "x", schema: {} } },
};
const out = executor.transformRequest("openai", body, true, null);
assert.equal(out.jsonMode, true);
});

View File

@@ -0,0 +1,56 @@
// Characterization of the pricing.ts split (god-file decomposition): the host became a barrel that
// re-exports DEFAULT_PRICING (now merged from 4 semantic family files that import shared tier consts)
// and keeps the 3 helper functions. Pure-data move → behavior identical. Locks: public surface, the
// spread-merge integrity, and that lookups/cost math resolve unchanged.
import { test } from "node:test";
import assert from "node:assert/strict";
const P = await import("../../src/shared/constants/pricing.ts");
test("barrel still exports DEFAULT_PRICING + the 3 helpers", () => {
for (const name of [
"DEFAULT_PRICING",
"getPricingForModel",
"getDefaultPricing",
"calculateCostFromTokens",
]) {
assert.ok(name in P, `missing export: ${name}`);
}
});
test("DEFAULT_PRICING merges the 4 family files; families partition all entries", async () => {
const merged = Object.keys((P as Record<string, object>).DEFAULT_PRICING).length;
const families: [string, string][] = [
["oauth-subscriptions", "DEFAULT_PRICING_OAUTH"],
["frontier-labs", "DEFAULT_PRICING_FRONTIER"],
["inference-hosts", "DEFAULT_PRICING_INFERENCE"],
["regional", "DEFAULT_PRICING_REGIONAL"],
];
let famTotal = 0;
const seen = new Set<string>();
for (const [file, exportName] of families) {
const mod = await import(`../../src/shared/constants/pricing/${file}.ts`);
for (const k of Object.keys(mod[exportName])) {
assert.ok(!seen.has(k), `pricing key ${k} appears in more than one family`);
seen.add(k);
famTotal++;
}
}
assert.equal(merged, famTotal, "spread-merge lost/duplicated a top-level key");
assert.ok(merged > 25);
});
test("shared tier consts feed the parts (a known model resolves to a shared rate)", () => {
const pricing = (P as Record<string, (p: string, m: string) => unknown>).getPricingForModel(
"openai",
"gpt-4o"
);
assert.ok(pricing && typeof pricing === "object");
assert.equal(typeof (pricing as { input?: number }).input, "number");
});
test("calculateCostFromTokens stays callable and numeric", () => {
const fn = (P as Record<string, (...a: unknown[]) => unknown>).calculateCostFromTokens;
const out = fn("openai", "gpt-4o", { prompt_tokens: 1000, completion_tokens: 1000 });
assert.equal(typeof out, "number");
});

View File

@@ -1,6 +1,6 @@
/**
* #4091 — MCP tool dispatch broken in Claude Code ("No such tool available")
* on native Claude OAuth, regression from v3.8.27.
* #4091 — third-party tool-name cloak map must survive the request-capture
* round-trip (native Claude OAuth; original symptom: "No such tool available").
*
* Root cause: the native-Claude tool-name cloak stores the per-request
* alias→original map as a NON-ENUMERABLE `_toolNameMap` on the request body.
@@ -10,8 +10,13 @@
* `finalBody = providerRequestCapture.body(transformedBody)` then resolves to
* that round-tripped copy, so the response-side un-cloak
* (`mergeResponseToolNameMap` → `remapToolNamesInResponse`) sees an empty map
* and the cloaked PascalCase name (`McpN8nMcpSearchWorkflows`) streams verbatim
* to Claude Code, which rejects it client-side.
* and the cloaked PascalCase name streams verbatim to Claude Code, which
* rejects it client-side.
*
* The example tool is a generic third-party name (`search_workflows`), not an
* `mcp__…` name: #4861 deliberately exempts the `mcp__` namespace from cloaking
* (Anthropic accepts those natively), so only non-`mcp__` third-party tools are
* cloaked and exercise this map-preservation path.
*
* These tests pin the lossy boundary: `createPreparedRequestLogger().body()`
* must preserve the cloak map from the real (fallback) transformed body even
@@ -40,8 +45,8 @@ function makeCapture() {
return { capture: createPreparedRequestLogger(reqLogger, scope), logged };
}
const ALIAS = "McpN8nMcpSearchWorkflows";
const ORIGINAL = "mcp__n8n-mcp__search_workflows";
const ALIAS = "SearchWorkflows";
const ORIGINAL = "search_workflows";
function makeCloakedClaudeBody(): Record<string, unknown> {
const body: Record<string, unknown> = {

View File

@@ -168,7 +168,10 @@ test("anthropic-compatible validation requires a base URL", async () => {
assert.match(result.error, /No base URL configured/i);
});
test("anthropic-compatible validation rejects invalid keys from /models", async () => {
test("anthropic-compatible validation rejects invalid keys (auth-fail on both /models and /messages)", async () => {
// After the 584cf66a port, /models alone is not authoritative — many compatible
// proxies 401/403 on /models even with a valid key. To prove the key is bad we
// require an auth-shaped failure on POST /v1/messages too.
const calls = [];
globalThis.fetch = async (url) => {
calls.push(String(url));
@@ -183,7 +186,10 @@ test("anthropic-compatible validation rejects invalid keys from /models", async
assert.equal(result.valid, false);
assert.equal(result.error, "Invalid API key");
assert.deepEqual(calls, ["https://api.example.com/v1/models"]);
assert.deepEqual(calls, [
"https://api.example.com/v1/models",
"https://api.example.com/v1/messages",
]);
});
test("anthropic-compatible validation falls back to /messages and treats 400 as auth success", async () => {

View File

@@ -0,0 +1,87 @@
// 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 (157 APIKEY entries, no loss/dup), and that
// load-time Zod validation still runs. Pure-data move → behavior must be identical.
import { test } from "node:test";
import assert from "node:assert/strict";
const P = await import("../../src/shared/constants/providers.ts");
test("barrel still exports every catalog + key helpers", () => {
for (const name of [
"NOAUTH_PROVIDERS",
"OAUTH_PROVIDERS",
"WEB_COOKIE_PROVIDERS",
"APIKEY_PROVIDERS",
"LOCAL_PROVIDERS",
"SEARCH_PROVIDERS",
"AUDIO_ONLY_PROVIDERS",
"UPSTREAM_PROXY_PROVIDERS",
"CLOUD_AGENT_PROVIDERS",
"SYSTEM_PROVIDERS",
"AI_PROVIDERS",
"ALIAS_TO_ID",
"ID_TO_ALIAS",
"getProviderById",
"getProviderByAlias",
"resolveProviderId",
]) {
assert.ok(name in P, `missing export: ${name}`);
}
});
test("APIKEY_PROVIDERS merges the 6 family files into 157 entries (no loss / no dup)", async () => {
const keys = Object.keys((P as Record<string, object>).APIKEY_PROVIDERS);
assert.equal(keys.length, 157);
assert.equal(new Set(keys).size, 157, "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 157.
const families: [string, string][] = [
["gateways", "APIKEY_PROVIDERS_GATEWAYS"],
["frontier-labs", "APIKEY_PROVIDERS_FRONTIER"],
["inference-hosts", "APIKEY_PROVIDERS_INFERENCE"],
["enterprise-cloud", "APIKEY_PROVIDERS_ENTERPRISE"],
["regional", "APIKEY_PROVIDERS_REGIONAL"],
["specialty-media", "APIKEY_PROVIDERS_SPECIALTY"],
];
let famTotal = 0;
const seen = new Set<string>();
for (const [file, exportName] of families) {
const mod = await import(`../../src/shared/constants/providers/apikey/${file}.ts`);
const famKeys = Object.keys(mod[exportName]);
famTotal += famKeys.length;
for (const k of famKeys) {
assert.ok(!seen.has(k), `provider ${k} appears in more than one family`);
seen.add(k);
}
}
assert.equal(famTotal, 157, "families must partition all 157 providers");
});
test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => {
const ai = (P as Record<string, Record<string, unknown>>).AI_PROVIDERS;
assert.ok(Object.keys(ai).length > 200);
assert.ok((P as Record<string, (id: string) => unknown>).getProviderById("openai"));
assert.ok((P as Record<string, (id: string) => unknown>).getProviderById("claude"));
// a moved catalog is reachable through the barrel re-export
assert.ok((P as Record<string, Record<string, unknown>>).APIKEY_PROVIDERS["openai"]);
});
test("each extracted data module is importable on its own", async () => {
const mods = [
["noauth", "NOAUTH_PROVIDERS"],
["oauth", "OAUTH_PROVIDERS"],
["web-cookie", "WEB_COOKIE_PROVIDERS"],
["local", "LOCAL_PROVIDERS"],
["search", "SEARCH_PROVIDERS"],
["audio", "AUDIO_ONLY_PROVIDERS"],
["upstream-proxy", "UPSTREAM_PROXY_PROVIDERS"],
["cloud-agent", "CLOUD_AGENT_PROVIDERS"],
["system", "SYSTEM_PROVIDERS"],
];
for (const [file, name] of mods) {
const m = await import(`../../src/shared/constants/providers/${file}.ts`);
assert.ok(m[name] && typeof m[name] === "object", `${file}.ts must export ${name}`);
}
});

View File

@@ -18,14 +18,19 @@ import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
test("bypassProxyPatch is present in the NVIDIA validation path (#3226 documented exception)", () => {
const validation = readFileSync("src/lib/providers/validation.ts", "utf8");
// After the validation.ts godfile split (#4921#4930), bypassProxyPatch moved to the
// extracted validation/headers.ts. Assert the bypass lives in the validation LAYER
// (validation.ts + its extracted modules), not that it stayed in one file.
const validationLayer =
readFileSync("src/lib/providers/validation.ts", "utf8") +
readFileSync("src/lib/providers/validation/headers.ts", "utf8");
assert.ok(
validation.includes("bypassProxyPatch"),
"expected bypassProxyPatch to be present in validation.ts (the documented NVIDIA exception)"
validationLayer.includes("bypassProxyPatch"),
"expected bypassProxyPatch in the validation layer (validation/headers.ts) — the documented NVIDIA exception"
);
assert.ok(
validation.includes("directHttpsRequest"),
"expected directHttpsRequest helper to be present in validation.ts"
validationLayer.includes("directHttpsRequest"),
"expected directHttpsRequest helper in the validation layer"
);
});

View File

@@ -0,0 +1,183 @@
import test from "node:test";
import assert from "node:assert/strict";
// Port of upstream decolua/9router PR #1360: Cloudflare Workers as proxy relay.
//
// Architecture mirrors the existing Vercel relay (same x-relay-target /
// x-relay-path / x-relay-auth header spec, same SSRF guard inlined into the
// worker, same fail-closed missing-relayAuth check). Only the deployment
// target changes: instead of POSTing to the Vercel /v13/deployments API,
// we PUT a Worker script to Cloudflare's accounts/{accountId}/workers/scripts
// API and enable the workers.dev subdomain.
//
// Coverage:
// 1. buildCloudflareWorkerScript(relayAuth) returns an Edge-runtime worker
// module whose source enforces the same x-relay-auth check as the
// Vercel relay (so a leaked workers.dev URL cannot be used as an
// open SSRF proxy by a third party).
// 2. proxyFetch's relay short-circuit treats type "cloudflare" the same
// way as "vercel" — uses buildVercelRelayHeaders + routes to the
// workers.dev origin, never the upstream target.
// 3. The proxyDispatcher / DB layer recognise "cloudflare" as a relay
// type (extractRelayAuth fires, dispatcher returns the worker URL).
// --- Install the relay sink BEFORE importing the module under test. ---
type FetchCall = { input: unknown; init: RequestInit & { headers?: HeadersInit } };
const relayCalls: FetchCall[] = [];
const realGlobalFetch = globalThis.fetch;
const relaySink = (async (input: unknown, init: RequestInit = {}) => {
relayCalls.push({ input, init });
return Response.json({ via: "cloudflare-relay" });
}) as unknown as typeof globalThis.fetch;
globalThis.fetch = relaySink;
const proxyDispatcher = await import("../../open-sse/utils/proxyDispatcher.ts");
const { buildVercelRelayHeaders, proxyConfigToUrl } = proxyDispatcher;
const proxyFetchMod = await import("../../open-sse/utils/proxyFetch.ts");
const { proxyFetch, runWithProxyContext } = proxyFetchMod;
const cfDeploy = await import("../../src/lib/proxyRelay/cloudflareWorkerScript.ts");
const { buildCloudflareWorkerScript } = cfDeploy;
test.after(() => {
globalThis.fetch = realGlobalFetch;
});
test.beforeEach(() => {
relayCalls.length = 0;
});
// --------------------------------------------------------------------------
// 1. buildCloudflareWorkerScript — emitted worker source contract
// --------------------------------------------------------------------------
test("buildCloudflareWorkerScript embeds the supplied relayAuth literal", () => {
const src = buildCloudflareWorkerScript("a-very-specific-secret-token");
assert.ok(
src.includes('"a-very-specific-secret-token"'),
"worker source must embed the relayAuth secret as a string literal"
);
});
test("buildCloudflareWorkerScript rejects requests without a valid x-relay-auth header", () => {
// The worker source must enforce the same auth check as the Vercel relay:
// a 401 short-circuit when x-relay-auth does not match the embedded token.
// We don't run the worker here — we check the source contains the guard.
const src = buildCloudflareWorkerScript("the-secret");
assert.ok(
/x-relay-auth/.test(src),
"worker source must reference the x-relay-auth header"
);
assert.ok(
/401|Unauthorized/.test(src),
"worker source must short-circuit unauthorised requests with 401"
);
});
test("buildCloudflareWorkerScript blocks loopback / RFC1918 / link-local hosts (SSRF guard)", () => {
// Mirrors the Vercel relay's inlined SSRF guard. A leaked workers.dev URL
// must not be usable to scan internal networks.
const src = buildCloudflareWorkerScript("tok");
// The guard recognises private CIDRs / loopback by literal substrings in
// the inline function. These specific tokens are load-bearing.
assert.ok(/127\.0\.0\.1|localhost/.test(src), "blocks loopback hosts");
assert.ok(/192\.168|10\.|172/.test(src), "blocks RFC1918 hosts");
assert.ok(/169\.254|link-local|fe80/.test(src), "blocks link-local hosts");
});
test("buildCloudflareWorkerScript uses ESM default-export fetch handler (Workers Modules format)", () => {
// Cloudflare's PUT /workers/scripts API expects a module-format worker
// (main_module = index.js, content-type application/javascript+module).
// The handler must be exposed as `export default { fetch }`.
const src = buildCloudflareWorkerScript("tok");
assert.ok(/export\s+default/.test(src), "must be an ES module (export default)");
assert.ok(/fetch\s*\(/.test(src), "must export a fetch handler");
});
// --------------------------------------------------------------------------
// 2. proxyFetch — cloudflare type takes the relay short-circuit
// --------------------------------------------------------------------------
const CLOUDFLARE_CTX = {
type: "cloudflare" as const,
host: "omniroute-relay.acme.workers.dev",
relayAuth: "live-cf-secret",
};
test("proxyFetch routes a cloudflare-type context through the relay endpoint with relay headers", async () => {
const response = await runWithProxyContext(CLOUDFLARE_CTX, () =>
proxyFetch("https://api.anthropic.com/v1/messages?x=1", {
method: "POST",
headers: { "x-existing": "keep-me" },
})
);
assert.deepEqual(await response.json(), { via: "cloudflare-relay" });
assert.equal(relayCalls.length, 1, "exactly one relay dispatch");
const call = relayCalls[0];
// Rewritten to the workers.dev origin, NOT the upstream target.
assert.equal(call.input, "https://omniroute-relay.acme.workers.dev");
const sentHeaders = new Headers(call.init.headers);
assert.equal(sentHeaders.get("x-relay-target"), "https://api.anthropic.com");
assert.equal(sentHeaders.get("x-relay-path"), "/v1/messages?x=1");
assert.equal(sentHeaders.get("x-relay-auth"), "live-cf-secret");
assert.equal(sentHeaders.get("x-existing"), "keep-me");
assert.equal(call.init.method, "POST");
assert.equal((call.init as { duplex?: string }).duplex, "half");
});
test("proxyFetch throws (without dispatching) when a cloudflare context is missing relayAuth", async () => {
await assert.rejects(
runWithProxyContext({ type: "cloudflare", host: "x.workers.dev" }, () =>
proxyFetch("https://api.anthropic.com/v1/messages", { method: "POST" })
),
/relay configuration error: missing relayAuth/
);
assert.equal(relayCalls.length, 0, "no relay dispatch when relayAuth is missing");
});
test("the missing-relayAuth error message does not leak internal [ProxyFetch] diagnostics", async () => {
await runWithProxyContext({ type: "cloudflare", host: "x.workers.dev" }, async () => {
try {
await proxyFetch("https://api.anthropic.com/v1/messages", { method: "POST" });
assert.fail("expected the relay branch to throw on missing relayAuth");
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
assert.ok(!message.includes("[ProxyFetch]"), "no internal [ProxyFetch] label");
assert.ok(!message.includes("at /"), "no stack-trace path leaked");
}
});
});
// --------------------------------------------------------------------------
// 3. proxyConfigToUrl — cloudflare type yields the worker https origin
// --------------------------------------------------------------------------
test("proxyConfigToUrl returns the cloudflare worker URL (no HTTP-proxy dispatcher needed)", () => {
const url = proxyConfigToUrl({
type: "cloudflare",
host: "omniroute-relay.acme.workers.dev",
});
assert.equal(url, "https://omniroute-relay.acme.workers.dev");
});
// --------------------------------------------------------------------------
// 4. buildVercelRelayHeaders is shared (renamed-or-aliased? at minimum still
// works for cloudflare — same header spec).
// --------------------------------------------------------------------------
test("buildVercelRelayHeaders is the shared relay-header builder used for cloudflare too", () => {
const headers = buildVercelRelayHeaders(
"https://api.openai.com/v1/chat/completions",
"cf-tok"
);
assert.deepEqual(headers, {
"x-relay-target": "https://api.openai.com",
"x-relay-path": "/v1/chat/completions",
"x-relay-auth": "cf-tok",
});
});

View File

@@ -0,0 +1,100 @@
import test from "node:test";
import assert from "node:assert/strict";
// Port of decolua/9router PR #1437 — Deno Deploy relays.
// OmniRoute already supports Vercel-typed relays (#2743 test). Deno Deploy is a
// second cloud edge runtime that ships the same x-relay-{target,path,auth}
// envelope: from the dispatch path's perspective the two are interchangeable.
//
// This test pins three contracts the port must respect:
// 1. The deny-list of "this is a relay, not a TCP proxy" types is shared
// between vercel and deno via a single RELAY_TYPES set in proxyDispatcher
// (no copy-pasted `type === "vercel"` branches that would diverge).
// 2. proxyConfigToUrl returns the relay URL verbatim for type:"deno" (callers
// use buildRelayHeaders() + fetch directly, just like vercel).
// 3. proxyFetch routes a type:"deno" context through the relay branch with
// the same x-relay-* headers and never touches the TCP dispatcher path
// (which would fail loudly against the fake hostname).
// --- Install the relay sink BEFORE importing the module under test. ---
type FetchCall = { input: unknown; init: RequestInit & { headers?: HeadersInit } };
const relayCalls: FetchCall[] = [];
const realGlobalFetch = globalThis.fetch;
const relaySink = (async (input: unknown, init: RequestInit = {}) => {
relayCalls.push({ input, init });
return Response.json({ via: "deno-relay" });
}) as unknown as typeof globalThis.fetch;
globalThis.fetch = relaySink;
const proxyDispatcher = await import("../../open-sse/utils/proxyDispatcher.ts");
const { proxyConfigToUrl, RELAY_TYPES } = proxyDispatcher;
const proxyFetchMod = await import("../../open-sse/utils/proxyFetch.ts");
const { proxyFetch, runWithProxyContext } = proxyFetchMod;
test.after(() => {
globalThis.fetch = realGlobalFetch;
});
test.beforeEach(() => {
relayCalls.length = 0;
});
test("RELAY_TYPES is the single source of truth for relay-typed proxies", () => {
// Both edge-relay types route through the x-relay-* envelope, not a TCP
// dispatcher. Centralising the set prevents the upstream PR-1437 mistake of
// adding `|| type === "deno"` at every callsite (one is bound to be missed).
assert.ok(RELAY_TYPES instanceof Set, "RELAY_TYPES must be a Set");
assert.ok(RELAY_TYPES.has("vercel"), "vercel is a relay type");
assert.ok(RELAY_TYPES.has("deno"), "deno is a relay type");
assert.ok(!RELAY_TYPES.has("http"), "http is NOT a relay type");
assert.ok(!RELAY_TYPES.has("socks5"), "socks5 is NOT a relay type");
});
test("proxyConfigToUrl returns the relay URL verbatim for type:'deno'", () => {
const url = proxyConfigToUrl({
type: "deno",
host: "my-relay.example-org.deno.net",
});
assert.equal(url, "https://my-relay.example-org.deno.net");
});
test("proxyFetch routes a deno-type context through the relay endpoint with relay headers", async () => {
const DENO_CTX = {
type: "deno" as const,
host: "my-relay.example-org.deno.net",
relayAuth: "deno-relay-secret",
};
const response = await runWithProxyContext(DENO_CTX, () =>
proxyFetch("https://api.anthropic.com/v1/messages?x=1", {
method: "POST",
headers: { "x-existing": "keep-me" },
})
);
assert.deepEqual(await response.json(), { via: "deno-relay" });
assert.equal(relayCalls.length, 1, "exactly one relay dispatch");
const call = relayCalls[0];
assert.equal(call.input, "https://my-relay.example-org.deno.net");
const sentHeaders = new Headers(call.init.headers);
assert.equal(sentHeaders.get("x-relay-target"), "https://api.anthropic.com");
assert.equal(sentHeaders.get("x-relay-path"), "/v1/messages?x=1");
assert.equal(sentHeaders.get("x-relay-auth"), "deno-relay-secret");
assert.equal(sentHeaders.get("x-existing"), "keep-me");
assert.equal(call.init.method, "POST");
assert.equal((call.init as { duplex?: string }).duplex, "half");
});
test("proxyFetch fails closed when a deno context is missing relayAuth", async () => {
await assert.rejects(
runWithProxyContext({ type: "deno", host: "relay.example-org.deno.net" }, () =>
proxyFetch("https://api.anthropic.com/v1/messages", { method: "POST" })
),
/relay configuration error: missing relayAuth/i
);
assert.equal(relayCalls.length, 0, "no relay dispatch when relayAuth is missing");
});

View File

@@ -0,0 +1,140 @@
import "../../open-sse/utils/setupPolyfill.ts";
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-pool-sync-4878-"));
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
process.env.DATA_DIR = TEST_DATA_DIR;
delete process.env.OMNIROUTE_API_KEY;
const core = await import("../../src/lib/db/core.ts");
const freeProxiesDb = await import("../../src/lib/db/freeProxies.ts");
const addToPoolRoute = await import(
"../../src/app/api/settings/free-proxies/[id]/add-to-pool/route.ts"
);
const rateLimiter = await import("../../src/shared/utils/rateLimiter.ts");
function reset() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
function makeReq(): Request {
return new Request("http://localhost/test", { method: "POST" });
}
test.beforeEach(() => {
reset();
addToPoolRoute._resetConnectivityTesterForTests();
});
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
if (ORIGINAL_DATA_DIR === undefined) {
delete process.env.DATA_DIR;
} else {
process.env.DATA_DIR = ORIGINAL_DATA_DIR;
}
});
// ── SUB-FIX 1: non-2xx status when the add fails ─────────────────────────────
test("#4878 add-to-pool returns a non-2xx status when connectivity test fails", async () => {
const { id } = await freeProxiesDb.upsertFreeProxy({
source: "1proxy",
host: "10.9.0.1",
port: 8080,
type: "http",
countryCode: null,
qualityScore: null,
latencyMs: null,
anonymity: null,
lastValidated: null,
});
addToPoolRoute._setConnectivityTesterForTests(async () => ({ success: false, latencyMs: 7 }));
const res = await addToPoolRoute.POST(makeReq(), { params: Promise.resolve({ id }) });
assert.ok(
!res.ok && res.status >= 400,
`expected a non-2xx status on failure, got ${res.status}`
);
const body = await res.json();
assert.equal(body.success, false);
assert.ok(body.error);
// Proxy must NOT have been added to the pool.
const fp = await freeProxiesDb.getFreeProxyById(id);
assert.ok(!fp?.inPool);
});
test("#4878 add-to-pool still returns 2xx + success:true on the happy path", async () => {
const { id } = await freeProxiesDb.upsertFreeProxy({
source: "1proxy",
host: "10.9.0.2",
port: 8080,
type: "http",
countryCode: null,
qualityScore: null,
latencyMs: null,
anonymity: null,
lastValidated: null,
});
addToPoolRoute._setConnectivityTesterForTests(async () => ({
success: true,
latencyMs: 5,
publicIp: "1.2.3.4",
}));
const res = await addToPoolRoute.POST(makeReq(), { params: Promise.resolve({ id }) });
assert.ok(res.ok, `expected a 2xx status on success, got ${res.status}`);
const body = await res.json();
assert.equal(body.success, true);
});
// ── SUB-FIX 2: sync timestamp is persisted and surfaced in stats ─────────────
test("#4878 recordFreeProxySync persists a lastSyncAt surfaced by getFreeProxyStats", async () => {
// No proxies upserted at all → MAX(last_validated) is NULL. The sync
// timestamp must still be reported once a sync has run.
const before = await freeProxiesDb.getFreeProxyStats();
assert.equal(before.lastSyncAt, null);
const ts = await freeProxiesDb.recordFreeProxySync();
assert.ok(typeof ts === "string" && ts.length > 0);
const after = await freeProxiesDb.getFreeProxyStats();
assert.equal(after.lastSyncAt, ts);
});
test("#4878 recordFreeProxySync advances lastSyncAt on a subsequent sync", async () => {
const first = await freeProxiesDb.recordFreeProxySync("2020-01-01T00:00:00.000Z");
assert.equal(first, "2020-01-01T00:00:00.000Z");
const second = await freeProxiesDb.recordFreeProxySync("2030-06-25T12:00:00.000Z");
assert.equal(second, "2030-06-25T12:00:00.000Z");
const stats = await freeProxiesDb.getFreeProxyStats();
assert.equal(stats.lastSyncAt, "2030-06-25T12:00:00.000Z");
});
// ── SUB-FIX 3: Redis error-log throttle is state-change-gated ─────────────────
test("#4878 shouldLogRedisError only logs once per error-state change", () => {
const tracker = rateLimiter._createRedisLogThrottleForTests();
// First error in a run → log.
assert.equal(tracker.shouldLog("ECONNREFUSED"), true);
// Same error repeated (retry flood) → suppressed.
assert.equal(tracker.shouldLog("ECONNREFUSED"), false);
assert.equal(tracker.shouldLog("ECONNREFUSED"), false);
// A different error message → state changed → log once more.
assert.equal(tracker.shouldLog("ETIMEDOUT"), true);
assert.equal(tracker.shouldLog("ETIMEDOUT"), false);
});

Some files were not shown because too many files have changed in this diff Show More