mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 17:52:31 +03:00
Compare commits
22 Commits
fix/agentr
...
fix/deepse
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6e5d47e97 | ||
|
|
d4d2cd28c6 | ||
|
|
7589c9f71c | ||
|
|
9e3126828e | ||
|
|
5e344a3a99 | ||
|
|
52603fcffb | ||
|
|
9fcefcce9f | ||
|
|
dd44abf28a | ||
|
|
ed122b2caf | ||
|
|
7d5e8235da | ||
|
|
0334695bff | ||
|
|
3022df548e | ||
|
|
ef3f554665 | ||
|
|
6b0e11e378 | ||
|
|
a549db7dee | ||
|
|
f4e93f339d | ||
|
|
2c966c28af | ||
|
|
8ca40e7971 | ||
|
|
a61020153c | ||
|
|
ce764bc6f3 | ||
|
|
4da2dbe019 | ||
|
|
e9c0d561da |
18
.cbmignore
18
.cbmignore
@@ -119,11 +119,10 @@ omnirouteSite/
|
||||
# 4. Diretorios de dados / runtime locais (storage, env, secrets, scratch)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
data/
|
||||
src/lib/env/
|
||||
src/app/api/agent-skills/coverage/
|
||||
src/app/api/cloud/
|
||||
src/app/api/sync/cloud/
|
||||
src/app/api/system/env/
|
||||
# NOTA: src/lib/env/, src/app/api/{cloud,sync/cloud,system/env,agent-skills/coverage}/
|
||||
# foram removidos daqui (2026-08-05). Os nomes sugerem dados/segredos locais, mas os
|
||||
# 8 arquivos sao route handlers e modulos rastreados no git — escondia-los do grafo
|
||||
# criava pontos cegos em buscas e em analise de impacto.
|
||||
tests/golden-set/data/
|
||||
|
||||
# Logs e saida de teste
|
||||
@@ -142,6 +141,10 @@ obsidian-plugin/node_modules/
|
||||
# 6. Diretorios de documentacao interna / workflow
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
docs/superpowers/
|
||||
# Docs traduzidas: 1.215 arquivos / 94 MB (inclui 20+ copias do CHANGELOG).
|
||||
# Sao traducoes do tree em ingles, ja indexado — no grafo so geram ruido em
|
||||
# search_code e consomem o auto_index_limit.
|
||||
docs/i18n/
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 7. Arquivos especificos (nao diretorios inteiros)
|
||||
@@ -188,8 +191,9 @@ audit-report.json
|
||||
scripts/i18n/_audit.json
|
||||
scripts/i18n/_pending-keys.json
|
||||
|
||||
# Cli binario local (scratch)
|
||||
bin/omniroute.mjs
|
||||
# NOTA: bin/omniroute.mjs foi removido daqui (2026-08-05). Estava marcado como
|
||||
# "scratch", mas e o entrypoint real do CLI publicado (package.json -> bin.omniroute)
|
||||
# e consta em PACK_ARTIFACT_REQUIRED_PATHS. Precisa estar no grafo.
|
||||
|
||||
# Deploy / docker backups
|
||||
deploy.sh
|
||||
|
||||
@@ -7,7 +7,13 @@
|
||||
**/.vscode
|
||||
|
||||
# Dependencies and build output
|
||||
# `node_modules` alone matches the ROOT only — Docker's matcher does not cross
|
||||
# `/` like .gitignore does. Without the `**/` form, nested installs ship in the
|
||||
# build context (e.g. @omniroute/opencode-provider/node_modules, ~79 MB of
|
||||
# devDependencies). Both forms are kept: the bare one is the documented root
|
||||
# rule, the `**/` one covers every nested package.
|
||||
node_modules
|
||||
**/node_modules
|
||||
.next
|
||||
.build
|
||||
out
|
||||
@@ -37,6 +43,17 @@ tests
|
||||
test-results
|
||||
playwright-report
|
||||
blob-report
|
||||
output
|
||||
.playwright-cli
|
||||
.playwright-mcp
|
||||
.stryker-tmp
|
||||
reports/mutation
|
||||
|
||||
# Local caches and quality-gate artifacts (all gitignored). `_*` does not match
|
||||
# dot-prefixed names, so these need explicit entries.
|
||||
.artifacts
|
||||
.eslintcache
|
||||
.eslintcache-complexity
|
||||
|
||||
# Documentation
|
||||
# Issue #2348: The Dashboard Docs viewer reads markdown from `/app/docs` at
|
||||
@@ -49,6 +66,10 @@ blob-report
|
||||
# (English) sources at runtime, so translations are not required in the
|
||||
# container image.
|
||||
docs/i18n/**
|
||||
# Internal planning artifacts (gitignored). `*.md` above only matches the root,
|
||||
# so without this rule these land in /app/docs and become readable through the
|
||||
# dashboard's Docs viewer at runtime.
|
||||
docs/superpowers/**
|
||||
docs/diagrams/**/*.png
|
||||
docs/diagrams/**/*.jpg
|
||||
docs/diagrams/**/*.jpeg
|
||||
|
||||
13
.github/workflows/quality.yml
vendored
13
.github/workflows/quality.yml
vendored
@@ -155,7 +155,18 @@ jobs:
|
||||
- run: npm run check:fetch-targets
|
||||
# docs-all / openapi-routes / docs-symbols live in docs-gates (path-filtered).
|
||||
- run: npm run check:deps
|
||||
- run: npm run check:file-size
|
||||
# #8522: --base-ref mode for PR events — compare against max(frozen, base) so
|
||||
# inherited drift (base already over frozen cap) doesn't red an innocent PR.
|
||||
# workflow_dispatch (no PR base) falls back to absolute comparison.
|
||||
- name: File-size ratchet (base-relative on PR)
|
||||
env:
|
||||
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
run: |
|
||||
if [ -n "$PR_BASE_SHA" ]; then
|
||||
npm run check:file-size -- --base-ref "$PR_BASE_SHA"
|
||||
else
|
||||
npm run check:file-size
|
||||
fi
|
||||
- run: npm run check:error-helper
|
||||
- run: npm run check:migration-numbering
|
||||
- run: npm run check:public-creds
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -235,7 +235,10 @@ omniroute.md
|
||||
|
||||
# mise configuration
|
||||
mise.toml
|
||||
_artifacts/ # release-green artifacts
|
||||
# release-green artifacts (.gitignore has no inline comments — a trailing
|
||||
# `# ...` becomes part of the pattern, so it must sit on its own line).
|
||||
# Already covered by /_*/ above; kept explicit for discoverability.
|
||||
_artifacts/
|
||||
.claude-flow/
|
||||
|
||||
# ESLint file cache (npm run lint --cache / complexity ratchets)
|
||||
@@ -253,3 +256,7 @@ tests/homolog/ui/.auth/
|
||||
homolog-report/
|
||||
docker-compose.yml.bak
|
||||
.playwright-cli/
|
||||
# Playwright screenshot/log output. Today every artifact happens to land inside
|
||||
# output/**/.playwright-cli/ (covered above), but anything written directly to
|
||||
# output/ would otherwise show up as untracked.
|
||||
/output/
|
||||
|
||||
15
.npmignore
15
.npmignore
@@ -4,11 +4,14 @@ data/
|
||||
**/db.json
|
||||
|
||||
# VS Code extension test runtime (large binary, not needed in npm package)
|
||||
app/vscode-extension/
|
||||
**/data/
|
||||
**/db.json
|
||||
|
||||
# Source code (pre-built app/ is published instead)
|
||||
# Source code (pre-built dist/ is published instead)
|
||||
#
|
||||
# NOTA (2026-08-05): as entradas `app/*` foram removidas — o diretorio `app/`
|
||||
# foi renomeado para `dist/` na Layer 1 e nao existe mais. Elas sugeriam um
|
||||
# layout que ja nao e o do projeto.
|
||||
#
|
||||
# NOTE (#3578 / #3821-review): package.json "files" is the source of truth for what
|
||||
# ships. It now allowlists the backend source closure the MCP server needs at runtime
|
||||
@@ -49,8 +52,6 @@ scripts/
|
||||
.vscode/
|
||||
.agents/
|
||||
.env*
|
||||
app/.env
|
||||
app/.env*
|
||||
eslint.config.mjs
|
||||
prettier.config.mjs
|
||||
postcss.config.mjs
|
||||
@@ -82,8 +83,6 @@ bun.lock
|
||||
*.deb
|
||||
*.rpm
|
||||
electron/
|
||||
app/electron/
|
||||
app/vscode-extension/
|
||||
|
||||
# Subprojects
|
||||
clipr/
|
||||
@@ -93,10 +92,6 @@ vscode-extension/
|
||||
|
||||
# Root-level underscore-prefixed directories (private/draft — never publish)
|
||||
/_*/
|
||||
app/_*/
|
||||
app/coverage/
|
||||
app/logs/
|
||||
app/tests/
|
||||
|
||||
# Consistent with .gitignore and .dockerignore
|
||||
.DS_Store
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
# Long reference tables are manually aligned; formatting the whole file causes noisy diffs.
|
||||
docs/reference/ENVIRONMENT.md
|
||||
|
||||
# Generated by `npm run gen:provider-reference`; the generator aligns the tables and
|
||||
# is their formatter of record. Without this, lint-staged reformats the file whenever
|
||||
# it is staged and the next generator run reverts it — a diff ping-pong.
|
||||
docs/reference/PROVIDER_REFERENCE.md
|
||||
|
||||
# Dense auto-generated free-tier budget rows (one object per line) — prettier multi-line expand blows past file-size cap 800.
|
||||
open-sse/config/freeModelCatalog.data.ts
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
- **docs:** add management authentication terminology guide ([#7786](https://github.com/diegosouzapw/OmniRoute/issues/7786))
|
||||
1
changelog.d/features/9485-deepseek-thinking-efforts.md
Normal file
1
changelog.d/features/9485-deepseek-thinking-efforts.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(providers):** add native DeepSeek V4 Flash and Pro thinking-effort aliases for their documented per-model tiers ([#9485](https://github.com/diegosouzapw/OmniRoute/pull/9485)).
|
||||
1
changelog.d/fixes/8522-fix.plan.md
Normal file
1
changelog.d/fixes/8522-fix.plan.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(quality): add base-relative file-size check so inherited drift does not red innocent PRs (#8522)
|
||||
1
changelog.d/fixes/8956-fix.plan.md
Normal file
1
changelog.d/fixes/8956-fix.plan.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(auto-update): skip synthetic Next.js standalone package.json without `name` field in resolveProjectRoot (#8956)
|
||||
1
changelog.d/fixes/9033-fix.plan.md
Normal file
1
changelog.d/fixes/9033-fix.plan.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(auth): IP blacklist now blocks on direct connections via trusted peer stamp and re-reads config without restart (#9033)
|
||||
@@ -0,0 +1 @@
|
||||
- **chore(tests):** cleared two base-reds sitting on `release/v3.8.50` itself, both of which turned every open PR red the moment it merged the release. `tests/snapshots/provider/translate-path.json` was stale: #9064 added the `code-execution-2025-08-25` and `skills-2025-10-02` beta flags to the Anthropic header without regenerating the golden, so `provider-translate-path-golden` failed on the bare release tip (2 pass / 1 fail with zero PRs boarded). And `tests/unit/v1-models-auth-leak-9320.test.ts:83` shipped a `(k: any)` in a file new enough that `config/quality/eslint-suppressions.json` does not cover it — with `@typescript-eslint/no-explicit-any` set to `error` under `tests/`, that single cast failed the `No new ESLint warnings` gate repo-wide. Same class in `tests/unit/issue-9407-gemini-web-validation-false-positive.test.ts:50` (`(executor as any).testConnection`), which entered at `f1ea77fd04` — `testConnection` is a declared public method on `GeminiWebExecutor`, so that cast was redundant too. Regenerating the snapshot and dropping both casts restores the gates.
|
||||
@@ -0,0 +1 @@
|
||||
- fix(quality): tighten eslintWarnings baseline 5000->0 to match the gate's suppressions-applied measurement (unblocks require-tighten on every code PR)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"_rebaseline_2026_08_05_9485_deepseek_v4_effort_aliases": "PR #9485 (excessivechaos, fix/deepseek-thinking-efforts) own growth: src/app/api/v1/models/catalog.ts 1549->1555 (+6, check-file-size.mjs counts via split(\"\\n\").length) at the existing static-model emission chokepoint in buildUnifiedModelsResponseCore. Adds a `hasDeclaredEffortTiers` gate (skip the synced-coverage suppression when the static model declares its own supportedThinkingEfforts list) and calls the existing getThinkingCapabilityFields(...) helper — now with a new skipCanonicalEffortFallback flag — to spread thinking/effort_tiers fields onto both the alias and provider-prefixed model entries. DeepSeek V4's thinking-effort tiers (minimal/low/medium/high) were being silently suppressed by the synced-model-coverage guard added for #7786, and any effort variant not explicitly declared fell back to synthesizing unresolvable `<prefix>/<model>-{tier}` ids for every static reasoning model. All the actual field-computation logic (hasDeclaredTiers, effort_tiers selection, extendCodexGpt56EffortValues fallback) lives in the non-frozen leaf src/app/api/v1/models/catalogHelpers.ts::getThinkingCapabilityFields; catalog.ts only adds the 5-line gate + destructure + spread at its single static-model push site. Cohesive wiring at the existing chokepoint, mirroring prior thinking/vision-field rebaselines (#4264 supportsVision, #6218-era supportsThinking) at the same site; not extractable further without hiding the per-model field-merge boundary. Covered by tests/unit/deepseek-thinking-efforts.test.ts.",
|
||||
"_rebaseline_2026_07_24_8470_hyperagent_sticky_thread": "PR #8470 (artickc, fix/hyperagent-tool-loop-thread-sticky) own growth: open-sse/executors/hyperagent.ts 936->1025 (wc -l; check-file-size.mjs counts via split(\"\\n\").length so the gate sees 937->1026, +89, crosses the 1000 cap). Fixes a real bug where a reverse-conversion proxy (text-Intent/JSON to Claude Code native tool_calls) rewrites assistant messages between agentic tool-loop turns, breaking HyperAgent's conversation-prefix fingerprint and cold-starting the thread mid tool-loop. Adds Anthropic tool_use/tool_result flattening to extractMessageText() plus a new rootUserFingerprint()/root-key lookup tier in resolveHyperAgentThreadBinding()/storeHyperAgentThreadAfterTurn() so the thread stays sticky across the tool loop. Cohesive additions inside the existing single-file executor; not extractable without splitting the executor mid-request-flow. Covered by tests/unit/executor-hyperagent.test.ts (19/19, +5 new cases for tool_result/tool_use flattening + root-key stickiness). Pre-merge review flagged a cross-conversation root-key collision risk (tracked in the PR's own mandatory pre-merge checklist, not yet addressed) — unrelated to this file-size ratchet, tracked separately by /fix-prs.",
|
||||
"_rebaseline_2026_07_25_8494_capability_filter_fail_closed": "PR #8494 (fix/capability-filters-fail-closed, #8488) own growth: open-sse/services/combo.ts 3640->3693 (+53) adds a fail-closed guard after filterTargetsByRequestCompatibility() — when every eligible target is excluded by request-capability filtering (vision/tools/etc) instead of quota/health, the combo now returns an explicit `capability_mismatch` 400 (describeCapabilityFilterExhaustion, imported from combo/comboStructure.ts) rather than silently falling through to a generic no-targets error, plus a `compatFilterFailOpen` escape hatch (combo config OR settings) mirrored at both the main/auto and round-robin call sites for symmetry. combo/comboStructure.ts (previously under cap, un-frozen) grows 794->918 (+124) — new home for describeCapabilityFilterExhaustion + providerSupportsEmulatedToolCalling (#5240 emulated tool-calling exemption so fail-closed does not regress prompt-emulation-only combos like all-chatgpt-web). Irreducible orchestration wiring at the existing filter chokepoint (same precedent as #7301's universal-cooldown-retry generalization). Companion test tests/unit/combo-routing-engine.test.ts 3409->3449 (+40, fail-closed/fail-open coverage across both call sites) also rebaselined. Covered by tests/unit/8488-capability-filter-fail-closed.test.ts (new) + 95/95 passing across both files. Structural shrink of combo.ts tracked in #3501.",
|
||||
"_rebaseline_2026_07_25_8499_ts7_result_union_predicates": "PR #8499 (backryun, chore/ts7-types-executor-scattered) own growth: muse-spark-web.ts 1396->1405 (+9, irreducible). Under this workspace's `strictNullChecks: false`, the boolean-literal discriminant on `GraphqlResult` (`{ ok: true } | { ok: false; error: string }`) narrows the positive `.ok===true` branch but leaves `!result.ok` at the full union under TS7, making `.error` unreachable to the checker at the two call sites (warmup, mode-switch). Fixed by adding a single `isGraphqlFailure()` type-predicate helper (doc comment + 3-line body) reused at both call sites instead of duplicating the predicate inline — not extractable to a shared module without splitting a single-file executor's local narrowing helper out of its own file. Covered by the existing muse-spark-web executor test suite (no behavior change, pure narrowing fix).",
|
||||
@@ -388,7 +389,7 @@
|
||||
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2148,
|
||||
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1109,
|
||||
"src/app/api/providers/[id]/models/route.ts": 2250,
|
||||
"src/app/api/v1/models/catalog.ts": 1549,
|
||||
"src/app/api/v1/models/catalog.ts": 1555,
|
||||
"src/lib/tokenHealthCheck.ts": 1021,
|
||||
"src/lib/db/apiKeys.ts": 1529,
|
||||
"src/lib/db/core.ts": 1637,
|
||||
|
||||
@@ -3,23 +3,8 @@
|
||||
"metrics": {
|
||||
"eslintWarnings": {
|
||||
"value": 0,
|
||||
"_rebaseline_2026_07_03_v3844_residual_release_green": "4270->4279 (+9). v3.8.44 residual drift on release tip 716041223 (moving target: eslint 4270->4279 as the branch advanced past the prior rebaseline). Inherited from parallel-session merges (Quality Ratchet not on PR->release fast-gates).",
|
||||
"_rebaseline_2026_07_03_v3844_ipfilter_release_green": "4256->4270 (+14). v3.8.44 cycle drift measured on release tip 32e4c906e during the #6131/#5975 release-green rebaseline. Inherited from the merge burst (Quality Ratchet does not run on PR->release fast-gates). route-edge-coverage +7 is my #5975 test comment; the rest is parallel-session drift. Tighten via --update next cycle.",
|
||||
"_rebaseline_2026_07_03_v3844_review_prs_fix_batch": "4199->4256 (+57). Inherited v3.8.44 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings accrue unmeasured across the cycle). 4256 = measured by `node scripts/quality/collect-metrics.mjs` on the release tip 72ee80649 during the /review-prs fix-batch round. The round's own merges (#5958 SSE-accept, #5988 deepseek-web, #6013/#5974 retry-after-json, #5975 embeddings-proxy, #5973 non-json-guard) plus the parallel-session merge burst into release/v3.8.44 account for the delta; all `any`-warn-allowed in open-sse/ + tests/. Cyclomatic is already green (2012 < baseline 2015) and needs no bump. Tighten via --require-tighten next cycle.",
|
||||
"_rebaseline_2026_07_02_v3843_release_close": "4158->4199 (+41). v3.8.43 release-close drift measured by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across the ~120 commits merged after the mid-cycle 4158 rebaseline — the compression T02/T05/T06/T07/T08/T10 engine families, memory typed decay, provider adds Ollama/SenseNova, ~55 SSE/translator/kiro/oauth/dashboard fixes, and the god-file decomposition wave). Trust-but-verify: measured 4199 via `npm run lint` on the release-finalize working tree INCLUDING my changes (CHANGELOG/i18n/README docs + kiro pricing data entry + the 3 base-red CODE fixes: opencode fabrication removal, resolveEffectiveKey type-widen, openai-to-claude claudeFinishEmitted flag + 4 test-alignment files + golden snapshot regen) — the code fixes NET-REMOVE lines and add no `any`/unused, and lint reported 4199 both before and after them, so all +41 is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.",
|
||||
"direction": "down",
|
||||
"_rebaseline_2026_07_01_v3843_release": "4121->4158 (+37). v3.8.43 cycle drift surfaced by the release-green pre-flight; the Quality Ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across this cycle. 4158 = the value measured by the CI Quality Ratchet on the release tip fce85136c (release PR #5609). Trust-but-verify: the fix/release-v3843-ci-reds branch touches only test files (rtk-mcp-tools de-flake, compression-studio e2e anchor, oauth-error-linkify hardening test) + src/shared/utils/linkify.ts (eslint-clean, 0 warnings) + stryker.conf.json + this baseline -> 0 new warnings, so all +37 is inherited cycle drift (any warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.",
|
||||
"_rebaseline_2026_06_30_v3842_release": "4116->4121 (+5). v3.8.42 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across this cycle's 90 commits — chatgpt-web PoW sha3-512 BoringSSL fix #5540, provider baseUrl/i18n umbrella #5511, proxy union proxyUrlMap+acct.proxy #5521, dead-code + duplication waves #5468-#5495, tls-options packaging #5503, release-freeze + .npmrc fetch-retries #5506, dast-smoke spawn-prefix client-safe extraction #5546, plus ~30 SSE/translator/combo/dashboard fixes). Trust-but-verify: measured 4121 via `npm run check:release-green` on the working tree INCLUDING my reconciliation (CHANGELOG/i18n/golden snapshot + file-size baseline) — those touch only config JSON + a provider snapshot (eslint-ignored) and contribute 0 warnings; all +5 is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.",
|
||||
"_rebaseline_2026_06_29_v3841_release": "4103->4116 (+13). v3.8.41 cycle drift surfaced by the release-green collect (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across this cycle's 52 commits — relay backend #5315, gemini catalog #5337, services dashboard #5299, empty-Claude-messages guard #5342, thinking-budget/redacted-replay + marker opt-out #5312/#5352/#5367, opencode proxy-pool + observability #5217/#5370/#5351, cors + HTTPS-serve #5242/#5360/#5361, grok cf_clearance #5350/#5358, oauth/chatgpt-web/routing/cli/dashboard/rerank #5326/#5240/#5239/#5238/#5264/#5332, partially offset by the dead-code sweep #5321-#5371). Trust-but-verify: measured 4116 via `npm run quality:collect` on the working tree INCLUDING my reconciliation (CHANGELOG/i18n/README/env docs + baselines) AND the lint-fix in useServiceLogs.ts — that fix REMOVES a setState-in-effect ERROR (eslintErrors stays 0) and adds an `open` listener with no `any`/unused, contributing 0 warnings; all +13 is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.",
|
||||
"_rebaseline_2026_06_29_v3840_release": "4090->4103 (+13). v3.8.40 cycle drift surfaced by the release-green pre-flight + the release PR Quality Ratchet (the ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across this cycle's ~57 commits — compression roadmap relevance/hard-budget/memoization/transparency/saliency/splitter/tool_search/RTK/QuantumLock #5289/#5288/#5286/#5284/#5285/#5283/#5269/#5268/#5260, ~20 SSE/translator/combo fixes #5248/#5250/#5254/#5261/#5255/#5273/#5258, M365 Copilot provider #5302, public-origin centralization #5278). Trust-but-verify: measured 4103 locally via `npm run quality:collect` on the release tip INCLUDING my reconciliation commits (CHANGELOG + main merge + the 2 regression test fixes 165c823f5) — the test fixes add 0 `any`/warnings (health-autopilot added a NextRequest import + asserts; chat-pipeline changed one Accept string + a comment), so all +13 is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.",
|
||||
"_rebaseline_2026_06_28_v3839_release": "4002->4090 (+88). v3.8.39 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across this cycle's 40 commits — antigravity remote-login + quota-family #5203/#5180/#5193, compression CCR-retrieve + TOON encoder #5187/#5163, ~20 SSE/translator/responses fixes #5156/#5154/#5197/#5204/#5158/#5123/#5166, proxy/health hardening #5202/#5208/#5209/#5201 from @KooshaPari, combo quota-share/context-relay E2E tests #5179/#5168/#5195). Trust-but-verify: this release-finalize working tree touches ONLY CHANGELOG.md, docs/i18n/*/CHANGELOG.md mirrors, README.md and these baselines — 0 production-code change, so all +88 is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.",
|
||||
"_rebaseline_2026_06_27_v3838_release": "3987->4002 (+15). v3.8.38 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across this cycle's ~78 commits — provider adds Factory/Grok-Build/ZenMux-Free/Alibaba-video, ~30 SSE/translator/diagnostics fixes, compression fidelity-gate + playground #5080/#5143, Fusion editor #5074, salvage batches #5138/#5141). Trust-but-verify: this release-finalize working tree touches ONLY CHANGELOG.md, docs/i18n/*/CHANGELOG.md mirrors, README.md and these baselines — 0 production-code change, so all +15 is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.",
|
||||
"_rebaseline_2026_06_25_v3836_release": "v3.8.36 cycle drift surfaced by the post-merge fix PR #5029 (the Quality Ratchet was SKIPPED on the release PR #4854 itself, and does NOT run on the PR→release fast-gates, so warnings accrued unmeasured across this cycle's 137 commits — Quota-Share Fase 2/3 features, god-file decomposition #3501/#4811-#4956, 14 external contributor PRs). 3912→3970 (+58), the exact value measured by the CI Quality Ratchet on #5029. Trust-but-verify: this fix PR touches ONLY scripts/build/pack-artifact-policy.ts (a string-literal allowlist array, scripts/ is eslint-light) and tests/integration/resilience-http-e2e.test.ts (2 string keys, no `any`) — 0 new warnings, so all +58 is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Same precedent as _rebaseline_2026_06_23_v3835_release. Tighten via --require-tighten next cycle.",
|
||||
"_rebaseline_2026_06_23_v3835_release": "v3.8.35 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR→release fast-gates, so warnings accrued across this cycle's parallel-session merges — Compression Phase 4 #4694/#4707/#4716/#4720, chatCore #3501 leaf extractions, contributor PRs #4726/#4753/#4774/#4781/#4783/#4793, etc.). 3907→3912 (+5). Verified my release-finalize working tree touches ONLY docs/*.md (THREAT_MODEL), CHANGELOG.md, baselines, and 1 string line in scripts/check/check-fabricated-docs.mjs — 0 production-code change, so all +5 is inherited contributor drift. No coverage/openapi/i18n regressions.",
|
||||
"_rebaseline_2026_06_22_v3834_release": "v3.8.34 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR→release fast-gates, so warnings accrued across this cycle's parallel-session merges — #4583-4586/#4588-4593/#4606-4621/#4644/#4647/#4696/etc.). 3900→3907 (+7). Verified my release-finalize working tree touches ONLY CHANGELOG.md (git status: 0 code changes), so all +7 is inherited contributor drift. No coverage/openapi/i18n regressions.",
|
||||
"_rebaseline_2026_06_22_v3833_release": "Cumulative cycle drift surfaced by the release PR full CI. 3867→3900 (+33).",
|
||||
"_rebaseline_2026_06_26_v3837_release": "3970->3987. v3.8.37 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings/complexity accrued unmeasured across this cycle's 76 commits — provider adds DGrid/Pioneer/xAI, headroom proxy lifecycle #4649, ~50 SSE/translator fixes, Engine Combos #5062). Trust-but-verify: this release-finalize working tree touches ONLY CHANGELOG.md, docs/i18n/*/CHANGELOG.md mirrors, and these baselines — 0 production-code change, so all drift is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.",
|
||||
"_rebaseline_2026_07_04_pacote4_no_new_warnings": "4279->0. Pacote 4 do plano mestre testes+CI: a divida pre-existente (4279 warnings + violacoes das 3 regras promovidas a error em src/**) foi CONGELADA em config/quality/eslint-suppressions.json (ESLint bulk suppressions nativo) e passa a ser bloqueada NO PR que a introduziria (job lint-guard no quality.yml + npm run lint + lint-staged, todos suppressions-aware; fork = report-only, Principio Zero). collect-metrics agora mede sob o baseline congelado -> a metrica vira 'divida liquida NOVA' (~0 em regime). O aperto do ESTOQUE congelado acontece via `npx eslint . --prune-suppressions --suppressions-location config/quality/eslint-suppressions.json` na reconciliacao da release. Fim das rebaselines-surpresa de +41/+88 por ciclo."
|
||||
"_rebaseline_2026_08_05_post_prune": "Apertado 5000->0 em 2026-08-05: o gate mede via lint:json COM as suppressions aplicadas (config/quality/eslint-suppressions.json congela a divida da migracao TS7), entao a contagem real do gate e 0. O 5000 anterior foi medido SEM suppressions (4139 brutos) e fazia o require-tighten reprovar todo PR de codigo (delta 5000>slack). Divida TS7 continua rastreada nas suppressions; warning NOVO (fora delas) agora e red imediato, que e a politica."
|
||||
},
|
||||
"eslintErrors": {
|
||||
"value": 0,
|
||||
|
||||
@@ -64,6 +64,18 @@
|
||||
"tests/unit/ui/provider-plan-config.test.tsx": {
|
||||
"replacement": "tests/unit/quota-plans-route-retired.test.ts",
|
||||
"reason": "v3.8.49 #7127: fix(tests) suíte vitest UI de volta ao verde — a rota Plans e o ProviderPlanConfigClient foram APOSENTADOS; o replacement inverte a asserção e guarda a aposentadoria (o arquivo da rota e o ProviderPlanConfigClient não existem mais, costs-quota-plans saiu do sidebarVisibility e da navegação)."
|
||||
},
|
||||
"tests/unit/plugin-sandbox-permissions.test.ts": {
|
||||
"sourceRemoved": [
|
||||
"src/lib/plugins/pluginWorker.ts",
|
||||
"src/lib/plugins/sandbox.ts",
|
||||
"src/lib/plugins/signing.ts"
|
||||
],
|
||||
"reason": "v3.8.50 #9126 (commit 8fac6bcd48): pluginWorker.ts, sandbox.ts e signing.ts foram removidos por completo (\"zero importers confirmed\") — o subsistema de sandbox de plugins com worker-thread nunca foi ligado a nenhum consumidor. O teste era source-scan sobre pluginWorker.ts (ver docstring do arquivo deletado); sem o arquivo-fonte não há mais o que testar. OMNIROUTE_PLUGINS_ALLOW_EXEC também foi removido de .env.example e da doc na mesma release. Sem substituto porque a feature foi extinta, não migrada."
|
||||
},
|
||||
"tests/unit/plugins-sandbox.test.ts": {
|
||||
"sourceRemoved": ["src/lib/plugins/sandbox.ts"],
|
||||
"reason": "v3.8.50 #9126 (commit 8fac6bcd48): sandbox.ts foi removido por completo junto com pluginWorker.ts e signing.ts (\"zero importers confirmed\", subsistema de sandbox de plugins nunca ligado a nenhum consumidor). O teste cobria SandboxLevel/getSandboxLabel exportados por sandbox.ts; sem o arquivo-fonte não há mais símbolo a testar. Mesma causa-raiz de tests/unit/plugin-sandbox-permissions.test.ts nesta entrada."
|
||||
}
|
||||
},
|
||||
"tests/unit/catalog-updates-v3x.test.ts": "v3.8.45 #6248: fix(providers) remove deprecated MiMo V2 entries — os 5 asserts removidos pinavam specs de modelos mimo-v2-* que deixaram de existir no catálogo (54→49). Asserts seguem a remoção dos modelos, não enfraquecimento. Verificado legítimo. Prune após v3.8.45 mergear para main.",
|
||||
@@ -96,5 +108,6 @@
|
||||
"tests/unit/usage-providers.test.ts": "v3.8.49 #7866: o case \"qwen\" saiu de getUsageForProvider (não há mais case \"qwen\" no switch de open-sse/services/usage.ts); o teste cobria esse ramo extinto (net 20→19). Verificado legítimo. Prune após v3.8.49 mergear para main.",
|
||||
"tests/unit/usage-service-hardening.test.ts": "v3.8.49 #7866/#8565/#8013: qwen removido (−3 asserts); o Kimi/Kiro builder-id (uso profileless) passou a ter SUCESSO real em vez de erro de ARN — supportsProfilelessKiroUsage(\"builder-id\") retorna true —, trocando 1 assert de regex de erro por 3 asserts de valor; e os ids de bucket de quota do Antigravity foram atualizados para o catálogo atual. Rodado no HEAD: 23/23 passam. Net 210→209. Verificado legítimo. Prune após v3.8.49 mergear para main.",
|
||||
"tests/unit/virtual-auto-combo.test.ts": "v3.8.49 #7928/#8183: o pooling de contas passou a agrupar conexões web-session do mesmo provider numa entrada lógica com allowedConnectionIds (campo confirmado em open-sse/services/autoCombo/virtualFactory.ts), e o pool no-auth virou uma allowlist fixa (AUTO_COMBO_NOAUTH_ALLOWLIST = opencode, felo-web) — os testes antigos esperavam duplicatas e a inclusão de duckduckgo-web/theoldllm/chipotle, que hoje são corretamente excluídos. Guard dedicado em noauth-autocombo-allowlist.test.ts. Rodado no HEAD: 10/10 passam. Net 39→31. Verificado legítimo. Prune após v3.8.49 mergear para main.",
|
||||
"open-sse/services/__tests__/tierResolver.test.ts": "v3.8.49 #7866: refactor(qwen) remove o provider OAuth legado — o teste \"classifies Qwen as free\" e a entrada de qwen na lista do batch saíram junto com o provider, e os índices do batch desceram de 10 para 9 elementos (net 61→59). Superfície extinta, não enfraquecimento. Verificado legítimo. Prune após v3.8.49 mergear para main."
|
||||
"open-sse/services/__tests__/tierResolver.test.ts": "v3.8.49 #7866: refactor(qwen) remove o provider OAuth legado — o teste \"classifies Qwen as free\" e a entrada de qwen na lista do batch saíram junto com o provider, e os índices do batch desceram de 10 para 9 elementos (net 61→59). Superfície extinta, não enfraquecimento. Verificado legítimo. Prune após v3.8.49 mergear para main.",
|
||||
"tests/unit/plugins-welcome-banner-e2e.test.ts": "v3.8.50 #9126 (commit 8fac6bcd48): o teste único 'BUILTIN_EVENTS has all 14 events' (13 asserts .ok/.equal) foi reestruturado em 3 testes mais específicos — 'contains only emitted/public events' (assert.deepEqual da lista completa), 'does not advertise dead events' (7 asserts .equal(false) para eventos sem emissor real: onModelSelect/onComboResolve/onRateLimit/onQuotaExhaust/onProviderError/onStreamStart/onStreamEnd) e 'lifecycle events remain represented' (4 asserts .ok). Contrato mais forte (agora também nega presença dos eventos mortos), não mais fraco — a contagem líquida cai (73→61) porque o assert.deepEqual único substitui múltiplos assert.ok redundantes com a mesma cobertura. Asserts restruturados, não removidos sem substituição. Verificado legítimo."
|
||||
}
|
||||
|
||||
47
docs/guides/MANAGEMENT-AUTH.md
Normal file
47
docs/guides/MANAGEMENT-AUTH.md
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: "Management Authentication"
|
||||
version: 3.8.50
|
||||
lastUpdated: 2026-08-05
|
||||
---
|
||||
|
||||
# Management Authentication
|
||||
|
||||
OmniRoute uses four distinct credential families for management access. This guide
|
||||
distinguishes them by purpose, scope, and locality.
|
||||
|
||||
| Credential | Scope | Locality | Use Case |
|
||||
|-------------------------|--------------------|---------------|-----------------------------------|
|
||||
| Dashboard JWT session | Full management | Localhost | Web dashboard login |
|
||||
| CLI machine-id token | Full management | Per-machine | `omniroute` CLI commands |
|
||||
| Scoped `oma_` token | Configurable scope | External | Automation / CI / API access |
|
||||
| Manage-scope API key | `manage` scope | External | Management API calls |
|
||||
|
||||
## Dashboard JWT Session
|
||||
|
||||
Generated on dashboard login (`/api/auth/login`). Stored in HTTP-only cookie.
|
||||
Valid for the session duration. Cannot be used from external hosts.
|
||||
|
||||
## CLI Machine-ID Token
|
||||
|
||||
Created by `omniroute auth login` on first use. Stored in `~/.omniroute/auth.json`.
|
||||
Used by the CLI for all management operations. Tied to the machine identity.
|
||||
|
||||
## Scoped `oma_` Access Token
|
||||
|
||||
Created via dashboard or CLI with configurable scopes (e.g., `manage`, `read`).
|
||||
Format: `oma_<random-hex>`. Used for programmatic access from external systems.
|
||||
|
||||
## Manage-Scope API Key
|
||||
|
||||
Standard API key with the `manage` scope enabled. Created in dashboard API Keys page.
|
||||
Used for management API calls from external hosts.
|
||||
|
||||
## Header Examples
|
||||
|
||||
```
|
||||
Authorization: Bearer oma_abc123def456
|
||||
Authorization: Bearer <standard-api-key-with-manage-scope>
|
||||
Cookie: omniroute_session=<jwt-token>
|
||||
```
|
||||
|
||||
See `docs/reference/API_REFERENCE.md` for endpoint-specific auth requirements.
|
||||
@@ -1,3 +1,9 @@
|
||||
---
|
||||
title: "AgentRouter WAF"
|
||||
version: 3.8.50
|
||||
lastUpdated: 2026-08-03
|
||||
---
|
||||
|
||||
# agentrouter.org WAF (Web Application Firewall)
|
||||
|
||||
The `agentrouter` upstream gateway runs a keyword-based content filter on
|
||||
|
||||
@@ -22,8 +22,7 @@ const LOCAL_DB_IMPORT_RESTRICTION = {
|
||||
|
||||
const EXECUTOR_IMPORT_RESTRICTION = {
|
||||
regex: "^(?:@omniroute/)?open-sse/executors(?:/|$)",
|
||||
message:
|
||||
"Executor implementations must stay behind an open-sse handler or service boundary.",
|
||||
message: "Executor implementations must stay behind an open-sse handler or service boundary.",
|
||||
};
|
||||
|
||||
const PROP_TYPES_RESTRICTION = {
|
||||
@@ -165,6 +164,14 @@ const eslintConfig = [
|
||||
// their files move mid-scan, so never lint them from the main checkout.
|
||||
".claude/**",
|
||||
".omnivscodeagent/**",
|
||||
// _tasks/ — planning/handoff/research artifacts (gitignored, external code)
|
||||
"_tasks/**",
|
||||
// .agents/ — skill definitions + their helper scripts (gitignored; the
|
||||
// canonical copy lives here and is symlinked into .claude/).
|
||||
".agents/**",
|
||||
// .source/ — fumadocs codegen output (@ts-nocheck + bundler-only import
|
||||
// query params like `?collection=docs`, which are not valid TS on their own).
|
||||
".source/**",
|
||||
// VS Code extension and its large test fixtures
|
||||
"vscode-extension/**",
|
||||
"_references/**",
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
node_modules/
|
||||
*.log
|
||||
.DS_Store
|
||||
test/
|
||||
*.test.js
|
||||
.env
|
||||
.env.*
|
||||
|
||||
@@ -9,7 +9,17 @@ export const deepseekProvider: RegistryEntry = {
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
models: [
|
||||
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", supportsReasoning: true },
|
||||
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", supportsReasoning: true },
|
||||
{
|
||||
id: "deepseek-v4-pro",
|
||||
name: "DeepSeek V4 Pro",
|
||||
supportsReasoning: true,
|
||||
supportedThinkingEfforts: ["none", "high", "max"],
|
||||
},
|
||||
{
|
||||
id: "deepseek-v4-flash",
|
||||
name: "DeepSeek V4 Flash",
|
||||
supportsReasoning: true,
|
||||
supportedThinkingEfforts: ["none", "low", "high", "max"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -265,17 +265,25 @@ export function sanitizeReasoningEffortForProvider(
|
||||
return stripEffortValue(b, c);
|
||||
}
|
||||
|
||||
// Native DeepSeek (api.deepseek.com) — V4 thinking mode accepts reasoning_effort
|
||||
// ONLY as {high, max} (its own top tier is literally "max"). OmniRoute's internal
|
||||
// scale is low|medium|high|xhigh where xhigh is the top, so map onto DeepSeek's
|
||||
// vocabulary: xhigh → max (top→top), low|medium → high (below the enum floor).
|
||||
// high/max pass through unchanged. Without this, the claude→openai translator's
|
||||
// xhigh (and max-normalized-to-xhigh below) reaches DeepSeek as an unknown value,
|
||||
// silently dropping the client's requested effort. This is the INVERSE of the
|
||||
// OpenRouter-DeepSeek path, whose normalized API expects xhigh, not max (pi#4055).
|
||||
// Native DeepSeek (api.deepseek.com) — V4 thinking mode uses the native
|
||||
// {low, high, max} vocabulary on Flash and {high, max} on Pro. OmniRoute's
|
||||
// internal top tier xhigh maps to DeepSeek's literal max. Pro's unsupported
|
||||
// low/medium values still clamp to high; Flash's documented low tier passes
|
||||
// through. This is the INVERSE of the OpenRouter-DeepSeek path, whose
|
||||
// normalized API expects xhigh, not max (pi#4055). `none` is already the
|
||||
// OpenAI no-thinking carrier and passes through unchanged.
|
||||
if (provider === "deepseek") {
|
||||
// Match the Flash family even when the sanitizer sees a suffixed or prefixed
|
||||
// id — exact-match would silently clamp Flash `low → high` if a future route
|
||||
// forwards the raw catalog id (`deepseek-v4-flash-low`) before resolution
|
||||
// (#9485 review).
|
||||
const isFlash = modelStr.toLowerCase().startsWith("deepseek-v4-flash");
|
||||
const mapped =
|
||||
effortStr === "xhigh" ? "max" : effortStr === "low" || effortStr === "medium" ? "high" : null;
|
||||
effortStr === "xhigh"
|
||||
? "max"
|
||||
: effortStr === "medium" || (effortStr === "low" && !isFlash)
|
||||
? "high"
|
||||
: null;
|
||||
if (mapped && mapped !== effortStr) {
|
||||
log?.info?.(
|
||||
"REASONING_SANITIZE",
|
||||
|
||||
@@ -67,7 +67,7 @@ import {
|
||||
resolveMemoryOwnerId,
|
||||
} from "./chatCore/memoryExtraction.ts";
|
||||
import { CORS_HEADERS } from "../utils/cors.ts";
|
||||
import { checkHeapPressureGuard } from "../utils/heapPressure.ts";
|
||||
import { checkResourcePressureGuard } from "../utils/resourcePressure.ts";
|
||||
import { normalizeHeaders } from "../utils/headers.ts";
|
||||
import { resolveChatCoreRequestFormat } from "./chatCore/requestFormat.ts";
|
||||
import { resolveChatCoreTargetFormat } from "./chatCore/targetFormat.ts";
|
||||
@@ -359,13 +359,6 @@ import {
|
||||
isRpmExhausted,
|
||||
} from "../services/geminiRateLimitTracker.ts";
|
||||
|
||||
// ── Global memory pressure guard ────────────────────────────────────────
|
||||
// Prevents OOM by rejecting new requests when V8 heap exceeds threshold.
|
||||
// Self-healing: no counters to leak, no cleanup needed. The threshold
|
||||
// auto-calibrates to 85% of the actual V8 heap ceiling (see heapPressure.ts) so
|
||||
// it tracks --max-old-space-size across 1GB/2GB/large VPS instead of a fixed
|
||||
// 200MB that sat below the app's own ~260MB baseline and rejected every request.
|
||||
|
||||
import { isSmallEnoughForSemanticCache } from "../utils/estimateSize.ts";
|
||||
|
||||
/**
|
||||
@@ -415,17 +408,16 @@ export async function handleChatCore({
|
||||
createPiiTransform = null,
|
||||
correlationId = null,
|
||||
modelPinned = false,
|
||||
skipResourcePressureGuard = false,
|
||||
}) {
|
||||
let { provider, model, extendedContext } = modelInfo;
|
||||
// ── Memory pressure guard ────────────────────────────────────────────
|
||||
// Reject early if V8 heap is already near the 256MB limit. Prevents
|
||||
// cascading OOM when many large-context requests arrive concurrently.
|
||||
try {
|
||||
const heapUsedMB = process.memoryUsage().heapUsed / (1024 * 1024);
|
||||
const heapGuard = checkHeapPressureGuard(heapUsedMB);
|
||||
if (heapGuard) return heapGuard;
|
||||
} catch {
|
||||
/* memoryUsage() never throws */
|
||||
if (!skipResourcePressureGuard) {
|
||||
try {
|
||||
const pressureGuard = checkResourcePressureGuard();
|
||||
if (pressureGuard) return pressureGuard;
|
||||
} catch {
|
||||
/* fail open */
|
||||
}
|
||||
}
|
||||
|
||||
// Per-request model-routing metadata (first extracted slice of the request-setup phase).
|
||||
|
||||
168
open-sse/services/admission/adaptation.ts
Normal file
168
open-sse/services/admission/adaptation.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import type { AdmissionPressure, AdmissionReleaseOutcome } from "./types.ts";
|
||||
|
||||
export interface AdaptationParams {
|
||||
minLimit: number;
|
||||
maxLimit: number;
|
||||
windowMs: number;
|
||||
shortLatencyAlpha: number;
|
||||
longLatencyAlpha: number;
|
||||
increaseStep: number;
|
||||
decreaseFactor: number;
|
||||
criticalDecreaseFactor: number;
|
||||
highUtilizationThreshold: number;
|
||||
lowUtilizationThreshold: number;
|
||||
latencyGradientThreshold: number;
|
||||
maxIncreasePerWindow: number;
|
||||
}
|
||||
|
||||
export interface AdaptationState {
|
||||
currentLimit: number;
|
||||
shortLatencyEwma: number;
|
||||
longLatencyEwma: number;
|
||||
pressure: AdmissionPressure;
|
||||
/** Sum of admitted cost * time contribution proxies in the open window. */
|
||||
windowActiveCostIntegral: number;
|
||||
windowCompleted: number;
|
||||
windowLatencySamples: number;
|
||||
windowStartMs: number;
|
||||
freezeGrowth: boolean;
|
||||
/**
|
||||
* When true, critical multiplicative decrease already applied for this window
|
||||
* (e.g. via immediate observePressure). Window close must not re-apply it.
|
||||
*/
|
||||
criticalDecreaseConsumed: boolean;
|
||||
utilization: number;
|
||||
}
|
||||
|
||||
export function clampLimit(value: number, minLimit: number, maxLimit: number): number {
|
||||
if (!Number.isFinite(value)) return minLimit;
|
||||
return Math.min(maxLimit, Math.max(minLimit, Math.floor(value)));
|
||||
}
|
||||
|
||||
export function createAdaptationState(
|
||||
initialLimit: number,
|
||||
minLimit: number,
|
||||
maxLimit: number,
|
||||
nowMs: number
|
||||
): AdaptationState {
|
||||
return {
|
||||
currentLimit: clampLimit(initialLimit, minLimit, maxLimit),
|
||||
shortLatencyEwma: 0,
|
||||
longLatencyEwma: 0,
|
||||
pressure: "normal",
|
||||
windowActiveCostIntegral: 0,
|
||||
windowCompleted: 0,
|
||||
windowLatencySamples: 0,
|
||||
windowStartMs: nowMs,
|
||||
freezeGrowth: false,
|
||||
criticalDecreaseConsumed: false,
|
||||
utilization: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function noteLatency(
|
||||
state: AdaptationState,
|
||||
latencyMs: number,
|
||||
params: AdaptationParams
|
||||
): void {
|
||||
const sample = Number.isFinite(latencyMs) && latencyMs >= 0 ? latencyMs : 0;
|
||||
state.windowLatencySamples += 1;
|
||||
const sa = params.shortLatencyAlpha;
|
||||
const la = params.longLatencyAlpha;
|
||||
if (state.shortLatencyEwma <= 0 && state.longLatencyEwma <= 0) {
|
||||
state.shortLatencyEwma = sample;
|
||||
state.longLatencyEwma = sample;
|
||||
return;
|
||||
}
|
||||
state.shortLatencyEwma = sa * sample + (1 - sa) * state.shortLatencyEwma;
|
||||
state.longLatencyEwma = la * sample + (1 - la) * state.longLatencyEwma;
|
||||
}
|
||||
|
||||
export function noteOutcome(state: AdaptationState, outcome: AdmissionReleaseOutcome): void {
|
||||
// A single upstream business error freezes growth for the current window; it must not
|
||||
// apply critical multiplicative collapse on its own.
|
||||
if (outcome === "upstream_error") {
|
||||
state.freezeGrowth = true;
|
||||
return;
|
||||
}
|
||||
if (outcome === "timeout") {
|
||||
state.freezeGrowth = true;
|
||||
}
|
||||
}
|
||||
|
||||
export function setPressure(state: AdaptationState, pressure: AdmissionPressure): void {
|
||||
const severity: Record<AdmissionPressure, number> = { normal: 0, high: 1, critical: 2 };
|
||||
if (severity[pressure] > severity[state.pressure]) state.pressure = pressure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the current feedback window and adjust the limit.
|
||||
* Recovery (increase) is slower than decrease; idle/low utilization does not inflate.
|
||||
*/
|
||||
export function closeAdaptationWindow(
|
||||
state: AdaptationState,
|
||||
params: AdaptationParams,
|
||||
nowMs: number
|
||||
): void {
|
||||
const elapsed = Math.max(1, Math.min(params.windowMs, nowMs - state.windowStartMs));
|
||||
// sampleActiveIntegral already accounts for every interval exactly once.
|
||||
const avgActive = state.windowActiveCostIntegral / elapsed;
|
||||
const util = state.currentLimit > 0 ? avgActive / state.currentLimit : 0;
|
||||
state.utilization = Math.max(0, Math.min(1, util));
|
||||
|
||||
let next = state.currentLimit;
|
||||
const gradient =
|
||||
state.longLatencyEwma > 0
|
||||
? (state.shortLatencyEwma - state.longLatencyEwma) / state.longLatencyEwma
|
||||
: 0;
|
||||
|
||||
if (state.pressure === "critical") {
|
||||
// Immediate observePressure may already have applied the critical factor once.
|
||||
if (!state.criticalDecreaseConsumed) {
|
||||
next = Math.floor(next * params.criticalDecreaseFactor);
|
||||
}
|
||||
} else if (
|
||||
state.pressure === "high" ||
|
||||
(state.windowLatencySamples > 0 && gradient >= params.latencyGradientThreshold)
|
||||
) {
|
||||
next = Math.floor(next * params.decreaseFactor);
|
||||
} else if (
|
||||
!state.freezeGrowth &&
|
||||
state.pressure === "normal" &&
|
||||
state.utilization >= params.highUtilizationThreshold &&
|
||||
state.windowCompleted > 0
|
||||
) {
|
||||
const step = Math.min(params.increaseStep, params.maxIncreasePerWindow);
|
||||
next = next + step;
|
||||
}
|
||||
// A genuinely low-utilization window recovers the latency baseline so stale gradients expire.
|
||||
if (state.utilization <= params.lowUtilizationThreshold) {
|
||||
state.shortLatencyEwma = state.longLatencyEwma;
|
||||
}
|
||||
|
||||
state.currentLimit = clampLimit(next, params.minLimit, params.maxLimit);
|
||||
state.windowActiveCostIntegral = 0;
|
||||
state.windowCompleted = 0;
|
||||
state.windowLatencySamples = 0;
|
||||
state.windowStartMs = nowMs;
|
||||
state.freezeGrowth = false;
|
||||
state.criticalDecreaseConsumed = false;
|
||||
state.pressure = "normal";
|
||||
}
|
||||
|
||||
export function sampleActiveIntegral(
|
||||
state: AdaptationState,
|
||||
activeCost: number,
|
||||
dtMs: number
|
||||
): void {
|
||||
if (dtMs <= 0 || activeCost <= 0) return;
|
||||
const boundedActiveCost = Math.min(activeCost, state.currentLimit);
|
||||
const contribution =
|
||||
dtMs > Math.floor(Number.MAX_SAFE_INTEGER / boundedActiveCost)
|
||||
? Number.MAX_SAFE_INTEGER
|
||||
: boundedActiveCost * dtMs;
|
||||
state.windowActiveCostIntegral =
|
||||
contribution >= Number.MAX_SAFE_INTEGER - state.windowActiveCostIntegral
|
||||
? Number.MAX_SAFE_INTEGER
|
||||
: state.windowActiveCostIntegral + contribution;
|
||||
}
|
||||
167
open-sse/services/admission/config.ts
Normal file
167
open-sse/services/admission/config.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { resolveCostConfig } from "./cost.ts";
|
||||
import {
|
||||
MAX_ADMISSION_COST_OR_LIMIT,
|
||||
MAX_ADMISSION_WINDOW_MS,
|
||||
type AdaptiveAdmissionConfig,
|
||||
type AdmissionMode,
|
||||
} from "./types.ts";
|
||||
import type { AdaptationParams } from "./adaptation.ts";
|
||||
|
||||
export { MAX_ADMISSION_COST_OR_LIMIT, MAX_ADMISSION_WINDOW_MS };
|
||||
|
||||
export interface ValidatedConfig {
|
||||
mode: AdmissionMode;
|
||||
minLimit: number;
|
||||
maxLimit: number;
|
||||
initialLimit: number;
|
||||
maxQueueCount: number;
|
||||
maxQueueCost: number;
|
||||
defaultMaxWaitMs: number;
|
||||
windowMs: number;
|
||||
adaptation: AdaptationParams;
|
||||
maxRequestCost: number;
|
||||
costConfig: ReturnType<typeof resolveCostConfig>;
|
||||
}
|
||||
|
||||
function requirePositiveInt(
|
||||
name: string,
|
||||
value: unknown,
|
||||
max: number = MAX_ADMISSION_COST_OR_LIMIT
|
||||
): number {
|
||||
if (
|
||||
typeof value !== "number" ||
|
||||
!Number.isFinite(value) ||
|
||||
value <= 0 ||
|
||||
!Number.isSafeInteger(value)
|
||||
) {
|
||||
throw new RangeError(`${name} must be a positive safe integer`);
|
||||
}
|
||||
if (value > max) {
|
||||
throw new RangeError(`${name} must be <= ${max}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function requireUnitInterval(name: string, value: unknown, fallback: number): number {
|
||||
if (value === undefined) return fallback;
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > 1) {
|
||||
throw new RangeError(`${name} must be in (0, 1]`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function requireDecreaseFactor(name: string, value: unknown, fallback: number): number {
|
||||
if (value === undefined) return fallback;
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value >= 1) {
|
||||
throw new RangeError(`${name} must be in (0, 1)`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function resolveMode(mode: AdaptiveAdmissionConfig["mode"]): AdmissionMode {
|
||||
if (mode === undefined) return "shadow";
|
||||
if (mode !== "off" && mode !== "shadow" && mode !== "enforce") {
|
||||
throw new RangeError("mode must be off|shadow|enforce");
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
|
||||
function resolveAdaptationParams(
|
||||
input: AdaptiveAdmissionConfig,
|
||||
minLimit: number,
|
||||
maxLimit: number,
|
||||
windowMs: number
|
||||
): AdaptationParams {
|
||||
const decreaseFactor = requireDecreaseFactor("decreaseFactor", input.decreaseFactor, 0.8);
|
||||
const criticalDecreaseFactor = requireDecreaseFactor(
|
||||
"criticalDecreaseFactor",
|
||||
input.criticalDecreaseFactor,
|
||||
0.5
|
||||
);
|
||||
const increaseStep =
|
||||
input.increaseStep === undefined ? 1 : requirePositiveInt("increaseStep", input.increaseStep);
|
||||
const maxIncreasePerWindow =
|
||||
input.maxIncreasePerWindow === undefined
|
||||
? increaseStep
|
||||
: requirePositiveInt("maxIncreasePerWindow", input.maxIncreasePerWindow);
|
||||
|
||||
const shortLatencyAlpha = requireUnitInterval("shortLatencyAlpha", input.shortLatencyAlpha, 0.5);
|
||||
const longLatencyAlpha = requireUnitInterval("longLatencyAlpha", input.longLatencyAlpha, 0.1);
|
||||
const highUtilizationThreshold = requireUnitInterval(
|
||||
"highUtilizationThreshold",
|
||||
input.highUtilizationThreshold,
|
||||
0.7
|
||||
);
|
||||
const lowUtilizationThreshold = requireUnitInterval(
|
||||
"lowUtilizationThreshold",
|
||||
input.lowUtilizationThreshold,
|
||||
0.3
|
||||
);
|
||||
if (criticalDecreaseFactor > decreaseFactor) {
|
||||
throw new RangeError("criticalDecreaseFactor must be <= decreaseFactor");
|
||||
}
|
||||
if (lowUtilizationThreshold >= highUtilizationThreshold) {
|
||||
throw new RangeError("lowUtilizationThreshold must be < highUtilizationThreshold");
|
||||
}
|
||||
if (shortLatencyAlpha <= longLatencyAlpha) {
|
||||
throw new RangeError("shortLatencyAlpha must be > longLatencyAlpha");
|
||||
}
|
||||
|
||||
return {
|
||||
minLimit,
|
||||
maxLimit,
|
||||
windowMs,
|
||||
shortLatencyAlpha,
|
||||
longLatencyAlpha,
|
||||
increaseStep,
|
||||
decreaseFactor,
|
||||
criticalDecreaseFactor,
|
||||
highUtilizationThreshold,
|
||||
lowUtilizationThreshold,
|
||||
latencyGradientThreshold: requireUnitInterval(
|
||||
"latencyGradientThreshold",
|
||||
input.latencyGradientThreshold,
|
||||
0.25
|
||||
),
|
||||
maxIncreasePerWindow,
|
||||
};
|
||||
}
|
||||
|
||||
export function validateConfig(input: AdaptiveAdmissionConfig): ValidatedConfig {
|
||||
const minLimit = requirePositiveInt("minLimit", input.minLimit);
|
||||
const maxLimit = requirePositiveInt("maxLimit", input.maxLimit);
|
||||
if (minLimit > maxLimit) {
|
||||
throw new RangeError("minLimit must be <= maxLimit");
|
||||
}
|
||||
const initialLimit = requirePositiveInt("initialLimit", input.initialLimit);
|
||||
// Queue count is not multiplied into cost×time products; keep the full safe-integer range.
|
||||
const maxQueueCount = requirePositiveInt(
|
||||
"maxQueueCount",
|
||||
input.maxQueueCount,
|
||||
Number.MAX_SAFE_INTEGER
|
||||
);
|
||||
const maxQueueCost = requirePositiveInt("maxQueueCost", input.maxQueueCost);
|
||||
const windowMs =
|
||||
input.windowMs === undefined
|
||||
? 1000
|
||||
: requirePositiveInt("windowMs", input.windowMs, MAX_ADMISSION_WINDOW_MS);
|
||||
const defaultMaxWaitMs =
|
||||
input.defaultMaxWaitMs === undefined
|
||||
? 5_000
|
||||
: requirePositiveInt("defaultMaxWaitMs", input.defaultMaxWaitMs, MAX_ADMISSION_WINDOW_MS);
|
||||
const costConfig = resolveCostConfig(input.cost);
|
||||
|
||||
return {
|
||||
mode: resolveMode(input.mode),
|
||||
minLimit,
|
||||
maxLimit,
|
||||
initialLimit,
|
||||
maxQueueCount,
|
||||
maxQueueCost,
|
||||
defaultMaxWaitMs,
|
||||
windowMs,
|
||||
maxRequestCost: costConfig.maxRequestCost,
|
||||
costConfig,
|
||||
adaptation: resolveAdaptationParams(input, minLimit, maxLimit, windowMs),
|
||||
};
|
||||
}
|
||||
624
open-sse/services/admission/controller.ts
Normal file
624
open-sse/services/admission/controller.ts
Normal file
@@ -0,0 +1,624 @@
|
||||
import {
|
||||
closeAdaptationWindow,
|
||||
createAdaptationState,
|
||||
noteLatency,
|
||||
noteOutcome,
|
||||
sampleActiveIntegral,
|
||||
setPressure,
|
||||
type AdaptationState,
|
||||
} from "./adaptation.ts";
|
||||
import { validateConfig, type ValidatedConfig } from "./config.ts";
|
||||
import { estimateAdmissionCost, normalizeRequestCost } from "./cost.ts";
|
||||
import { FairCostQueue, type QueueEntry } from "./queue.ts";
|
||||
import {
|
||||
MAX_ADMISSION_WINDOW_MS,
|
||||
createAdmissionRejectError,
|
||||
type AdaptiveAdmissionConfig,
|
||||
type AdmissionAcquireResult,
|
||||
type AdmissionAdmitted,
|
||||
type AdmissionClock,
|
||||
type AdmissionLease,
|
||||
type AdmissionPressure,
|
||||
type AdmissionRejectCode,
|
||||
type AdmissionReleaseMeta,
|
||||
type AdmissionReleaseOutcome,
|
||||
type AdmissionRequest,
|
||||
type AdmissionSnapshot,
|
||||
type ShadowDecision,
|
||||
} from "./types.ts";
|
||||
|
||||
type VirtualDisposition = "active" | "queued" | "rejected" | "none";
|
||||
|
||||
const MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
|
||||
|
||||
/** Snapshot numbers are always finite safe integers; never emit rounded unsafe Number values. */
|
||||
function saturateSnapshotNumber(value: number): number {
|
||||
if (!Number.isFinite(value) || value <= 0) return 0;
|
||||
if (value >= Number.MAX_SAFE_INTEGER) return Number.MAX_SAFE_INTEGER;
|
||||
return Math.floor(value);
|
||||
}
|
||||
|
||||
function bigintToSnapshotNumber(value: bigint): number {
|
||||
if (value <= 0n) return 0;
|
||||
if (value >= MAX_SAFE_BIGINT) return Number.MAX_SAFE_INTEGER;
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
function addSaturated(total: number, delta: number): number {
|
||||
if (delta <= 0) return saturateSnapshotNumber(total);
|
||||
if (total >= Number.MAX_SAFE_INTEGER - delta) return Number.MAX_SAFE_INTEGER;
|
||||
return total + delta;
|
||||
}
|
||||
|
||||
interface ActiveLeaseRecord {
|
||||
id: string;
|
||||
cost: number;
|
||||
released: boolean;
|
||||
admittedAtMs: number;
|
||||
virtualDisposition: VirtualDisposition;
|
||||
}
|
||||
|
||||
interface QueuedPayload {
|
||||
resolve: (value: AdmissionAdmitted) => void;
|
||||
reject: (err: Error) => void;
|
||||
signal?: AbortSignal;
|
||||
onAbort?: () => void;
|
||||
}
|
||||
|
||||
let leaseSeq = 0;
|
||||
|
||||
function nextId(prefix: string): string {
|
||||
leaseSeq += 1;
|
||||
return `${prefix}-${leaseSeq}`;
|
||||
}
|
||||
|
||||
function defaultClock(): AdmissionClock {
|
||||
return {
|
||||
now: () => Date.now(),
|
||||
setTimer: (fn, delayMs) => {
|
||||
const handle = setTimeout(fn, delayMs);
|
||||
// Window/deadline timers must not pin the event loop open when idle.
|
||||
if (typeof handle.unref === "function") handle.unref();
|
||||
return handle;
|
||||
},
|
||||
clearTimer: (id) => clearTimeout(id as ReturnType<typeof setTimeout>),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dependency-injected weighted adaptive admission controller.
|
||||
* Pure in-process core: no env/settings/route wiring.
|
||||
*/
|
||||
export class AdaptiveAdmissionController {
|
||||
private config: ValidatedConfig;
|
||||
private readonly clock: AdmissionClock;
|
||||
private adaptation: AdaptationState;
|
||||
private queue: FairCostQueue<QueuedPayload>;
|
||||
private virtualQueue: FairCostQueue<{ recordId: string }>;
|
||||
private readonly active = new Map<string, ActiveLeaseRecord>();
|
||||
private activeCost = 0n;
|
||||
private virtualActiveCost = 0;
|
||||
private virtualActiveCount = 0;
|
||||
private lastSampleMs: number;
|
||||
private windowTimer: unknown = undefined;
|
||||
private shutDown = false;
|
||||
|
||||
private admittedCount = 0;
|
||||
private rejectedCount = 0;
|
||||
private wouldAdmitCount = 0;
|
||||
private wouldQueueCount = 0;
|
||||
private wouldRejectCount = 0;
|
||||
|
||||
constructor(config: AdaptiveAdmissionConfig, clock?: Partial<AdmissionClock>) {
|
||||
this.config = validateConfig(config);
|
||||
this.clock = {
|
||||
now: clock?.now ?? defaultClock().now,
|
||||
setTimer: clock?.setTimer ?? defaultClock().setTimer,
|
||||
clearTimer: clock?.clearTimer ?? defaultClock().clearTimer,
|
||||
};
|
||||
const now = this.clock.now();
|
||||
this.adaptation = createAdaptationState(
|
||||
this.config.initialLimit,
|
||||
this.config.minLimit,
|
||||
this.config.maxLimit,
|
||||
now
|
||||
);
|
||||
this.queue = new FairCostQueue(this.config.maxQueueCount, this.config.maxQueueCost);
|
||||
this.virtualQueue = new FairCostQueue(this.config.maxQueueCount, this.config.maxQueueCost);
|
||||
this.lastSampleMs = now;
|
||||
this.armWindowTimer();
|
||||
}
|
||||
|
||||
updateConfig(config: AdaptiveAdmissionConfig): void {
|
||||
const next = validateConfig(config);
|
||||
this.sampleIntegral();
|
||||
this.config = next;
|
||||
this.adaptation.currentLimit = Math.min(
|
||||
next.maxLimit,
|
||||
Math.max(next.minLimit, this.adaptation.currentLimit)
|
||||
);
|
||||
this.adaptation.windowStartMs = this.clock.now();
|
||||
this.adaptation.windowActiveCostIntegral = 0;
|
||||
this.adaptation.windowCompleted = 0;
|
||||
this.adaptation.windowLatencySamples = 0;
|
||||
this.adaptation.freezeGrowth = false;
|
||||
this.adaptation.criticalDecreaseConsumed = false;
|
||||
this.adaptation.pressure = "normal";
|
||||
this.lastSampleMs = this.clock.now();
|
||||
|
||||
const drained = this.queue.drain();
|
||||
this.queue = new FairCostQueue(next.maxQueueCount, next.maxQueueCost);
|
||||
for (const entry of drained) {
|
||||
if (next.mode !== "enforce") {
|
||||
this.clearEntryTimer(entry);
|
||||
this.detachAbort(entry);
|
||||
entry.payload.resolve(this.admit(entry.cost));
|
||||
continue;
|
||||
}
|
||||
// Cost above the new enforce limit must fail closed immediately, never strand until deadline.
|
||||
if (entry.cost > this.adaptation.currentLimit) {
|
||||
this.failQueued(
|
||||
entry,
|
||||
"ADMISSION_OVERSIZED",
|
||||
"request cost exceeds max budget after config update"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!this.queue.enqueue(entry)) {
|
||||
this.failQueued(entry, "ADMISSION_QUEUE_FULL", "queue capacity reduced");
|
||||
}
|
||||
}
|
||||
|
||||
this.rebuildVirtualState(next.mode === "shadow");
|
||||
this.armWindowTimer();
|
||||
if (next.mode === "enforce") {
|
||||
this.dispatch();
|
||||
}
|
||||
}
|
||||
|
||||
snapshot(): AdmissionSnapshot {
|
||||
this.sampleIntegral();
|
||||
return {
|
||||
mode: this.config.mode,
|
||||
currentLimit: this.adaptation.currentLimit,
|
||||
minLimit: this.config.minLimit,
|
||||
maxLimit: this.config.maxLimit,
|
||||
activeCost: bigintToSnapshotNumber(this.activeCost),
|
||||
activeCount: saturateSnapshotNumber(this.active.size),
|
||||
queuedCost: saturateSnapshotNumber(this.queue.totalCost),
|
||||
queuedCount: saturateSnapshotNumber(this.queue.size),
|
||||
virtualActiveCost: saturateSnapshotNumber(this.virtualActiveCost),
|
||||
virtualActiveCount: saturateSnapshotNumber(this.virtualActiveCount),
|
||||
virtualQueuedCost: saturateSnapshotNumber(this.virtualQueue.totalCost),
|
||||
virtualQueuedCount: saturateSnapshotNumber(this.virtualQueue.size),
|
||||
admittedCount: saturateSnapshotNumber(this.admittedCount),
|
||||
rejectedCount: saturateSnapshotNumber(this.rejectedCount),
|
||||
wouldAdmitCount: saturateSnapshotNumber(this.wouldAdmitCount),
|
||||
wouldQueueCount: saturateSnapshotNumber(this.wouldQueueCount),
|
||||
wouldRejectCount: saturateSnapshotNumber(this.wouldRejectCount),
|
||||
shortLatencyEwma: this.adaptation.shortLatencyEwma,
|
||||
longLatencyEwma: this.adaptation.longLatencyEwma,
|
||||
utilization: this.adaptation.utilization,
|
||||
pressure: this.adaptation.pressure,
|
||||
shutdown: this.shutDown,
|
||||
};
|
||||
}
|
||||
|
||||
observePressure(pressure: AdmissionPressure): void {
|
||||
setPressure(this.adaptation, pressure);
|
||||
if (pressure === "critical") {
|
||||
// Immediate fast decrease once per window; window close must not re-apply it.
|
||||
if (!this.adaptation.criticalDecreaseConsumed) {
|
||||
this.adaptation.currentLimit = Math.max(
|
||||
this.config.minLimit,
|
||||
Math.floor(this.adaptation.currentLimit * this.config.adaptation.criticalDecreaseFactor)
|
||||
);
|
||||
this.adaptation.criticalDecreaseConsumed = true;
|
||||
this.dispatch();
|
||||
this.dispatchVirtual();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Deterministic window tick for tests / injected clocks. */
|
||||
tick(): void {
|
||||
this.sampleIntegral();
|
||||
closeAdaptationWindow(this.adaptation, this.config.adaptation, this.clock.now());
|
||||
// Real queue first, then virtual: raised limits must promote shadow-queued work
|
||||
// before newer arrivals are classified against the updated budget.
|
||||
this.dispatch();
|
||||
this.dispatchVirtual();
|
||||
}
|
||||
|
||||
async acquire(request: AdmissionRequest): Promise<AdmissionAcquireResult> {
|
||||
if (this.shutDown) {
|
||||
return this.reject("ADMISSION_SHUTDOWN", "admission controller is shut down");
|
||||
}
|
||||
|
||||
if (request.signal?.aborted) {
|
||||
return this.reject("ADMISSION_ABORTED", "request aborted before acquire");
|
||||
}
|
||||
|
||||
if (request.pressure) setPressure(this.adaptation, request.pressure);
|
||||
|
||||
const cost = this.resolveCost(request);
|
||||
const mode = this.config.mode;
|
||||
|
||||
if (mode === "off") {
|
||||
return this.admitVirtual(cost);
|
||||
}
|
||||
|
||||
const limit = this.adaptation.currentLimit;
|
||||
|
||||
if (mode === "shadow") {
|
||||
return this.acquireShadow(request, cost, limit);
|
||||
}
|
||||
|
||||
// enforce
|
||||
if (cost > limit) {
|
||||
return this.reject("ADMISSION_OVERSIZED", "request cost exceeds max budget");
|
||||
}
|
||||
|
||||
// Once work is queued, every newer request joins the same fair queue even if it
|
||||
// currently fits. This makes bounded bypass accounting effective and prevents
|
||||
// direct arrivals from indefinitely jumping an older reserved weighted request.
|
||||
if (this.queue.size === 0 && this.activeCost + BigInt(cost) <= BigInt(limit)) {
|
||||
return this.admit(cost);
|
||||
}
|
||||
|
||||
if (!this.queue.canAccept(cost)) {
|
||||
return this.reject("ADMISSION_QUEUE_FULL", "admission queue is full");
|
||||
}
|
||||
|
||||
return this.enqueue(request, cost);
|
||||
}
|
||||
|
||||
shutdown(): void {
|
||||
if (this.shutDown) return;
|
||||
this.shutDown = true;
|
||||
if (this.windowTimer !== undefined) {
|
||||
this.clock.clearTimer(this.windowTimer);
|
||||
this.windowTimer = undefined;
|
||||
}
|
||||
const drained = this.queue.drain();
|
||||
for (const entry of drained) {
|
||||
this.clearEntryTimer(entry);
|
||||
this.detachAbort(entry);
|
||||
entry.payload.reject(
|
||||
createAdmissionRejectError("ADMISSION_SHUTDOWN", "admission controller shut down")
|
||||
);
|
||||
this.rejectedCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
private resolveCost(request: AdmissionRequest): number {
|
||||
if (request.cost !== undefined) {
|
||||
return normalizeRequestCost(request.cost, this.config.maxRequestCost);
|
||||
}
|
||||
if (request.features) {
|
||||
return estimateAdmissionCost(request.features, this.config.costConfig);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
private acquireShadow(request: AdmissionRequest, cost: number, limit: number): AdmissionAdmitted {
|
||||
let decision: ShadowDecision;
|
||||
let disposition: VirtualDisposition;
|
||||
if (cost > limit || !Number.isSafeInteger(cost)) {
|
||||
decision = "would-reject";
|
||||
disposition = "rejected";
|
||||
this.wouldRejectCount += 1;
|
||||
} else if (this.virtualActiveCost + cost <= limit) {
|
||||
decision = "would-admit";
|
||||
disposition = "active";
|
||||
this.virtualActiveCost = addSaturated(this.virtualActiveCost, cost);
|
||||
this.virtualActiveCount = addSaturated(this.virtualActiveCount, 1);
|
||||
this.wouldAdmitCount = addSaturated(this.wouldAdmitCount, 1);
|
||||
} else if (this.virtualQueue.canAccept(cost)) {
|
||||
decision = "would-queue";
|
||||
disposition = "queued";
|
||||
this.wouldQueueCount += 1;
|
||||
} else {
|
||||
decision = "would-reject";
|
||||
disposition = "rejected";
|
||||
this.wouldRejectCount += 1;
|
||||
}
|
||||
|
||||
const admitted = this.admit(cost, disposition);
|
||||
if (disposition === "queued") {
|
||||
this.virtualQueue.enqueue({
|
||||
id: admitted.lease.id,
|
||||
tenantKey: request.tenantKey || "_default",
|
||||
cost,
|
||||
enqueuedAtMs: this.clock.now(),
|
||||
deadlineMs: Number.MAX_SAFE_INTEGER,
|
||||
payload: { recordId: admitted.lease.id },
|
||||
});
|
||||
}
|
||||
return { ...admitted, shadowDecision: decision };
|
||||
}
|
||||
|
||||
private admitVirtual(cost: number): AdmissionAdmitted {
|
||||
// Mode off: no accounting.
|
||||
const id = nextId("lease");
|
||||
const lease: AdmissionLease = {
|
||||
id,
|
||||
cost,
|
||||
get released() {
|
||||
return true;
|
||||
},
|
||||
release: () => {
|
||||
/* no-op */
|
||||
},
|
||||
};
|
||||
this.admittedCount += 1;
|
||||
return { status: "admitted", lease };
|
||||
}
|
||||
|
||||
private admit(cost: number, virtualDisposition: VirtualDisposition = "none"): AdmissionAdmitted {
|
||||
this.sampleIntegral();
|
||||
const id = nextId("lease");
|
||||
const record: ActiveLeaseRecord = {
|
||||
id,
|
||||
cost,
|
||||
released: false,
|
||||
admittedAtMs: this.clock.now(),
|
||||
virtualDisposition,
|
||||
};
|
||||
this.active.set(id, record);
|
||||
this.activeCost += BigInt(cost);
|
||||
this.admittedCount += 1;
|
||||
|
||||
const controller = this;
|
||||
const lease: AdmissionLease = {
|
||||
id,
|
||||
cost,
|
||||
get released() {
|
||||
return record.released;
|
||||
},
|
||||
release(outcome: AdmissionReleaseOutcome = "success", meta?: AdmissionReleaseMeta) {
|
||||
controller.releaseLease(record, outcome, meta);
|
||||
},
|
||||
};
|
||||
return { status: "admitted", lease };
|
||||
}
|
||||
|
||||
private releaseLease(
|
||||
record: ActiveLeaseRecord,
|
||||
outcome: AdmissionReleaseOutcome,
|
||||
meta?: AdmissionReleaseMeta
|
||||
): void {
|
||||
if (record.released) return;
|
||||
record.released = true;
|
||||
// Sample while the lease still contributes to activeCost so utilization EWMA sees load.
|
||||
this.sampleIntegral();
|
||||
if (this.active.has(record.id)) {
|
||||
this.active.delete(record.id);
|
||||
this.activeCost -= BigInt(record.cost);
|
||||
}
|
||||
|
||||
const latency =
|
||||
meta?.latencyMs !== undefined
|
||||
? meta.latencyMs
|
||||
: Math.max(0, this.clock.now() - record.admittedAtMs);
|
||||
noteLatency(this.adaptation, latency, this.config.adaptation);
|
||||
noteOutcome(this.adaptation, outcome);
|
||||
this.adaptation.windowCompleted += 1;
|
||||
if (meta?.pressure) setPressure(this.adaptation, meta.pressure);
|
||||
this.releaseVirtual(record);
|
||||
|
||||
this.dispatch();
|
||||
}
|
||||
|
||||
private enqueue(request: AdmissionRequest, cost: number): AdmissionAcquireResult {
|
||||
const id = nextId("q");
|
||||
const maxWait = normalizeRequestCost(
|
||||
request.maxWaitMs ?? this.config.defaultMaxWaitMs,
|
||||
MAX_ADMISSION_WINDOW_MS
|
||||
);
|
||||
const now = this.clock.now();
|
||||
const deadlineMs = Math.min(Number.MAX_SAFE_INTEGER, now + maxWait);
|
||||
|
||||
let settle: {
|
||||
resolve: (v: AdmissionAdmitted) => void;
|
||||
reject: (e: Error) => void;
|
||||
};
|
||||
const promise = new Promise<AdmissionAdmitted>((resolve, reject) => {
|
||||
settle = { resolve, reject };
|
||||
});
|
||||
|
||||
const entry: QueueEntry<QueuedPayload> = {
|
||||
id,
|
||||
tenantKey: request.tenantKey && request.tenantKey.length > 0 ? request.tenantKey : "_default",
|
||||
cost,
|
||||
enqueuedAtMs: now,
|
||||
deadlineMs,
|
||||
payload: {
|
||||
resolve: (v) => settle.resolve(v),
|
||||
reject: (e) => settle.reject(e),
|
||||
signal: request.signal,
|
||||
},
|
||||
};
|
||||
|
||||
if (!this.queue.enqueue(entry)) {
|
||||
return this.reject("ADMISSION_QUEUE_FULL", "admission queue is full");
|
||||
}
|
||||
|
||||
entry.timerId = this.clock.setTimer(
|
||||
() => {
|
||||
this.expireEntry(id, "ADMISSION_DEADLINE", "admission wait deadline exceeded");
|
||||
},
|
||||
Math.max(0, deadlineMs - now)
|
||||
);
|
||||
|
||||
if (request.signal) {
|
||||
const onAbort = () => {
|
||||
this.expireEntry(id, "ADMISSION_ABORTED", "request aborted while queued");
|
||||
};
|
||||
entry.payload.onAbort = onAbort;
|
||||
request.signal.addEventListener("abort", onAbort, { once: true });
|
||||
}
|
||||
|
||||
// Capacity may have freed between check and enqueue in concurrent hosts; try dispatch.
|
||||
this.dispatch();
|
||||
|
||||
return { status: "queued", promise };
|
||||
}
|
||||
|
||||
private expireEntry(id: string, code: AdmissionRejectCode, message: string): void {
|
||||
const entry = this.queue.removeById(id);
|
||||
if (!entry) return;
|
||||
this.clearEntryTimer(entry);
|
||||
this.detachAbort(entry);
|
||||
entry.payload.reject(createAdmissionRejectError(code, message));
|
||||
this.rejectedCount += 1;
|
||||
// Resume enforce dispatch so a now-fitting successor is not stranded until
|
||||
// unrelated activity. dispatch() is a no-op after shutdown / non-enforce.
|
||||
this.dispatch();
|
||||
}
|
||||
|
||||
private failQueued(
|
||||
entry: QueueEntry<QueuedPayload>,
|
||||
code: AdmissionRejectCode,
|
||||
message: string
|
||||
): void {
|
||||
this.clearEntryTimer(entry);
|
||||
this.detachAbort(entry);
|
||||
entry.payload.reject(createAdmissionRejectError(code, message));
|
||||
this.rejectedCount += 1;
|
||||
}
|
||||
|
||||
private dispatch(): void {
|
||||
if (this.shutDown || this.config.mode !== "enforce") return;
|
||||
|
||||
while (this.queue.size > 0) {
|
||||
const limit = this.adaptation.currentLimit;
|
||||
const available = BigInt(limit) - this.activeCost;
|
||||
if (available <= 0n) return;
|
||||
const entry = this.queue.dequeue(Number(available));
|
||||
if (!entry) return;
|
||||
this.clearEntryTimer(entry);
|
||||
this.detachAbort(entry);
|
||||
if (entry.payload.signal?.aborted) {
|
||||
entry.payload.reject(
|
||||
createAdmissionRejectError("ADMISSION_ABORTED", "request aborted while queued")
|
||||
);
|
||||
this.rejectedCount += 1;
|
||||
continue;
|
||||
}
|
||||
if (this.clock.now() >= entry.deadlineMs) {
|
||||
entry.payload.reject(
|
||||
createAdmissionRejectError("ADMISSION_DEADLINE", "admission wait deadline exceeded")
|
||||
);
|
||||
this.rejectedCount += 1;
|
||||
continue;
|
||||
}
|
||||
entry.payload.resolve(this.admit(entry.cost));
|
||||
}
|
||||
}
|
||||
|
||||
private releaseVirtual(record: ActiveLeaseRecord): void {
|
||||
if (record.virtualDisposition === "active") {
|
||||
this.virtualActiveCost -= record.cost;
|
||||
this.virtualActiveCount -= 1;
|
||||
} else if (record.virtualDisposition === "queued") {
|
||||
this.virtualQueue.removeById(record.id);
|
||||
}
|
||||
record.virtualDisposition = "none";
|
||||
this.dispatchVirtual();
|
||||
}
|
||||
|
||||
private dispatchVirtual(): void {
|
||||
while (this.virtualQueue.size > 0) {
|
||||
const available = this.adaptation.currentLimit - this.virtualActiveCost;
|
||||
if (available <= 0) return;
|
||||
const entry = this.virtualQueue.dequeue(available);
|
||||
if (!entry) return;
|
||||
const record = this.active.get(entry.payload.recordId);
|
||||
if (!record || record.released) continue;
|
||||
record.virtualDisposition = "active";
|
||||
this.virtualActiveCost = addSaturated(this.virtualActiveCost, record.cost);
|
||||
this.virtualActiveCount = addSaturated(this.virtualActiveCount, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private rebuildVirtualState(enable: boolean): void {
|
||||
this.virtualQueue = new FairCostQueue(this.config.maxQueueCount, this.config.maxQueueCost);
|
||||
this.virtualActiveCost = 0;
|
||||
this.virtualActiveCount = 0;
|
||||
for (const record of this.active.values()) record.virtualDisposition = "none";
|
||||
if (!enable) return;
|
||||
for (const record of this.active.values()) {
|
||||
// Individually oversized work is virtual-rejected, never virtually queued.
|
||||
if (record.cost > this.adaptation.currentLimit) {
|
||||
record.virtualDisposition = "rejected";
|
||||
continue;
|
||||
}
|
||||
if (record.cost <= this.adaptation.currentLimit - this.virtualActiveCost) {
|
||||
record.virtualDisposition = "active";
|
||||
this.virtualActiveCost = addSaturated(this.virtualActiveCost, record.cost);
|
||||
this.virtualActiveCount = addSaturated(this.virtualActiveCount, 1);
|
||||
} else if (
|
||||
this.virtualQueue.enqueue({
|
||||
id: record.id,
|
||||
tenantKey: "_existing",
|
||||
cost: record.cost,
|
||||
enqueuedAtMs: record.admittedAtMs,
|
||||
deadlineMs: Number.MAX_SAFE_INTEGER,
|
||||
payload: { recordId: record.id },
|
||||
})
|
||||
) {
|
||||
record.virtualDisposition = "queued";
|
||||
} else {
|
||||
record.virtualDisposition = "rejected";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private reject(code: AdmissionRejectCode, message: string): AdmissionAcquireResult {
|
||||
this.rejectedCount += 1;
|
||||
return { status: "rejected", code, message };
|
||||
}
|
||||
|
||||
private clearEntryTimer(entry: QueueEntry<QueuedPayload>): void {
|
||||
if (entry.timerId !== undefined) {
|
||||
this.clock.clearTimer(entry.timerId);
|
||||
entry.timerId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private detachAbort(entry: QueueEntry<QueuedPayload>): void {
|
||||
if (entry.payload.signal && entry.payload.onAbort) {
|
||||
entry.payload.signal.removeEventListener("abort", entry.payload.onAbort);
|
||||
entry.payload.onAbort = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private sampleIntegral(): void {
|
||||
const now = this.clock.now();
|
||||
const dt = now - this.lastSampleMs;
|
||||
if (dt > 0) {
|
||||
// Cap at currentLimit before Number conversion so shadow oversubscription never
|
||||
// feeds an unsafe rounded activeCost into the utilization integral.
|
||||
const limit = this.adaptation.currentLimit;
|
||||
const activeForIntegral = this.activeCost >= BigInt(limit) ? limit : Number(this.activeCost);
|
||||
sampleActiveIntegral(this.adaptation, activeForIntegral, dt);
|
||||
this.lastSampleMs = now;
|
||||
}
|
||||
}
|
||||
|
||||
private armWindowTimer(): void {
|
||||
if (this.windowTimer !== undefined) {
|
||||
this.clock.clearTimer(this.windowTimer);
|
||||
this.windowTimer = undefined;
|
||||
}
|
||||
if (this.shutDown || this.config.mode === "off") return;
|
||||
const tick = () => {
|
||||
this.tick();
|
||||
if (!this.shutDown && this.config.mode !== "off") {
|
||||
this.windowTimer = this.clock.setTimer(tick, this.config.windowMs);
|
||||
}
|
||||
};
|
||||
this.windowTimer = this.clock.setTimer(tick, this.config.windowMs);
|
||||
}
|
||||
}
|
||||
107
open-sse/services/admission/cost.ts
Normal file
107
open-sse/services/admission/cost.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import {
|
||||
MAX_ADMISSION_COST_OR_LIMIT,
|
||||
type AdmissionCostConfig,
|
||||
type AdmissionCostFeatures,
|
||||
} from "./types.ts";
|
||||
|
||||
export { MAX_ADMISSION_COST_OR_LIMIT };
|
||||
|
||||
export const DEFAULT_ADMISSION_COST_CONFIG: AdmissionCostConfig = Object.freeze({
|
||||
baseCost: 1,
|
||||
bodyBytesPerUnit: 16_384,
|
||||
tokensPerUnit: 1_024,
|
||||
messagesPerUnit: 32,
|
||||
toolsPerUnit: 8,
|
||||
fanoutPerUnit: 1,
|
||||
streamingClassCost: 1,
|
||||
nonStreamingClassCost: 2,
|
||||
maxRequestCost: 1_000,
|
||||
});
|
||||
|
||||
function finiteNonNegative(value: unknown): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return 0;
|
||||
return Math.min(value, Number.MAX_SAFE_INTEGER);
|
||||
}
|
||||
|
||||
function requirePositiveSafeInteger(
|
||||
name: string,
|
||||
value: unknown,
|
||||
max: number = MAX_ADMISSION_COST_OR_LIMIT
|
||||
): number {
|
||||
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
||||
throw new RangeError(`${name} must be a positive safe integer`);
|
||||
}
|
||||
if (value > max) {
|
||||
throw new RangeError(`${name} must be <= ${max}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const COST_CONFIG_KEYS = [
|
||||
"baseCost",
|
||||
"bodyBytesPerUnit",
|
||||
"tokensPerUnit",
|
||||
"messagesPerUnit",
|
||||
"toolsPerUnit",
|
||||
"fanoutPerUnit",
|
||||
"streamingClassCost",
|
||||
"nonStreamingClassCost",
|
||||
"maxRequestCost",
|
||||
] as const satisfies ReadonlyArray<keyof AdmissionCostConfig>;
|
||||
|
||||
/** Merge cost quanta after strictly validating every supplied value. */
|
||||
export function resolveCostConfig(partial?: Partial<AdmissionCostConfig>): AdmissionCostConfig {
|
||||
const d = DEFAULT_ADMISSION_COST_CONFIG;
|
||||
const resolved = {} as AdmissionCostConfig;
|
||||
for (const key of COST_CONFIG_KEYS) {
|
||||
resolved[key] = requirePositiveSafeInteger(key, partial?.[key] ?? d[key]);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function unitsFrom(amount: number, quantum: number): number {
|
||||
return amount <= 0 ? 0 : Math.ceil(amount / quantum);
|
||||
}
|
||||
|
||||
function addBounded(total: number, contribution: number, maximum: number): number {
|
||||
if (contribution >= maximum - total) return maximum;
|
||||
return total + contribution;
|
||||
}
|
||||
|
||||
/** Pure bounded cost estimator from transparent positive safe-integer quanta. */
|
||||
export function estimateAdmissionCost(
|
||||
features: AdmissionCostFeatures,
|
||||
config?: Partial<AdmissionCostConfig>
|
||||
): number {
|
||||
const cfg = resolveCostConfig(config);
|
||||
const body = finiteNonNegative(features?.bodyBytes);
|
||||
const tokens = finiteNonNegative(features?.estimatedInputTokens);
|
||||
const messages = finiteNonNegative(features?.messageCount);
|
||||
const tools = finiteNonNegative(features?.toolCount);
|
||||
const fanout = Math.max(1, finiteNonNegative(features?.requestedFanout));
|
||||
const contributions = [
|
||||
unitsFrom(body, cfg.bodyBytesPerUnit),
|
||||
unitsFrom(tokens, cfg.tokensPerUnit),
|
||||
unitsFrom(messages, cfg.messagesPerUnit),
|
||||
unitsFrom(tools, cfg.toolsPerUnit),
|
||||
unitsFrom(fanout, cfg.fanoutPerUnit),
|
||||
features?.streaming !== false ? cfg.streamingClassCost : cfg.nonStreamingClassCost,
|
||||
];
|
||||
|
||||
let total = Math.min(cfg.baseCost, cfg.maxRequestCost);
|
||||
for (const contribution of contributions) {
|
||||
total = addBounded(total, contribution, cfg.maxRequestCost);
|
||||
if (total === cfg.maxRequestCost) break;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/** Validate and bound a caller-supplied request cost. */
|
||||
export function normalizeRequestCost(
|
||||
cost: unknown,
|
||||
maxRequestCost: number = DEFAULT_ADMISSION_COST_CONFIG.maxRequestCost
|
||||
): number {
|
||||
const max = requirePositiveSafeInteger("maxRequestCost", maxRequestCost);
|
||||
const value = requirePositiveSafeInteger("request cost", cost);
|
||||
return Math.min(value, max);
|
||||
}
|
||||
37
open-sse/services/admission/index.ts
Normal file
37
open-sse/services/admission/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Pure weighted adaptive admission-control core.
|
||||
* No route, settings, or environment wiring in this module surface.
|
||||
*/
|
||||
|
||||
export {
|
||||
DEFAULT_ADMISSION_COST_CONFIG,
|
||||
estimateAdmissionCost,
|
||||
normalizeRequestCost,
|
||||
resolveCostConfig,
|
||||
} from "./cost.ts";
|
||||
|
||||
export { AdaptiveAdmissionController } from "./controller.ts";
|
||||
|
||||
export {
|
||||
MAX_ADMISSION_COST_OR_LIMIT,
|
||||
MAX_ADMISSION_WINDOW_MS,
|
||||
createAdmissionRejectError,
|
||||
type AdaptiveAdmissionConfig,
|
||||
type AdmissionAcquireResult,
|
||||
type AdmissionAdmitted,
|
||||
type AdmissionClock,
|
||||
type AdmissionCostConfig,
|
||||
type AdmissionCostFeatures,
|
||||
type AdmissionLease,
|
||||
type AdmissionMode,
|
||||
type AdmissionPressure,
|
||||
type AdmissionQueued,
|
||||
type AdmissionRejectCode,
|
||||
type AdmissionRejectError,
|
||||
type AdmissionRejected,
|
||||
type AdmissionReleaseMeta,
|
||||
type AdmissionReleaseOutcome,
|
||||
type AdmissionRequest,
|
||||
type AdmissionSnapshot,
|
||||
type ShadowDecision,
|
||||
} from "./types.ts";
|
||||
194
open-sse/services/admission/queue.ts
Normal file
194
open-sse/services/admission/queue.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* Bounded multi-tenant fair queue (round-robin across tenant buckets).
|
||||
* Count + total cost caps; no unbounded arrays of timers beyond one per entry.
|
||||
*/
|
||||
|
||||
/**
|
||||
* After this many pass-overs while unfittable, reserve capacity for the aged head
|
||||
* instead of indefinitely admitting smaller work from other tenants.
|
||||
*/
|
||||
const MAX_UNFITTABLE_SKIPS = 2;
|
||||
|
||||
export interface QueueEntry<T> {
|
||||
id: string;
|
||||
tenantKey: string;
|
||||
cost: number;
|
||||
enqueuedAtMs: number;
|
||||
deadlineMs: number;
|
||||
payload: T;
|
||||
timerId?: unknown;
|
||||
/** Times this head was skipped because it did not fit available cost. */
|
||||
skipCount?: number;
|
||||
}
|
||||
|
||||
export interface FairQueueSnapshot {
|
||||
count: number;
|
||||
cost: number;
|
||||
}
|
||||
|
||||
export class FairCostQueue<T> {
|
||||
private readonly buckets = new Map<string, QueueEntry<T>[]>();
|
||||
private readonly order: string[] = [];
|
||||
private cursor = 0;
|
||||
private count = 0;
|
||||
private cost = 0;
|
||||
|
||||
constructor(
|
||||
readonly maxCount: number,
|
||||
readonly maxCost: number
|
||||
) {}
|
||||
|
||||
get size(): number {
|
||||
return this.count;
|
||||
}
|
||||
|
||||
get totalCost(): number {
|
||||
return this.cost;
|
||||
}
|
||||
|
||||
snapshot(): FairQueueSnapshot {
|
||||
return { count: this.count, cost: this.cost };
|
||||
}
|
||||
|
||||
canAccept(entryCost: number): boolean {
|
||||
if (!Number.isSafeInteger(entryCost) || entryCost <= 0) return false;
|
||||
if (this.count >= this.maxCount) return false;
|
||||
if (entryCost > this.maxCost - this.cost) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
enqueue(entry: QueueEntry<T>): boolean {
|
||||
if (!this.canAccept(entry.cost)) return false;
|
||||
let bucket = this.buckets.get(entry.tenantKey);
|
||||
if (!bucket) {
|
||||
bucket = [];
|
||||
this.buckets.set(entry.tenantKey, bucket);
|
||||
this.order.push(entry.tenantKey);
|
||||
}
|
||||
bucket.push(entry);
|
||||
this.count += 1;
|
||||
this.cost += entry.cost;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Round-robin dequeue, optionally skipping tenant heads that do not fit available cost.
|
||||
* After MAX_UNFITTABLE_SKIPS actual pass-overs, an unfittable head reserves capacity:
|
||||
* smaller work is not admitted ahead of it until it fits, is removed, or capacity rises.
|
||||
*/
|
||||
dequeue(maxCost = Number.MAX_SAFE_INTEGER): QueueEntry<T> | undefined {
|
||||
if (this.count === 0) return undefined;
|
||||
const n = this.order.length;
|
||||
|
||||
// Bounded anti-starvation: prefer the oldest aged unfittable head once reserved.
|
||||
let reserved: { idx: number; entry: QueueEntry<T> } | undefined;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const idx = (this.cursor + i) % n;
|
||||
const tenant = this.order[idx];
|
||||
const entry = this.buckets.get(tenant)?.[0];
|
||||
if (!entry) continue;
|
||||
if ((entry.skipCount ?? 0) >= MAX_UNFITTABLE_SKIPS) {
|
||||
if (!reserved || entry.enqueuedAtMs < reserved.entry.enqueuedAtMs) {
|
||||
reserved = { idx, entry };
|
||||
}
|
||||
}
|
||||
}
|
||||
if (reserved) {
|
||||
if (reserved.entry.cost > maxCost) return undefined;
|
||||
return this.takeAt(reserved.idx);
|
||||
}
|
||||
|
||||
const bypassed: QueueEntry<T>[] = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const idx = (this.cursor + i) % n;
|
||||
const tenant = this.order[idx];
|
||||
const bucket = this.buckets.get(tenant);
|
||||
const entry = bucket?.[0];
|
||||
if (!entry) continue;
|
||||
if (entry.cost > maxCost) {
|
||||
bypassed.push(entry);
|
||||
continue;
|
||||
}
|
||||
// Only an actual smaller admission counts as a pass-over. Merely polling
|
||||
// with no available capacity must not age a head into reservation.
|
||||
for (const skipped of bypassed) {
|
||||
skipped.skipCount = (skipped.skipCount ?? 0) + 1;
|
||||
}
|
||||
return this.takeAt(idx);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private takeAt(idx: number): QueueEntry<T> | undefined {
|
||||
const tenant = this.order[idx];
|
||||
const bucket = this.buckets.get(tenant);
|
||||
const entry = bucket?.[0];
|
||||
if (!entry) return undefined;
|
||||
bucket!.shift();
|
||||
this.count -= 1;
|
||||
this.cost -= entry.cost;
|
||||
entry.skipCount = 0;
|
||||
if (bucket!.length === 0) {
|
||||
this.buckets.delete(tenant);
|
||||
this.order.splice(idx, 1);
|
||||
this.cursor = this.order.length === 0 ? 0 : idx % this.order.length;
|
||||
} else {
|
||||
this.cursor = (idx + 1) % this.order.length;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
/** Peek next without removing (for oversized-vs-limit checks). */
|
||||
peek(): QueueEntry<T> | undefined {
|
||||
if (this.count === 0) return undefined;
|
||||
const n = this.order.length;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const idx = (this.cursor + i) % n;
|
||||
const tenant = this.order[idx];
|
||||
const bucket = this.buckets.get(tenant);
|
||||
if (bucket && bucket.length > 0) return bucket[0];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
removeById(id: string): QueueEntry<T> | undefined {
|
||||
for (let ti = 0; ti < this.order.length; ti++) {
|
||||
const tenant = this.order[ti];
|
||||
const bucket = this.buckets.get(tenant);
|
||||
if (!bucket) continue;
|
||||
const idx = bucket.findIndex((e) => e.id === id);
|
||||
if (idx < 0) continue;
|
||||
const [entry] = bucket.splice(idx, 1);
|
||||
this.count -= 1;
|
||||
this.cost -= entry.cost;
|
||||
if (bucket.length === 0) {
|
||||
this.buckets.delete(tenant);
|
||||
this.order.splice(ti, 1);
|
||||
if (this.order.length === 0) {
|
||||
this.cursor = 0;
|
||||
} else if (ti < this.cursor) {
|
||||
// Removing a prior bucket shifts the successor into cursor - 1.
|
||||
this.cursor -= 1;
|
||||
} else if (this.cursor >= this.order.length) {
|
||||
// Removed the final bucket at the cursor; wrap to the head.
|
||||
this.cursor = 0;
|
||||
}
|
||||
// ti === cursor: leave cursor so it now points at the logical successor.
|
||||
// ti > cursor: cursor is unaffected.
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
drain(): QueueEntry<T>[] {
|
||||
const out: QueueEntry<T>[] = [];
|
||||
while (true) {
|
||||
const e = this.dequeue();
|
||||
if (!e) break;
|
||||
out.push(e);
|
||||
}
|
||||
this.cursor = 0;
|
||||
return out;
|
||||
}
|
||||
}
|
||||
186
open-sse/services/admission/requestFeatures.ts
Normal file
186
open-sse/services/admission/requestFeatures.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Cheap bounded admission cost features from an already-parsed request body.
|
||||
* Never re-parses, stringifies, clones, or invokes toJSON.
|
||||
*/
|
||||
|
||||
import { estimateSizeFast } from "../../utils/estimateSize.ts";
|
||||
import type { AdmissionCostFeatures } from "./types.ts";
|
||||
|
||||
export type AdmissionFeatureExtractionContext = {
|
||||
/** When set, wins over any body/wrapped stream field. */
|
||||
streaming?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Max tools/functions array entries inspected.
|
||||
* Uninspected tail is charged conservatively so truncation cannot undercharge cost.
|
||||
*/
|
||||
export const ADMISSION_TOOL_SCAN_BUDGET = 64;
|
||||
|
||||
type FeatureDraft = {
|
||||
messageCount: number;
|
||||
toolCount: number;
|
||||
requestedFanout: number | null;
|
||||
streaming: boolean | null;
|
||||
};
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function asArray(value: unknown): unknown[] | null {
|
||||
return Array.isArray(value) ? value : null;
|
||||
}
|
||||
|
||||
function positiveInt(value: unknown): number | null {
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return null;
|
||||
if (!Number.isSafeInteger(value)) {
|
||||
return Math.min(Number.MAX_SAFE_INTEGER, Math.floor(value));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function saturateCount(n: number): number {
|
||||
if (!Number.isFinite(n) || n <= 0) return 0;
|
||||
if (!Number.isSafeInteger(n)) {
|
||||
return Math.min(Number.MAX_SAFE_INTEGER, Math.floor(n));
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count all recognized tool aliases/layers under one shared entry budget.
|
||||
* If their combined length cannot be inspected completely, saturate before indexed access
|
||||
* so an unseen alias or wrapped tail cannot undercharge heavier declarations.
|
||||
*/
|
||||
function countTools(layers: Array<Record<string, unknown>>): number {
|
||||
const sources: unknown[][] = [];
|
||||
const seen = new Set<unknown[]>();
|
||||
for (const layer of layers) {
|
||||
for (const value of [layer.tools, layer.functions]) {
|
||||
const source = asArray(value);
|
||||
if (!source || seen.has(source)) continue;
|
||||
seen.add(source);
|
||||
sources.push(source);
|
||||
}
|
||||
}
|
||||
|
||||
let entryCount = 0;
|
||||
for (const source of sources) {
|
||||
if (source.length > ADMISSION_TOOL_SCAN_BUDGET - entryCount) {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
entryCount += source.length;
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
for (const source of sources) {
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
const entry = source[i];
|
||||
if (isPlainObject(entry)) {
|
||||
const declarations = asArray(entry.functionDeclarations);
|
||||
if (declarations) {
|
||||
total = Math.min(Number.MAX_SAFE_INTEGER, total + saturateCount(declarations.length));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
total = Math.min(Number.MAX_SAFE_INTEGER, total + 1);
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
function countMessages(layer: Record<string, unknown>): number {
|
||||
const messages = asArray(layer.messages);
|
||||
const contents = asArray(layer.contents);
|
||||
const inputArr = asArray(layer.input);
|
||||
let count = Math.max(
|
||||
saturateCount(messages?.length ?? 0),
|
||||
saturateCount(contents?.length ?? 0),
|
||||
saturateCount(inputArr?.length ?? 0)
|
||||
);
|
||||
// Responses API: non-empty string `input` is one input item.
|
||||
if (count === 0 && typeof layer.input === "string" && layer.input.length > 0) {
|
||||
count = 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function readFanout(layer: Record<string, unknown>): number | null {
|
||||
const direct =
|
||||
positiveInt(layer.n) ?? positiveInt(layer.candidateCount) ?? positiveInt(layer.candidate_count);
|
||||
if (direct != null) return direct;
|
||||
// Known nested Gemini/Antigravity shape only — no recursive walk.
|
||||
if (isPlainObject(layer.generationConfig)) {
|
||||
return (
|
||||
positiveInt(layer.generationConfig.candidateCount) ??
|
||||
positiveInt(layer.generationConfig.candidate_count)
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function featureLayers(body: unknown): Array<Record<string, unknown>> {
|
||||
const top = isPlainObject(body) ? body : null;
|
||||
const wrapped = top && isPlainObject(top.request) ? top.request : null;
|
||||
const layers: Array<Record<string, unknown>> = [];
|
||||
if (top) layers.push(top);
|
||||
if (wrapped) layers.push(wrapped);
|
||||
return layers;
|
||||
}
|
||||
|
||||
function absorbLayer(draft: FeatureDraft, layer: Record<string, unknown>): void {
|
||||
if (draft.messageCount === 0) {
|
||||
draft.messageCount = countMessages(layer);
|
||||
}
|
||||
if (draft.requestedFanout == null) {
|
||||
draft.requestedFanout = readFanout(layer);
|
||||
}
|
||||
if (draft.streaming == null && "stream" in layer) {
|
||||
draft.streaming = layer.stream === true;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveStreaming(
|
||||
draftStreaming: boolean | null,
|
||||
context?: AdmissionFeatureExtractionContext
|
||||
): boolean {
|
||||
if (context && "streaming" in context && context.streaming !== undefined) {
|
||||
return context.streaming === true;
|
||||
}
|
||||
return draftStreaming ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspect top-level fields and one known wrapper (`request`) only.
|
||||
* Prefer the first non-empty match for each feature family.
|
||||
*/
|
||||
export function extractAdmissionCostFeatures(
|
||||
body: unknown,
|
||||
context?: AdmissionFeatureExtractionContext
|
||||
): AdmissionCostFeatures {
|
||||
const bodyBytes = estimateSizeFast(body);
|
||||
const layers = featureLayers(body);
|
||||
const draft: FeatureDraft = {
|
||||
messageCount: 0,
|
||||
toolCount: countTools(layers),
|
||||
requestedFanout: null,
|
||||
streaming: null,
|
||||
};
|
||||
for (const layer of layers) {
|
||||
absorbLayer(draft, layer);
|
||||
}
|
||||
|
||||
// Conservative token estimate from already-measured body size (no re-walk/stringify).
|
||||
const estimatedInputTokens =
|
||||
bodyBytes > 0 ? Math.min(Number.MAX_SAFE_INTEGER, Math.ceil(bodyBytes / 4)) : 0;
|
||||
|
||||
return {
|
||||
bodyBytes,
|
||||
estimatedInputTokens,
|
||||
messageCount: draft.messageCount,
|
||||
toolCount: draft.toolCount,
|
||||
requestedFanout: draft.requestedFanout ?? 1,
|
||||
streaming: resolveStreaming(draft.streaming, context),
|
||||
};
|
||||
}
|
||||
614
open-sse/services/admission/runtime.ts
Normal file
614
open-sse/services/admission/runtime.ts
Normal file
@@ -0,0 +1,614 @@
|
||||
/**
|
||||
* Process-local adaptive admission runtime facade around the pure controller.
|
||||
* No HTTP route wiring — suitable for later shared handleChat integration.
|
||||
*/
|
||||
|
||||
import { AdaptiveAdmissionController } from "./controller.ts";
|
||||
import { validateConfig } from "./config.ts";
|
||||
import { extractAdmissionCostFeatures } from "./requestFeatures.ts";
|
||||
import {
|
||||
type AdaptiveAdmissionConfig,
|
||||
type AdmissionAcquireResult,
|
||||
type AdmissionClock,
|
||||
type AdmissionLease,
|
||||
type AdmissionMode,
|
||||
type AdmissionPressure,
|
||||
type AdmissionRejectCode,
|
||||
type AdmissionReleaseOutcome,
|
||||
type AdmissionSnapshot,
|
||||
type ShadowDecision,
|
||||
} from "./types.ts";
|
||||
import { buildErrorBody } from "../../utils/error.ts";
|
||||
import { CORS_HEADERS } from "../../utils/cors.ts";
|
||||
import {
|
||||
checkResourcePressureGuard,
|
||||
getResourcePressureObservation,
|
||||
type ResourcePressureGuardResult,
|
||||
type ResourcePressureObservation,
|
||||
} from "../../utils/resourcePressure.ts";
|
||||
import type { PressureReason, PressureSeverity } from "../../utils/resourcePressurePolicy.ts";
|
||||
|
||||
export { extractAdmissionCostFeatures } from "./requestFeatures.ts";
|
||||
|
||||
export const DEFAULT_ADAPTIVE_ADMISSION_CONFIG: Readonly<AdaptiveAdmissionConfig> = Object.freeze({
|
||||
mode: "shadow",
|
||||
minLimit: 8,
|
||||
initialLimit: 64,
|
||||
maxLimit: 1000,
|
||||
maxQueueCount: 128,
|
||||
maxQueueCost: 2000,
|
||||
defaultMaxWaitMs: 5_000,
|
||||
windowMs: 1_000,
|
||||
});
|
||||
|
||||
const RUNTIME_STORE_KEY = Symbol.for("omniroute.adaptiveAdmission.runtime");
|
||||
|
||||
type RuntimeStore = {
|
||||
runtime: AdaptiveAdmissionRuntime | null;
|
||||
};
|
||||
|
||||
type GlobalWithRuntimeStore = typeof globalThis & {
|
||||
[RUNTIME_STORE_KEY]?: RuntimeStore;
|
||||
};
|
||||
|
||||
function getRuntimeStore(): RuntimeStore {
|
||||
const globalWithStore = globalThis as GlobalWithRuntimeStore;
|
||||
let store = globalWithStore[RUNTIME_STORE_KEY];
|
||||
if (!store) {
|
||||
store = { runtime: null };
|
||||
globalWithStore[RUNTIME_STORE_KEY] = store;
|
||||
}
|
||||
return store;
|
||||
}
|
||||
|
||||
const ENV_KEYS = {
|
||||
mode: "ADAPTIVE_ADMISSION_MODE",
|
||||
minLimit: "ADAPTIVE_ADMISSION_MIN_LIMIT",
|
||||
initialLimit: "ADAPTIVE_ADMISSION_INITIAL_LIMIT",
|
||||
maxLimit: "ADAPTIVE_ADMISSION_MAX_LIMIT",
|
||||
maxQueueCount: "ADAPTIVE_ADMISSION_MAX_QUEUE_COUNT",
|
||||
maxQueueCost: "ADAPTIVE_ADMISSION_MAX_QUEUE_COST",
|
||||
defaultMaxWaitMs: "ADAPTIVE_ADMISSION_MAX_WAIT_MS",
|
||||
windowMs: "ADAPTIVE_ADMISSION_WINDOW_MS",
|
||||
} as const;
|
||||
|
||||
function parsePositiveSafeInt(name: string, raw: string): number {
|
||||
if (!/^[0-9]+$/.test(raw)) {
|
||||
throw new RangeError(`${name} must be a positive safe integer`);
|
||||
}
|
||||
const value = Number(raw);
|
||||
if (!Number.isSafeInteger(value) || value <= 0) {
|
||||
throw new RangeError(`${name} must be a positive safe integer`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Strict env → config resolver. Throws clear config errors for direct callers. */
|
||||
export function resolveAdaptiveAdmissionConfigFromEnv(
|
||||
env: NodeJS.ProcessEnv | Record<string, string | undefined> = process.env
|
||||
): AdaptiveAdmissionConfig {
|
||||
const cfg: AdaptiveAdmissionConfig = { ...DEFAULT_ADAPTIVE_ADMISSION_CONFIG };
|
||||
|
||||
const modeRaw = env[ENV_KEYS.mode];
|
||||
if (modeRaw !== undefined && modeRaw !== "") {
|
||||
if (modeRaw !== "off" && modeRaw !== "shadow" && modeRaw !== "enforce") {
|
||||
throw new RangeError(`${ENV_KEYS.mode} must be off|shadow|enforce`);
|
||||
}
|
||||
cfg.mode = modeRaw;
|
||||
}
|
||||
|
||||
// Numeric env keys only — typed assignment without index-signature cast (TS2352).
|
||||
type EnvIntField = Exclude<keyof typeof ENV_KEYS, "mode">;
|
||||
const intFields = [
|
||||
"minLimit",
|
||||
"initialLimit",
|
||||
"maxLimit",
|
||||
"maxQueueCount",
|
||||
"maxQueueCost",
|
||||
"defaultMaxWaitMs",
|
||||
"windowMs",
|
||||
] as const satisfies ReadonlyArray<EnvIntField>;
|
||||
for (const field of intFields) {
|
||||
const envName = ENV_KEYS[field];
|
||||
const raw = env[envName];
|
||||
if (raw === undefined || raw === "") continue;
|
||||
cfg[field] = parsePositiveSafeInt(envName, raw);
|
||||
}
|
||||
|
||||
// Shared pure validation — accept exact documented maxima, reject core-invalid configs.
|
||||
validateConfig(cfg);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
export type AdaptiveAdmissionAcquireInput = {
|
||||
/** Opaque fairness key; never exposed in snapshots or client errors. */
|
||||
tenantKey: string;
|
||||
/** Already-parsed request body — must not be re-read or stringified for cost. */
|
||||
body: unknown;
|
||||
signal?: AbortSignal;
|
||||
maxWaitMs?: number;
|
||||
/** Authoritative streaming class; wins body stream inference when set. */
|
||||
streaming?: boolean;
|
||||
};
|
||||
|
||||
export type AdaptiveAdmissionAdmitted = {
|
||||
status: "admitted";
|
||||
mode: AdmissionMode;
|
||||
lease: AdmissionLease;
|
||||
admittedAtMs: number;
|
||||
shadowDecision?: ShadowDecision;
|
||||
};
|
||||
|
||||
export type AdaptiveAdmissionRejected = {
|
||||
status: "rejected";
|
||||
code: string;
|
||||
response: Response;
|
||||
};
|
||||
|
||||
export type AdaptiveAdmissionAcquireResult = AdaptiveAdmissionAdmitted | AdaptiveAdmissionRejected;
|
||||
|
||||
export type AdaptiveAdmissionPublicSnapshot = AdmissionSnapshot & {
|
||||
resourceSeverity: PressureSeverity;
|
||||
resourceReason: PressureReason;
|
||||
resourceObservedAtMs: number;
|
||||
pressureGuardRejectCount: number;
|
||||
};
|
||||
|
||||
export type AdaptiveAdmissionLifecycleOptions = {
|
||||
admittedAtMs: number;
|
||||
signal?: AbortSignal;
|
||||
nowMs?: () => number;
|
||||
};
|
||||
|
||||
export type AdaptiveAdmissionRuntimeOptions = {
|
||||
config?: AdaptiveAdmissionConfig;
|
||||
env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
|
||||
clock?: Partial<AdmissionClock>;
|
||||
checkResourcePressure?: () => ResourcePressureGuardResult | null;
|
||||
getResourcePressureObservation?: () => ResourcePressureObservation;
|
||||
/** Test seam: observe pressure values fed into the controller after dedupe. */
|
||||
onPressureObserved?: (pressure: AdmissionPressure) => void;
|
||||
warn?: (message: string) => void;
|
||||
nowMs?: () => number;
|
||||
};
|
||||
|
||||
/** Non-success release outcomes callers must choose explicitly for handler failures. */
|
||||
export type AdaptiveAdmissionFailureOutcome = Exclude<AdmissionReleaseOutcome, "success">;
|
||||
|
||||
export type AdaptiveAdmissionRuntime = {
|
||||
acquire(input: AdaptiveAdmissionAcquireInput): Promise<AdaptiveAdmissionAcquireResult>;
|
||||
snapshot(): AdaptiveAdmissionPublicSnapshot;
|
||||
dispose(): void;
|
||||
/**
|
||||
* Release an admitted lease after a handler failure before any HTTP response exists.
|
||||
* Callers must supply the concrete non-success outcome — never defaults to local_reject.
|
||||
*/
|
||||
releaseHandlerFailure(
|
||||
lease: AdmissionLease,
|
||||
outcome: AdaptiveAdmissionFailureOutcome,
|
||||
options?: { admittedAtMs?: number; nowMs?: () => number }
|
||||
): void;
|
||||
attachResponseLifecycle(
|
||||
response: Response,
|
||||
lease: AdmissionLease,
|
||||
options: AdaptiveAdmissionLifecycleOptions
|
||||
): Response;
|
||||
};
|
||||
|
||||
type RejectHttpMapping = {
|
||||
status: number;
|
||||
code: string;
|
||||
message: string;
|
||||
retryAfter?: string;
|
||||
};
|
||||
|
||||
const REJECT_MAP: Record<AdmissionRejectCode, RejectHttpMapping> = {
|
||||
ADMISSION_ABORTED: {
|
||||
status: 499,
|
||||
code: "admission_aborted",
|
||||
message: "Request aborted",
|
||||
},
|
||||
ADMISSION_OVERSIZED: {
|
||||
status: 503,
|
||||
code: "admission_oversized",
|
||||
message: "Request too large for current capacity",
|
||||
},
|
||||
ADMISSION_QUEUE_FULL: {
|
||||
status: 503,
|
||||
code: "admission_queue_full",
|
||||
message: "Service temporarily unavailable",
|
||||
retryAfter: "1",
|
||||
},
|
||||
ADMISSION_DEADLINE: {
|
||||
status: 503,
|
||||
code: "admission_deadline",
|
||||
message: "Service temporarily unavailable",
|
||||
retryAfter: "1",
|
||||
},
|
||||
ADMISSION_SHUTDOWN: {
|
||||
status: 503,
|
||||
code: "admission_shutdown",
|
||||
message: "Service temporarily unavailable",
|
||||
},
|
||||
ADMISSION_UNAVAILABLE: {
|
||||
status: 503,
|
||||
code: "admission_unavailable",
|
||||
message: "Service temporarily unavailable",
|
||||
retryAfter: "1",
|
||||
},
|
||||
};
|
||||
|
||||
function isAdmissionRejectError(
|
||||
err: unknown
|
||||
): err is { code: AdmissionRejectCode; name: string; message: string } {
|
||||
return (
|
||||
!!err &&
|
||||
typeof err === "object" &&
|
||||
(err as { name?: string }).name === "AdmissionRejectError" &&
|
||||
typeof (err as { code?: unknown }).code === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function buildAdmissionRejectResponse(code: AdmissionRejectCode): AdaptiveAdmissionRejected {
|
||||
const mapping = REJECT_MAP[code] ?? REJECT_MAP.ADMISSION_UNAVAILABLE;
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
...CORS_HEADERS,
|
||||
};
|
||||
if (mapping.retryAfter) headers["Retry-After"] = mapping.retryAfter;
|
||||
const body = buildErrorBody(mapping.status, mapping.message, undefined, {
|
||||
type: mapping.status === 499 ? "client_disconnected" : "server_error",
|
||||
code: mapping.code,
|
||||
});
|
||||
return {
|
||||
status: "rejected",
|
||||
code: mapping.code,
|
||||
response: new Response(JSON.stringify(body), {
|
||||
status: mapping.status,
|
||||
headers,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function observationIdentity(state: ResourcePressureObservation["state"]): string {
|
||||
return `${state.observedAtMs}|${state.severity}|${state.reason}`;
|
||||
}
|
||||
|
||||
function toAdmissionPressure(severity: PressureSeverity): AdmissionPressure {
|
||||
if (severity === "critical") return "critical";
|
||||
if (severity === "high") return "high";
|
||||
return "normal";
|
||||
}
|
||||
|
||||
function isSseResponse(response: Response): boolean {
|
||||
const contentType = response.headers.get("content-type") ?? "";
|
||||
return contentType.toLowerCase().includes("text/event-stream");
|
||||
}
|
||||
|
||||
function releaseOnce(
|
||||
lease: AdmissionLease,
|
||||
outcome: AdmissionReleaseOutcome,
|
||||
admittedAtMs: number | undefined,
|
||||
nowMs: () => number
|
||||
): void {
|
||||
if (lease.released) return;
|
||||
const latencyMs = admittedAtMs === undefined ? undefined : Math.max(0, nowMs() - admittedAtMs);
|
||||
lease.release(outcome, latencyMs === undefined ? undefined : { latencyMs });
|
||||
}
|
||||
|
||||
/**
|
||||
* Map HTTP status (+ optional request signal) to admission release outcome.
|
||||
* Cancellation always wins over status classification.
|
||||
*/
|
||||
function classifyHttpOutcome(status: number, signal?: AbortSignal): AdmissionReleaseOutcome {
|
||||
if (signal?.aborted || status === 499) return "cancelled";
|
||||
if (status === 408 || status === 504) return "timeout";
|
||||
if (status >= 500) return "upstream_error";
|
||||
if (status >= 400) return "local_reject";
|
||||
// 2xx / 3xx (and rare 1xx) complete successfully from admission's perspective.
|
||||
return "success";
|
||||
}
|
||||
|
||||
class AdaptiveAdmissionRuntimeImpl implements AdaptiveAdmissionRuntime {
|
||||
private readonly controller: AdaptiveAdmissionController;
|
||||
private readonly checkResourcePressure: () => ResourcePressureGuardResult | null;
|
||||
private readonly getResourcePressureObservation: () => ResourcePressureObservation;
|
||||
private readonly onPressureObserved?: (pressure: AdmissionPressure) => void;
|
||||
private readonly nowMs: () => number;
|
||||
private lastObservationKey: string | null = null;
|
||||
private lastResource: {
|
||||
severity: PressureSeverity;
|
||||
reason: PressureReason;
|
||||
observedAtMs: number;
|
||||
} = { severity: "normal", reason: "none", observedAtMs: 0 };
|
||||
private pressureGuardRejectCount = 0;
|
||||
private disposed = false;
|
||||
|
||||
constructor(options: AdaptiveAdmissionRuntimeOptions, config: AdaptiveAdmissionConfig) {
|
||||
this.controller = new AdaptiveAdmissionController(config, options.clock);
|
||||
this.checkResourcePressure = options.checkResourcePressure ?? checkResourcePressureGuard;
|
||||
this.getResourcePressureObservation =
|
||||
options.getResourcePressureObservation ?? getResourcePressureObservation;
|
||||
this.onPressureObserved = options.onPressureObserved;
|
||||
this.nowMs = options.nowMs ?? options.clock?.now ?? (() => Date.now());
|
||||
}
|
||||
|
||||
async acquire(input: AdaptiveAdmissionAcquireInput): Promise<AdaptiveAdmissionAcquireResult> {
|
||||
if (this.disposed) {
|
||||
return buildAdmissionRejectResponse("ADMISSION_SHUTDOWN");
|
||||
}
|
||||
|
||||
// Independent safety fuse first — never acquire provider work on critical guard.
|
||||
// Still feed pressure observations so the controller learns from critical samples.
|
||||
let guard: ResourcePressureGuardResult | null = null;
|
||||
try {
|
||||
guard = this.checkResourcePressure();
|
||||
} catch {
|
||||
// Fail open on sampling/check failures.
|
||||
}
|
||||
|
||||
this.feedFreshPressureObservation();
|
||||
|
||||
if (guard) {
|
||||
this.pressureGuardRejectCount += 1;
|
||||
return {
|
||||
status: "rejected",
|
||||
code: "resource_pressure",
|
||||
response: guard.response,
|
||||
};
|
||||
}
|
||||
|
||||
const features = extractAdmissionCostFeatures(
|
||||
input.body,
|
||||
input.streaming === undefined ? undefined : { streaming: input.streaming }
|
||||
);
|
||||
let result: AdmissionAcquireResult;
|
||||
try {
|
||||
result = await this.controller.acquire({
|
||||
tenantKey: input.tenantKey,
|
||||
features,
|
||||
signal: input.signal,
|
||||
maxWaitMs: input.maxWaitMs,
|
||||
});
|
||||
} catch (err) {
|
||||
if (isAdmissionRejectError(err)) {
|
||||
return buildAdmissionRejectResponse(err.code);
|
||||
}
|
||||
return buildAdmissionRejectResponse("ADMISSION_UNAVAILABLE");
|
||||
}
|
||||
|
||||
if (result.status === "rejected") {
|
||||
return buildAdmissionRejectResponse(result.code);
|
||||
}
|
||||
|
||||
if (result.status === "queued") {
|
||||
try {
|
||||
const admitted = await result.promise;
|
||||
return {
|
||||
status: "admitted",
|
||||
mode: this.controller.snapshot().mode,
|
||||
lease: admitted.lease,
|
||||
admittedAtMs: this.nowMs(),
|
||||
shadowDecision: admitted.shadowDecision,
|
||||
};
|
||||
} catch (err) {
|
||||
if (isAdmissionRejectError(err)) {
|
||||
return buildAdmissionRejectResponse(err.code);
|
||||
}
|
||||
return buildAdmissionRejectResponse("ADMISSION_UNAVAILABLE");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: "admitted",
|
||||
mode: this.controller.snapshot().mode,
|
||||
lease: result.lease,
|
||||
admittedAtMs: this.nowMs(),
|
||||
shadowDecision: result.shadowDecision,
|
||||
};
|
||||
}
|
||||
|
||||
snapshot(): AdaptiveAdmissionPublicSnapshot {
|
||||
const core = this.controller.snapshot();
|
||||
return {
|
||||
...core,
|
||||
resourceSeverity: this.lastResource.severity,
|
||||
resourceReason: this.lastResource.reason,
|
||||
resourceObservedAtMs: this.lastResource.observedAtMs,
|
||||
pressureGuardRejectCount: this.pressureGuardRejectCount,
|
||||
};
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.disposed) return;
|
||||
this.disposed = true;
|
||||
this.controller.shutdown();
|
||||
}
|
||||
|
||||
releaseHandlerFailure(
|
||||
lease: AdmissionLease,
|
||||
outcome: AdaptiveAdmissionFailureOutcome,
|
||||
options?: { admittedAtMs?: number; nowMs?: () => number }
|
||||
): void {
|
||||
releaseOnce(lease, outcome, options?.admittedAtMs, options?.nowMs ?? this.nowMs);
|
||||
}
|
||||
|
||||
attachResponseLifecycle(
|
||||
response: Response,
|
||||
lease: AdmissionLease,
|
||||
options: AdaptiveAdmissionLifecycleOptions
|
||||
): Response {
|
||||
const nowMs = options.nowMs ?? this.nowMs;
|
||||
const admittedAtMs = options.admittedAtMs;
|
||||
|
||||
if (!response.body || !isSseResponse(response)) {
|
||||
releaseOnce(lease, classifyHttpOutcome(response.status, options.signal), admittedAtMs, nowMs);
|
||||
return response;
|
||||
}
|
||||
|
||||
const upstream = response.body;
|
||||
const reader = upstream.getReader();
|
||||
let settled = false;
|
||||
let readerCancelled = false;
|
||||
|
||||
const settle = (outcome: AdmissionReleaseOutcome): void => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
releaseOnce(lease, outcome, admittedAtMs, nowMs);
|
||||
};
|
||||
|
||||
const cancelReader = (reason?: unknown): void => {
|
||||
if (readerCancelled) return;
|
||||
readerCancelled = true;
|
||||
void reader.cancel(reason).catch(() => {
|
||||
/* ignore cancel races */
|
||||
});
|
||||
};
|
||||
|
||||
const onAbort = (): void => {
|
||||
cancelReader(options.signal?.reason);
|
||||
settle("cancelled");
|
||||
};
|
||||
|
||||
if (options.signal) {
|
||||
if (options.signal.aborted) {
|
||||
onAbort();
|
||||
} else {
|
||||
options.signal.addEventListener("abort", onAbort, { once: true });
|
||||
}
|
||||
}
|
||||
|
||||
const detachAbort = (): void => {
|
||||
options.signal?.removeEventListener("abort", onAbort);
|
||||
};
|
||||
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
async pull(controller) {
|
||||
if (settled) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
detachAbort();
|
||||
settle(classifyHttpOutcome(response.status, options.signal));
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
controller.enqueue(value);
|
||||
} catch (err) {
|
||||
detachAbort();
|
||||
settle(options.signal?.aborted ? "cancelled" : "upstream_error");
|
||||
controller.error(err);
|
||||
}
|
||||
},
|
||||
cancel(reason) {
|
||||
detachAbort();
|
||||
cancelReader(reason);
|
||||
settle("cancelled");
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
});
|
||||
}
|
||||
|
||||
private feedFreshPressureObservation(): void {
|
||||
try {
|
||||
const observation = this.getResourcePressureObservation();
|
||||
const state = observation.state;
|
||||
this.lastResource = {
|
||||
severity: state.severity,
|
||||
reason: state.reason,
|
||||
observedAtMs: state.observedAtMs,
|
||||
};
|
||||
const key = observationIdentity(state);
|
||||
if (state.observedAtMs <= 0) return;
|
||||
if (key === this.lastObservationKey) return;
|
||||
this.lastObservationKey = key;
|
||||
const pressure = toAdmissionPressure(state.severity);
|
||||
this.controller.observePressure(pressure);
|
||||
this.onPressureObserved?.(pressure);
|
||||
} catch {
|
||||
// Fail open.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createRuntimeFromResolvedConfig(
|
||||
options: AdaptiveAdmissionRuntimeOptions,
|
||||
config: AdaptiveAdmissionConfig
|
||||
): AdaptiveAdmissionRuntime {
|
||||
return new AdaptiveAdmissionRuntimeImpl(options, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an injected adaptive-admission runtime for tests or process use.
|
||||
* Invalid explicit `config` still throws (direct callers want fail-fast).
|
||||
*/
|
||||
export function createAdaptiveAdmissionRuntime(
|
||||
options: AdaptiveAdmissionRuntimeOptions = {}
|
||||
): AdaptiveAdmissionRuntime {
|
||||
const config =
|
||||
options.config ??
|
||||
(options.env
|
||||
? resolveAdaptiveAdmissionConfigFromEnv(options.env)
|
||||
: { ...DEFAULT_ADAPTIVE_ADMISSION_CONFIG });
|
||||
return createRuntimeFromResolvedConfig(options, config);
|
||||
}
|
||||
|
||||
function warnInvalidDefaultConfig(warn: ((message: string) => void) | undefined): void {
|
||||
const message =
|
||||
"[adaptiveAdmission] invalid environment configuration; using default shadow admission settings";
|
||||
if (warn) {
|
||||
warn(message);
|
||||
return;
|
||||
}
|
||||
console.warn(message);
|
||||
}
|
||||
|
||||
function createDefaultProcessRuntime(
|
||||
options: AdaptiveAdmissionRuntimeOptions = {}
|
||||
): AdaptiveAdmissionRuntime {
|
||||
const warn = options.warn;
|
||||
try {
|
||||
const config =
|
||||
options.config ?? resolveAdaptiveAdmissionConfigFromEnv(options.env ?? process.env);
|
||||
return createRuntimeFromResolvedConfig(options, config);
|
||||
} catch {
|
||||
warnInvalidDefaultConfig(warn);
|
||||
return createRuntimeFromResolvedConfig(options, {
|
||||
...DEFAULT_ADAPTIVE_ADMISSION_CONFIG,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Call-time process-global runtime (HMR-safe via globalThis symbol store). */
|
||||
export function getAdaptiveAdmissionRuntime(): AdaptiveAdmissionRuntime {
|
||||
const store = getRuntimeStore();
|
||||
if (!store.runtime) {
|
||||
store.runtime = createDefaultProcessRuntime();
|
||||
}
|
||||
return store.runtime;
|
||||
}
|
||||
|
||||
/** Dispose previous controller and replace the process-global runtime. */
|
||||
export function reloadAdaptiveAdmissionRuntime(
|
||||
options: AdaptiveAdmissionRuntimeOptions = {}
|
||||
): AdaptiveAdmissionRuntime {
|
||||
const store = getRuntimeStore();
|
||||
store.runtime?.dispose();
|
||||
store.runtime = createDefaultProcessRuntime(options);
|
||||
return store.runtime;
|
||||
}
|
||||
|
||||
/** Test isolation: dispose and clear the process-global runtime slot. */
|
||||
export function resetAdaptiveAdmissionRuntimeForTests(): void {
|
||||
const store = getRuntimeStore();
|
||||
store.runtime?.dispose();
|
||||
store.runtime = null;
|
||||
}
|
||||
171
open-sse/services/admission/types.ts
Normal file
171
open-sse/services/admission/types.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Pure weighted adaptive admission-control types.
|
||||
* No route/settings wiring — dependency-injected controller seam only.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Upper bound for adaptation windows and wait deadlines that participate in
|
||||
* cost×time products (utilization integrals, deadline offsets).
|
||||
* 24h is far beyond practical control windows while keeping the product domain exact.
|
||||
*/
|
||||
export const MAX_ADMISSION_WINDOW_MS = 86_400_000;
|
||||
|
||||
/**
|
||||
* Upper bound for every validated cost, limit, and queue-cost quantum.
|
||||
* Derived so `MAX_ADMISSION_COST_OR_LIMIT * MAX_ADMISSION_WINDOW_MS` remains a
|
||||
* safe integer: a full window at the maximum limit integrates to utilization 1.0
|
||||
* without saturating or rounding Number arithmetic.
|
||||
*/
|
||||
export const MAX_ADMISSION_COST_OR_LIMIT = Math.floor(
|
||||
Number.MAX_SAFE_INTEGER / MAX_ADMISSION_WINDOW_MS
|
||||
);
|
||||
|
||||
export type AdmissionMode = "off" | "shadow" | "enforce";
|
||||
|
||||
export type AdmissionPressure = "normal" | "high" | "critical";
|
||||
|
||||
/** Local outcome categories. Upstream business errors must not collapse capacity. */
|
||||
export type AdmissionReleaseOutcome =
|
||||
"success" | "upstream_error" | "timeout" | "local_reject" | "cancelled";
|
||||
|
||||
export type AdmissionRejectCode =
|
||||
| "ADMISSION_OVERSIZED"
|
||||
| "ADMISSION_QUEUE_FULL"
|
||||
| "ADMISSION_DEADLINE"
|
||||
| "ADMISSION_ABORTED"
|
||||
| "ADMISSION_SHUTDOWN"
|
||||
| "ADMISSION_UNAVAILABLE";
|
||||
|
||||
export type ShadowDecision = "would-admit" | "would-queue" | "would-reject";
|
||||
|
||||
export interface AdmissionCostFeatures {
|
||||
bodyBytes?: number | null;
|
||||
estimatedInputTokens?: number | null;
|
||||
messageCount?: number | null;
|
||||
toolCount?: number | null;
|
||||
requestedFanout?: number | null;
|
||||
streaming?: boolean | null;
|
||||
}
|
||||
|
||||
export interface AdmissionCostConfig {
|
||||
baseCost: number;
|
||||
bodyBytesPerUnit: number;
|
||||
tokensPerUnit: number;
|
||||
messagesPerUnit: number;
|
||||
toolsPerUnit: number;
|
||||
fanoutPerUnit: number;
|
||||
streamingClassCost: number;
|
||||
nonStreamingClassCost: number;
|
||||
maxRequestCost: number;
|
||||
}
|
||||
|
||||
export interface AdaptiveAdmissionConfig {
|
||||
mode?: AdmissionMode;
|
||||
minLimit: number;
|
||||
maxLimit: number;
|
||||
initialLimit: number;
|
||||
maxQueueCount: number;
|
||||
maxQueueCost: number;
|
||||
defaultMaxWaitMs?: number;
|
||||
windowMs?: number;
|
||||
shortLatencyAlpha?: number;
|
||||
longLatencyAlpha?: number;
|
||||
increaseStep?: number;
|
||||
decreaseFactor?: number;
|
||||
criticalDecreaseFactor?: number;
|
||||
highUtilizationThreshold?: number;
|
||||
lowUtilizationThreshold?: number;
|
||||
latencyGradientThreshold?: number;
|
||||
maxIncreasePerWindow?: number;
|
||||
/** Optional cost quanta override used only when callers pass features instead of cost. */
|
||||
cost?: Partial<AdmissionCostConfig>;
|
||||
}
|
||||
|
||||
export interface AdmissionRequest {
|
||||
/** Positive integer cost units. If omitted, `features` + cost config are used. */
|
||||
cost?: number;
|
||||
features?: AdmissionCostFeatures;
|
||||
/** Opaque fairness key; never exposed in snapshots. */
|
||||
tenantKey?: string;
|
||||
maxWaitMs?: number;
|
||||
signal?: AbortSignal;
|
||||
pressure?: AdmissionPressure;
|
||||
}
|
||||
|
||||
export interface AdmissionReleaseMeta {
|
||||
latencyMs?: number;
|
||||
pressure?: AdmissionPressure;
|
||||
}
|
||||
|
||||
export interface AdmissionLease {
|
||||
readonly id: string;
|
||||
readonly cost: number;
|
||||
readonly released: boolean;
|
||||
release(outcome?: AdmissionReleaseOutcome, meta?: AdmissionReleaseMeta): void;
|
||||
}
|
||||
|
||||
export interface AdmissionAdmitted {
|
||||
status: "admitted";
|
||||
lease: AdmissionLease;
|
||||
shadowDecision?: ShadowDecision;
|
||||
}
|
||||
|
||||
export interface AdmissionQueued {
|
||||
status: "queued";
|
||||
promise: Promise<AdmissionAdmitted>;
|
||||
}
|
||||
|
||||
export interface AdmissionRejected {
|
||||
status: "rejected";
|
||||
code: AdmissionRejectCode;
|
||||
message: string;
|
||||
shadowDecision?: ShadowDecision;
|
||||
}
|
||||
|
||||
export type AdmissionAcquireResult = AdmissionAdmitted | AdmissionQueued | AdmissionRejected;
|
||||
|
||||
export interface AdmissionSnapshot {
|
||||
mode: AdmissionMode;
|
||||
currentLimit: number;
|
||||
minLimit: number;
|
||||
maxLimit: number;
|
||||
activeCost: number;
|
||||
activeCount: number;
|
||||
queuedCost: number;
|
||||
queuedCount: number;
|
||||
virtualActiveCost: number;
|
||||
virtualActiveCount: number;
|
||||
virtualQueuedCost: number;
|
||||
virtualQueuedCount: number;
|
||||
admittedCount: number;
|
||||
rejectedCount: number;
|
||||
wouldAdmitCount: number;
|
||||
wouldQueueCount: number;
|
||||
wouldRejectCount: number;
|
||||
shortLatencyEwma: number;
|
||||
longLatencyEwma: number;
|
||||
utilization: number;
|
||||
pressure: AdmissionPressure;
|
||||
shutdown: boolean;
|
||||
}
|
||||
|
||||
export interface AdmissionClock {
|
||||
now: () => number;
|
||||
setTimer: (fn: () => void, delayMs: number) => unknown;
|
||||
clearTimer: (id: unknown) => void;
|
||||
}
|
||||
|
||||
export interface AdmissionRejectError extends Error {
|
||||
code: AdmissionRejectCode;
|
||||
name: "AdmissionRejectError";
|
||||
}
|
||||
|
||||
export function createAdmissionRejectError(
|
||||
code: AdmissionRejectCode,
|
||||
message: string
|
||||
): AdmissionRejectError {
|
||||
const err = new Error(message) as AdmissionRejectError;
|
||||
err.name = "AdmissionRejectError";
|
||||
err.code = code;
|
||||
return err;
|
||||
}
|
||||
@@ -194,6 +194,8 @@ const REQUEST_SCOPED_UPSTREAM_ERROR_CODES = new Set([
|
||||
"context_length_exceeded",
|
||||
"upstream_empty_response",
|
||||
"upstream_response_failed",
|
||||
// Local combo per-target timer (targetTimeoutRunner) — not a connection health signal.
|
||||
"combo_target_timeout",
|
||||
]);
|
||||
|
||||
/** Request/model-specific failures must not poison provider-wide resilience state. */
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
/**
|
||||
* Wrap a single-model dispatch with a per-target timeout that aborts and falls back.
|
||||
*
|
||||
* Verbatim extraction of handleComboChat's `handleSingleModelWithTimeout` closure
|
||||
* (combo.ts). Behavior is byte-identical; the only change is that the closed-over locals
|
||||
* (`handleSingleModel`, `comboTargetTimeoutMs`, `log`) became explicit factory params.
|
||||
* Extracted from handleComboChat's `handleSingleModelWithTimeout` closure (combo.ts).
|
||||
* A locally expired timer aborts that target and returns a typed 504 response so the Combo
|
||||
* can fall back without treating OmniRoute's own deadline as a provider-connection failure.
|
||||
* The per-model abort signal still comes from the target (`target.modelAbortSignal`), so
|
||||
* the outer request signal is intentionally NOT a dependency here.
|
||||
*
|
||||
* See _tasks/superpowers/plans/2026-07-03-blocoJ-combo-hotpath-decomposition.md (Task 1).
|
||||
*/
|
||||
import { errorResponse } from "../../utils/error.ts";
|
||||
import { buildErrorBody, errorResponse, sanitizeErrorMessage } from "../../utils/error.ts";
|
||||
import type { HandleSingleModel, SingleModelTarget, ComboLogger } from "./types.ts";
|
||||
|
||||
/** Stable internal classification for OmniRoute's own combo per-target timer. */
|
||||
export const COMBO_TARGET_TIMEOUT_CODE = "combo_target_timeout";
|
||||
|
||||
export function buildTargetTimeoutRunner(deps: {
|
||||
handleSingleModel: HandleSingleModel;
|
||||
comboTargetTimeoutMs: number;
|
||||
@@ -44,11 +47,23 @@ export function buildTargetTimeoutRunner(deps: {
|
||||
`Model ${modelStr} exceeded ${comboTargetTimeoutMs}ms timeout — falling back`
|
||||
);
|
||||
timeoutController.abort(new Error("combo-per-model-timeout"));
|
||||
// HTTP 504 (not proprietary 524): this is OmniRoute's own per-target timer.
|
||||
// Typed as combo_target_timeout so request-scoped classification can keep the
|
||||
// connection eligible for fallback instead of treating it like Cloudflare 524
|
||||
// or a genuine upstream gateway timeout.
|
||||
resolve(
|
||||
new Response(JSON.stringify({ error: { message: `Model ${modelStr} timed out` } }), {
|
||||
status: 524,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
new Response(
|
||||
JSON.stringify(
|
||||
buildErrorBody(504, sanitizeErrorMessage(`Model ${modelStr} timed out`), undefined, {
|
||||
type: COMBO_TARGET_TIMEOUT_CODE,
|
||||
code: COMBO_TARGET_TIMEOUT_CODE,
|
||||
})
|
||||
),
|
||||
{
|
||||
status: 504,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
)
|
||||
);
|
||||
}, comboTargetTimeoutMs);
|
||||
});
|
||||
@@ -72,7 +87,7 @@ export function buildTargetTimeoutRunner(deps: {
|
||||
return await Promise.race([
|
||||
handleSingleModel(b, modelStr, targetWithSignal).catch((err) => {
|
||||
if (timedOut) {
|
||||
// Inner call rejected because we aborted it. The synthetic 524 from
|
||||
// Inner call rejected because we aborted it. The synthetic 504 from
|
||||
// timeoutPromise already wins the race; return an empty response so
|
||||
// the loser branch resolves cleanly without leaking err.message.
|
||||
return new Response(null, { status: 599 });
|
||||
|
||||
@@ -61,8 +61,8 @@ export function isComboCooldownWaitEligible(
|
||||
* When the combo is wait-eligible (see isComboCooldownWaitEligible), a single target's
|
||||
* dispatch can legitimately wait out cooldowns for up to `comboCooldownWait.budgetMs`
|
||||
* before it resolves — so the per-target timeout must never be shorter than that budget,
|
||||
* or the wait gets cut off mid-retry and the target times out with a synthetic 524
|
||||
* (open-sse/services/combo/targetTimeoutRunner.ts) instead of completing the wait. This
|
||||
* or the wait gets cut off mid-retry and the target times out with a synthetic 504
|
||||
* (`combo_target_timeout`, open-sse/services/combo/targetTimeoutRunner.ts) instead of completing the wait. This
|
||||
* only raises the *default* floor; an operator's explicit `targetTimeoutMs` on the combo
|
||||
* still wins (see resolveComboTargetTimeoutMs).
|
||||
*/
|
||||
|
||||
@@ -22,15 +22,16 @@ let _config = {
|
||||
// lazily loaded on first access. better-sqlite3 is synchronous, so both the load
|
||||
// and the save stay in the sync hot path without extra startup wiring. tempBans
|
||||
// are intentionally NOT persisted — they are ephemeral, TTL-swept runtime state.
|
||||
//
|
||||
// D2 (#9033): the _loaded one-shot gate was removed so a config persisted by the
|
||||
// dashboard settings route (a separate module instance, since @omniroute/open-sse
|
||||
// is bundled per-entry via transpilePackages) propagates to the proxy runtime
|
||||
// without a restart. A DB failure still degrades to the in-memory defaults, and
|
||||
// tempBans remain in-memory-only as before.
|
||||
const IP_FILTER_NAMESPACE = "ipFilter";
|
||||
const IP_FILTER_KEY = "config";
|
||||
let _loaded = false;
|
||||
|
||||
function ensureLoaded() {
|
||||
if (_loaded) return;
|
||||
// Mark loaded up-front so a DB failure (build phase / cloud / migration not yet
|
||||
// run) degrades to in-memory only instead of retrying on every request.
|
||||
_loaded = true;
|
||||
try {
|
||||
const row = getDbInstance()
|
||||
.prepare("SELECT value FROM key_value WHERE namespace = ? AND key = ?")
|
||||
@@ -235,9 +236,17 @@ export function createIPFilterMiddleware() {
|
||||
|
||||
/**
|
||||
* For Next.js App Router — check IP from request object
|
||||
*
|
||||
* D1 (#9033): accepts an optional trustedPeerIp (resolved from the authenticated
|
||||
* peer stamp, available on direct connections where the proxy runtime has no
|
||||
* socket). When provided, it is checked FIRST before falling through to the
|
||||
* forwarding headers, so a blacklisted IP on a direct connection (no XFF, no
|
||||
* socket) is blocked. When behind a reverse proxy (via-proxy marker set), the
|
||||
* caller passes null so the XFF path continues to work.
|
||||
*/
|
||||
export function checkRequestIP(request) {
|
||||
export function checkRequestIP(request, trustedPeerIp) {
|
||||
const ip =
|
||||
pickFirstValidIp(trustedPeerIp || null) ||
|
||||
pickFirstValidIp(request.headers?.get?.("cf-connecting-ip")) ||
|
||||
pickFirstValidIp(request.headers?.get?.("x-forwarded-for")) ||
|
||||
pickFirstValidIp(request.headers?.get?.("x-real-ip")) ||
|
||||
@@ -329,7 +338,6 @@ function extractClientIP(req) {
|
||||
* Reset config (for testing)
|
||||
*/
|
||||
export function resetIPFilter() {
|
||||
_loaded = false;
|
||||
_config = {
|
||||
enabled: false,
|
||||
mode: "blacklist",
|
||||
|
||||
@@ -1,32 +1,109 @@
|
||||
/**
|
||||
* Fast object-tree size estimator — walks without JSON.stringify.
|
||||
* Safe for circular references (uses WeakSet).
|
||||
* Early-exits at 256KB to avoid wasting CPU on huge payloads.
|
||||
* Fast object-tree size estimator — walks without JSON.stringify / toJSON / clone.
|
||||
* Safe for circular references (WeakSet). Iterative frames only (no recursive call stack).
|
||||
*
|
||||
* Budgets:
|
||||
* - ESTIMATE_SIZE_BYTE_LIMIT (256 KiB): early-exit once counted bytes exceed the limit
|
||||
* - ESTIMATE_SIZE_NODE_BUDGET: max value visits (containers + primitives/elements)
|
||||
*
|
||||
* Arrays are walked by index frame (never pre-push/copy every element reference).
|
||||
* Plain objects yield own enumerable values incrementally (no Object.keys materialization).
|
||||
* Node-budget exhaustion returns a value strictly above 256 KiB so callers fail closed.
|
||||
*/
|
||||
export function estimateSizeFast(value: unknown): number {
|
||||
let bytes = 0;
|
||||
const stack: unknown[] = [value];
|
||||
const seen = new WeakSet();
|
||||
while (stack.length > 0) {
|
||||
const v = stack.pop();
|
||||
if (v === null || v === undefined) continue;
|
||||
if (typeof v === "string") {
|
||||
bytes += v.length;
|
||||
if (bytes > 262144) return bytes;
|
||||
} else if (typeof v === "number") bytes += 8;
|
||||
else if (typeof v === "boolean") bytes += 4;
|
||||
else if (typeof v === "object") {
|
||||
if (seen.has(v as object)) continue;
|
||||
seen.add(v as object);
|
||||
if (Array.isArray(v)) {
|
||||
for (let i = 0; i < v.length; i++) stack.push(v[i]);
|
||||
} else {
|
||||
for (const key in v) {
|
||||
if (Object.prototype.hasOwnProperty.call(v, key)) stack.push((v as Record<string, unknown>)[key]);
|
||||
}
|
||||
|
||||
/** Byte early-exit threshold (256 KiB). */
|
||||
export const ESTIMATE_SIZE_BYTE_LIMIT = 262_144;
|
||||
|
||||
/**
|
||||
* Max value/element visits before fail-closed.
|
||||
* Conservative cap keeps auxiliary stack/WeakSet growth bounded under adversarial input.
|
||||
*/
|
||||
export const ESTIMATE_SIZE_NODE_BUDGET = 16_384;
|
||||
|
||||
type Frame =
|
||||
| { t: "v"; v: unknown }
|
||||
| { t: "a"; a: unknown[]; i: number }
|
||||
| { t: "o"; o: object; it: Iterator<string> };
|
||||
|
||||
function ownEnumerableKeyIterator(obj: object): Iterator<string> {
|
||||
return (function* ownEnumerableKeys() {
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
yield key;
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
/** @returns next byte total, or a value > limit when the limit is exceeded. */
|
||||
function addPrimitiveBytes(bytes: number, v: string | number | boolean): number {
|
||||
if (typeof v === "string") return bytes + v.length;
|
||||
if (typeof v === "number") return bytes + 8;
|
||||
return bytes + 4;
|
||||
}
|
||||
|
||||
function enqueueContainer(stack: Frame[], obj: object, seen: WeakSet<object>): void {
|
||||
if (seen.has(obj)) return;
|
||||
seen.add(obj);
|
||||
if (Array.isArray(obj)) {
|
||||
if (obj.length > 0) stack.push({ t: "a", a: obj, i: 0 });
|
||||
return;
|
||||
}
|
||||
stack.push({ t: "o", o: obj, it: ownEnumerableKeyIterator(obj) });
|
||||
}
|
||||
|
||||
type ValueFrame = Extract<Frame, { t: "v" }>;
|
||||
|
||||
function isValueFrame(frame: Frame): frame is ValueFrame {
|
||||
return frame.t === "v";
|
||||
}
|
||||
|
||||
/** Expand a container frame into the next child value. */
|
||||
function expandContainerFrame(stack: Frame[], frame: Exclude<Frame, ValueFrame>): void {
|
||||
if (frame.t === "a") {
|
||||
if (frame.i >= frame.a.length) return;
|
||||
if (frame.i + 1 < frame.a.length) {
|
||||
stack.push({ t: "a", a: frame.a, i: frame.i + 1 });
|
||||
}
|
||||
stack.push({ t: "v", v: frame.a[frame.i] });
|
||||
return;
|
||||
}
|
||||
const next = frame.it.next();
|
||||
if (next.done) return;
|
||||
stack.push(frame);
|
||||
stack.push({ t: "v", v: (frame.o as Record<string, unknown>)[next.value] });
|
||||
}
|
||||
|
||||
export function estimateSizeFast(value: unknown): number {
|
||||
let bytes = 0;
|
||||
let visitsLeft = ESTIMATE_SIZE_NODE_BUDGET;
|
||||
const seen = new WeakSet<object>();
|
||||
const stack: Frame[] = [{ t: "v", v: value }];
|
||||
|
||||
while (stack.length > 0) {
|
||||
if (visitsLeft <= 0) return ESTIMATE_SIZE_BYTE_LIMIT + 1;
|
||||
|
||||
const frame = stack.pop()!;
|
||||
if (!isValueFrame(frame)) {
|
||||
expandContainerFrame(stack, frame);
|
||||
continue;
|
||||
}
|
||||
|
||||
visitsLeft -= 1;
|
||||
const v = frame.v;
|
||||
if (v === null || v === undefined) continue;
|
||||
|
||||
const ty = typeof v;
|
||||
if (ty === "string" || ty === "number" || ty === "boolean") {
|
||||
bytes = addPrimitiveBytes(bytes, v as string | number | boolean);
|
||||
if (bytes > ESTIMATE_SIZE_BYTE_LIMIT) return bytes;
|
||||
continue;
|
||||
}
|
||||
if (ty === "object") {
|
||||
enqueueContainer(stack, v as object, seen);
|
||||
}
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,11 @@ type PendingToolCall = {
|
||||
|
||||
// Transform OpenAI SSE stream to Ollama JSON lines format
|
||||
export function transformToOllama(response, model) {
|
||||
// Only successful SSE responses belong to the NDJSON transformer. Preserve errors,
|
||||
// bodyless responses, and successful JSON responses without losing status/body/headers.
|
||||
const contentType = String(response.headers?.get?.("content-type") || "").toLowerCase();
|
||||
if (!response.ok || !response.body || !contentType.includes("text/event-stream")) return response;
|
||||
|
||||
let buffer = "";
|
||||
let pendingToolCalls: Record<number, PendingToolCall> = {};
|
||||
const completedToolCalls: PendingToolCall[] = [];
|
||||
|
||||
249
open-sse/utils/resourcePressure.ts
Normal file
249
open-sse/utils/resourcePressure.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import { checkHeapPressureGuard, HEAP_PRESSURE_THRESHOLD_MB } from "./heapPressure.ts";
|
||||
import { buildErrorBody } from "./error.ts";
|
||||
import {
|
||||
createResourcePressureTracker,
|
||||
resolveResourcePressureThresholds,
|
||||
type PressureReason,
|
||||
type ResourcePressureState,
|
||||
type ResourcePressureThresholds,
|
||||
type ResourceSignals,
|
||||
} from "./resourcePressurePolicy.ts";
|
||||
import {
|
||||
sampleResourceSignals,
|
||||
type SampleResourceSignalsDeps,
|
||||
} from "./resourcePressureSampler.ts";
|
||||
|
||||
const MB = 1024 * 1024;
|
||||
const RETRY_AFTER_SECONDS = "5";
|
||||
const PRESSURE_MESSAGE = "Service temporarily unavailable due to resource pressure. Retry shortly.";
|
||||
|
||||
export type ResourcePressureGuardResult = {
|
||||
success: false;
|
||||
status: 503;
|
||||
error: string;
|
||||
response: Response;
|
||||
};
|
||||
|
||||
export type ResourcePressureObservation = {
|
||||
signals: ResourceSignals | null;
|
||||
state: ResourcePressureState;
|
||||
};
|
||||
|
||||
export type ResourcePressureRuntimeOptions = {
|
||||
thresholds?: Partial<ResourcePressureThresholds>;
|
||||
heapThresholdMb?: number | null;
|
||||
immediateHeapUsedMb?: () => number;
|
||||
sample?: () => Promise<ResourceSignals>;
|
||||
nowMs?: () => number;
|
||||
schedule?: (refresh: () => void) => void;
|
||||
staleAfterMs?: number;
|
||||
maxStaleMs?: number;
|
||||
retryAfterMs?: number;
|
||||
samplerDeps?: SampleResourceSignalsDeps;
|
||||
};
|
||||
|
||||
export type ResourcePressureRuntime = {
|
||||
check: () => ResourcePressureGuardResult | null;
|
||||
getObservation: () => ResourcePressureObservation;
|
||||
whenRefreshSettled: () => Promise<void>;
|
||||
dispose: () => void;
|
||||
};
|
||||
|
||||
function emptyState(): ResourcePressureState {
|
||||
return {
|
||||
severity: "normal",
|
||||
reason: "none",
|
||||
elevatedStreak: 0,
|
||||
recoveryStreak: 0,
|
||||
lastTransitionAtMs: 0,
|
||||
observedAtMs: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function requireDuration(name: string, value: number): number {
|
||||
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0 || value > 3_600_000) {
|
||||
throw new RangeError(`${name} must be an integer between 0 and 3600000`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function buildCriticalGuard(reason: PressureReason): ResourcePressureGuardResult {
|
||||
console.warn(
|
||||
`[resourcePressure] critical pressure guard tripped (reason=${reason}); returning 503`
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
status: 503,
|
||||
error: PRESSURE_MESSAGE,
|
||||
response: new Response(
|
||||
JSON.stringify(
|
||||
buildErrorBody(503, PRESSURE_MESSAGE, undefined, {
|
||||
type: "server_error",
|
||||
code: "resource_pressure",
|
||||
})
|
||||
),
|
||||
{
|
||||
status: 503,
|
||||
headers: { "Content-Type": "application/json", "Retry-After": RETRY_AFTER_SECONDS },
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function immediateHeapGuard(
|
||||
heapUsedMb: number,
|
||||
thresholdMb: number | null
|
||||
): ResourcePressureGuardResult | null {
|
||||
if (thresholdMb == null) return null;
|
||||
const guard = checkHeapPressureGuard(heapUsedMb, thresholdMb);
|
||||
if (!guard) return null;
|
||||
return buildCriticalGuard("v8_heap_absolute");
|
||||
}
|
||||
|
||||
export function createResourcePressureRuntime(
|
||||
options: ResourcePressureRuntimeOptions = {}
|
||||
): ResourcePressureRuntime {
|
||||
const heapThresholdMb =
|
||||
options.heapThresholdMb === undefined ? HEAP_PRESSURE_THRESHOLD_MB : options.heapThresholdMb;
|
||||
if (heapThresholdMb !== null && (!Number.isFinite(heapThresholdMb) || heapThresholdMb <= 0)) {
|
||||
throw new RangeError("heapThresholdMb must be positive and finite or null");
|
||||
}
|
||||
const thresholds = resolveResourcePressureThresholds({
|
||||
...options.thresholds,
|
||||
heapAbsoluteThresholdMb:
|
||||
options.thresholds?.heapAbsoluteThresholdMb === undefined
|
||||
? null
|
||||
: options.thresholds.heapAbsoluteThresholdMb,
|
||||
});
|
||||
const staleAfterMs = requireDuration("staleAfterMs", options.staleAfterMs ?? 1_000);
|
||||
const maxStaleMs = requireDuration("maxStaleMs", options.maxStaleMs ?? 30_000);
|
||||
const retryAfterMs = requireDuration("retryAfterMs", options.retryAfterMs ?? 1_000);
|
||||
if (maxStaleMs < staleAfterMs) {
|
||||
throw new RangeError("maxStaleMs must be greater than or equal to staleAfterMs");
|
||||
}
|
||||
|
||||
const nowMs = options.nowMs ?? Date.now;
|
||||
const immediateHeapUsedMb =
|
||||
options.immediateHeapUsedMb ?? (() => process.memoryUsage().heapUsed / MB);
|
||||
const sample = options.sample ?? (() => sampleResourceSignals(options.samplerDeps));
|
||||
const schedule =
|
||||
options.schedule ??
|
||||
((refresh) => {
|
||||
const handle = setImmediate(refresh);
|
||||
handle.unref();
|
||||
});
|
||||
const tracker = createResourcePressureTracker(thresholds);
|
||||
|
||||
let lastSignals: ResourceSignals | null = null;
|
||||
let state = emptyState();
|
||||
let lastRefreshAtMs = Number.NEGATIVE_INFINITY;
|
||||
let nextRefreshAtMs = Number.NEGATIVE_INFINITY;
|
||||
let scheduled = false;
|
||||
let inFlight: Promise<void> | null = null;
|
||||
let disposed = false;
|
||||
|
||||
const refresh = (): void => {
|
||||
if (disposed || inFlight) return;
|
||||
scheduled = false;
|
||||
inFlight = Promise.resolve()
|
||||
.then(sample)
|
||||
.then((signals) => {
|
||||
if (disposed) return;
|
||||
const settledAtMs = nowMs();
|
||||
lastSignals = signals;
|
||||
state = tracker.observe(signals);
|
||||
lastRefreshAtMs = settledAtMs;
|
||||
nextRefreshAtMs = settledAtMs + staleAfterMs;
|
||||
})
|
||||
.catch(() => {
|
||||
if (!disposed) nextRefreshAtMs = nowMs() + retryAfterMs;
|
||||
})
|
||||
.finally(() => {
|
||||
inFlight = null;
|
||||
});
|
||||
};
|
||||
|
||||
const scheduleRefresh = (): void => {
|
||||
if (disposed || scheduled || inFlight) return;
|
||||
scheduled = true;
|
||||
schedule(refresh);
|
||||
};
|
||||
|
||||
return {
|
||||
check() {
|
||||
let heapUsedMb = 0;
|
||||
try {
|
||||
heapUsedMb = immediateHeapUsedMb();
|
||||
} catch {
|
||||
heapUsedMb = 0;
|
||||
}
|
||||
const immediate = immediateHeapGuard(heapUsedMb, heapThresholdMb);
|
||||
const now = nowMs();
|
||||
if (now >= nextRefreshAtMs) scheduleRefresh();
|
||||
if (immediate) {
|
||||
state = {
|
||||
severity: "critical",
|
||||
reason: "v8_heap_absolute",
|
||||
elevatedStreak: 0,
|
||||
recoveryStreak: 0,
|
||||
lastTransitionAtMs: now,
|
||||
observedAtMs: now,
|
||||
};
|
||||
return immediate;
|
||||
}
|
||||
const cacheAge = lastSignals ? Math.max(0, now - lastRefreshAtMs) : Number.POSITIVE_INFINITY;
|
||||
return cacheAge <= maxStaleMs && state.severity === "critical"
|
||||
? buildCriticalGuard(state.reason)
|
||||
: null;
|
||||
},
|
||||
getObservation: () => ({ signals: lastSignals, state }),
|
||||
whenRefreshSettled: async () => {
|
||||
if (scheduled) await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
if (inFlight) await inFlight;
|
||||
},
|
||||
dispose() {
|
||||
disposed = true;
|
||||
scheduled = false;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let defaultRuntime = createResourcePressureRuntime();
|
||||
|
||||
export function checkResourcePressureGuard(): ResourcePressureGuardResult | null {
|
||||
return defaultRuntime.check();
|
||||
}
|
||||
|
||||
export function getResourcePressureObservation(): ResourcePressureObservation {
|
||||
return defaultRuntime.getObservation();
|
||||
}
|
||||
|
||||
/** Replaces and disposes the process singleton when configuration is reloaded. */
|
||||
export function reloadResourcePressureRuntime(
|
||||
options: ResourcePressureRuntimeOptions = {}
|
||||
): ResourcePressureRuntime {
|
||||
defaultRuntime.dispose();
|
||||
defaultRuntime = createResourcePressureRuntime(options);
|
||||
return defaultRuntime;
|
||||
}
|
||||
|
||||
export type {
|
||||
PressureReason,
|
||||
PressureSeverity,
|
||||
ResourceMetricBytes,
|
||||
ResourcePressureState,
|
||||
ResourcePressureThresholds,
|
||||
ResourcePressureTracker,
|
||||
ResourceSignals,
|
||||
} from "./resourcePressurePolicy.ts";
|
||||
export {
|
||||
classifyAdaptiveResourcePressure as classifyResourcePressure,
|
||||
createResourcePressureTracker,
|
||||
resolveResourcePressureThresholds,
|
||||
} from "./resourcePressurePolicy.ts";
|
||||
export {
|
||||
sampleResourceSignals,
|
||||
sanitizeMemoryBytes,
|
||||
type ResourcePressureFs,
|
||||
type SampleResourceSignalsDeps,
|
||||
} from "./resourcePressureSampler.ts";
|
||||
344
open-sse/utils/resourcePressurePolicy.ts
Normal file
344
open-sse/utils/resourcePressurePolicy.ts
Normal file
@@ -0,0 +1,344 @@
|
||||
const MB = 1024 * 1024;
|
||||
const MAX_SUSTAINED_SAMPLES = 10_000;
|
||||
|
||||
export type PressureSeverity = "normal" | "high" | "critical";
|
||||
|
||||
export type PressureReason =
|
||||
| "none"
|
||||
| "v8_heap_ratio"
|
||||
| "v8_heap_absolute"
|
||||
| "cgroup_ratio"
|
||||
| "cgroup_high"
|
||||
| "psi_some"
|
||||
| "psi_full"
|
||||
| "oom_event";
|
||||
|
||||
export type ResourceMetricBytes = number | null;
|
||||
|
||||
export type ResourceSignals = {
|
||||
observedAtMs: number;
|
||||
v8: { heapUsedBytes: number; heapLimitBytes: number };
|
||||
process: {
|
||||
rssBytes: number;
|
||||
externalBytes: number;
|
||||
arrayBuffersBytes: number;
|
||||
availableBytes: ResourceMetricBytes;
|
||||
constrainedBytes: ResourceMetricBytes;
|
||||
};
|
||||
cgroup: {
|
||||
currentBytes: ResourceMetricBytes;
|
||||
maxBytes: ResourceMetricBytes;
|
||||
highBytes: ResourceMetricBytes;
|
||||
events: {
|
||||
low: ResourceMetricBytes;
|
||||
high: ResourceMetricBytes;
|
||||
max: ResourceMetricBytes;
|
||||
oom: ResourceMetricBytes;
|
||||
oom_kill: ResourceMetricBytes;
|
||||
} | null;
|
||||
};
|
||||
psi: {
|
||||
someAvg10: number | null;
|
||||
someAvg60: number | null;
|
||||
someAvg300: number | null;
|
||||
fullAvg10: number | null;
|
||||
fullAvg60: number | null;
|
||||
fullAvg300: number | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type ResourcePressureState = {
|
||||
severity: PressureSeverity;
|
||||
reason: PressureReason;
|
||||
elevatedStreak: number;
|
||||
recoveryStreak: number;
|
||||
lastTransitionAtMs: number;
|
||||
observedAtMs: number;
|
||||
};
|
||||
|
||||
export type ResourcePressureThresholds = {
|
||||
highRatio: number;
|
||||
criticalRatio: number;
|
||||
recoveryRatio: number;
|
||||
highPsiAvg10: number;
|
||||
criticalPsiAvg10: number;
|
||||
recoveryPsiAvg10: number;
|
||||
sustainedSamplesHigh: number;
|
||||
sustainedSamplesCritical: number;
|
||||
sustainedSamplesRecovery: number;
|
||||
heapAbsoluteThresholdMb: number | null;
|
||||
};
|
||||
|
||||
export const DEFAULT_RESOURCE_PRESSURE_THRESHOLDS: ResourcePressureThresholds = {
|
||||
highRatio: 0.85,
|
||||
criticalRatio: 0.92,
|
||||
recoveryRatio: 0.75,
|
||||
highPsiAvg10: 20,
|
||||
criticalPsiAvg10: 40,
|
||||
recoveryPsiAvg10: 10,
|
||||
sustainedSamplesHigh: 2,
|
||||
sustainedSamplesCritical: 2,
|
||||
sustainedSamplesRecovery: 3,
|
||||
heapAbsoluteThresholdMb: null,
|
||||
};
|
||||
|
||||
type RawLevel = { severity: PressureSeverity; reason: PressureReason };
|
||||
type OomCounters = { oom: number | null; oomKill: number | null };
|
||||
|
||||
function requireFiniteRange(name: string, value: number, minimum: number, maximum: number): void {
|
||||
if (!Number.isFinite(value) || value < minimum || value > maximum) {
|
||||
throw new RangeError(`${name} must be finite and between ${minimum} and ${maximum}`);
|
||||
}
|
||||
}
|
||||
|
||||
function requirePositiveInteger(name: string, value: number): void {
|
||||
if (!Number.isInteger(value) || value < 1 || value > MAX_SUSTAINED_SAMPLES) {
|
||||
throw new RangeError(`${name} must be an integer between 1 and ${MAX_SUSTAINED_SAMPLES}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveResourcePressureThresholds(
|
||||
partial: Partial<ResourcePressureThresholds> = {}
|
||||
): ResourcePressureThresholds {
|
||||
const resolved = { ...DEFAULT_RESOURCE_PRESSURE_THRESHOLDS, ...partial };
|
||||
requireFiniteRange("recoveryRatio", resolved.recoveryRatio, 0, 1);
|
||||
requireFiniteRange("highRatio", resolved.highRatio, 0, 1);
|
||||
requireFiniteRange("criticalRatio", resolved.criticalRatio, 0, 1);
|
||||
if (!(
|
||||
resolved.recoveryRatio < resolved.highRatio && resolved.highRatio < resolved.criticalRatio
|
||||
)) {
|
||||
throw new RangeError("ratio thresholds must satisfy recovery < high < critical");
|
||||
}
|
||||
|
||||
requireFiniteRange("recoveryPsiAvg10", resolved.recoveryPsiAvg10, 0, 100);
|
||||
requireFiniteRange("highPsiAvg10", resolved.highPsiAvg10, 0, 100);
|
||||
requireFiniteRange("criticalPsiAvg10", resolved.criticalPsiAvg10, 0, 100);
|
||||
if (!(
|
||||
resolved.recoveryPsiAvg10 < resolved.highPsiAvg10 &&
|
||||
resolved.highPsiAvg10 < resolved.criticalPsiAvg10
|
||||
)) {
|
||||
throw new RangeError("PSI thresholds must satisfy recovery < high < critical");
|
||||
}
|
||||
|
||||
requirePositiveInteger("sustainedSamplesHigh", resolved.sustainedSamplesHigh);
|
||||
requirePositiveInteger("sustainedSamplesCritical", resolved.sustainedSamplesCritical);
|
||||
requirePositiveInteger("sustainedSamplesRecovery", resolved.sustainedSamplesRecovery);
|
||||
if (
|
||||
resolved.heapAbsoluteThresholdMb !== null &&
|
||||
(!Number.isFinite(resolved.heapAbsoluteThresholdMb) || resolved.heapAbsoluteThresholdMb <= 0)
|
||||
) {
|
||||
throw new RangeError("heapAbsoluteThresholdMb must be positive and finite or null");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function severityRank(severity: PressureSeverity): number {
|
||||
return severity === "critical" ? 2 : severity === "high" ? 1 : 0;
|
||||
}
|
||||
|
||||
function maxLevel(current: RawLevel, candidate: RawLevel | null): RawLevel {
|
||||
if (!candidate || severityRank(candidate.severity) <= severityRank(current.severity)) {
|
||||
return current;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function ratioLevel(
|
||||
used: number | null,
|
||||
limit: number | null,
|
||||
thresholds: ResourcePressureThresholds,
|
||||
reason: PressureReason
|
||||
): RawLevel | null {
|
||||
if (used == null || limit == null || used < 0 || limit <= 0) return null;
|
||||
const ratio = used / limit;
|
||||
if (ratio >= thresholds.criticalRatio) return { severity: "critical", reason };
|
||||
if (ratio >= thresholds.highRatio) return { severity: "high", reason };
|
||||
return null;
|
||||
}
|
||||
|
||||
function psiLevel(
|
||||
value: number | null,
|
||||
thresholds: ResourcePressureThresholds,
|
||||
reason: Extract<PressureReason, "psi_some" | "psi_full">
|
||||
): RawLevel | null {
|
||||
if (value == null || !Number.isFinite(value)) return null;
|
||||
if (value >= thresholds.criticalPsiAvg10) return { severity: "critical", reason };
|
||||
if (value >= thresholds.highPsiAvg10) return { severity: "high", reason };
|
||||
return null;
|
||||
}
|
||||
|
||||
export function classifyAdaptiveResourcePressure(
|
||||
signals: ResourceSignals,
|
||||
thresholds: ResourcePressureThresholds
|
||||
): RawLevel {
|
||||
let best: RawLevel = { severity: "normal", reason: "none" };
|
||||
best = maxLevel(
|
||||
best,
|
||||
ratioLevel(signals.v8.heapUsedBytes, signals.v8.heapLimitBytes, thresholds, "v8_heap_ratio")
|
||||
);
|
||||
best = maxLevel(
|
||||
best,
|
||||
ratioLevel(signals.cgroup.currentBytes, signals.cgroup.maxBytes, thresholds, "cgroup_ratio")
|
||||
);
|
||||
best = maxLevel(
|
||||
best,
|
||||
ratioLevel(signals.cgroup.currentBytes, signals.cgroup.highBytes, thresholds, "cgroup_high")
|
||||
);
|
||||
best = maxLevel(best, psiLevel(signals.psi?.someAvg10 ?? null, thresholds, "psi_some"));
|
||||
return maxLevel(best, psiLevel(signals.psi?.fullAvg10 ?? null, thresholds, "psi_full"));
|
||||
}
|
||||
|
||||
function isRecovered(signals: ResourceSignals, thresholds: ResourcePressureThresholds): boolean {
|
||||
const ratios: Array<readonly [number | null, number | null]> = [
|
||||
[signals.v8.heapUsedBytes, signals.v8.heapLimitBytes],
|
||||
[signals.cgroup.currentBytes, signals.cgroup.maxBytes],
|
||||
[signals.cgroup.currentBytes, signals.cgroup.highBytes],
|
||||
];
|
||||
if (
|
||||
ratios.some(
|
||||
([used, limit]) =>
|
||||
used != null && limit != null && limit > 0 && used / limit > thresholds.recoveryRatio
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
thresholds.heapAbsoluteThresholdMb != null &&
|
||||
signals.v8.heapUsedBytes / MB > thresholds.heapAbsoluteThresholdMb * thresholds.recoveryRatio
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return ![signals.psi?.someAvg10, signals.psi?.fullAvg10].some(
|
||||
(value) => value != null && value > thresholds.recoveryPsiAvg10
|
||||
);
|
||||
}
|
||||
|
||||
function hasCounterIncrease(previous: OomCounters, current: OomCounters): boolean {
|
||||
return (
|
||||
(previous.oom != null && current.oom != null && current.oom > previous.oom) ||
|
||||
(previous.oomKill != null && current.oomKill != null && current.oomKill > previous.oomKill)
|
||||
);
|
||||
}
|
||||
|
||||
function countersReset(previous: OomCounters, current: OomCounters): boolean {
|
||||
return (
|
||||
(previous.oom != null && current.oom != null && current.oom < previous.oom) ||
|
||||
(previous.oomKill != null && current.oomKill != null && current.oomKill < previous.oomKill)
|
||||
);
|
||||
}
|
||||
|
||||
function initialState(): ResourcePressureState {
|
||||
return {
|
||||
severity: "normal",
|
||||
reason: "none",
|
||||
elevatedStreak: 0,
|
||||
recoveryStreak: 0,
|
||||
lastTransitionAtMs: 0,
|
||||
observedAtMs: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export type ResourcePressureTracker = {
|
||||
observe: (signals: ResourceSignals) => ResourcePressureState;
|
||||
getState: () => ResourcePressureState;
|
||||
};
|
||||
|
||||
export function createResourcePressureTracker(
|
||||
partialThresholds: Partial<ResourcePressureThresholds> = {}
|
||||
): ResourcePressureTracker {
|
||||
const thresholds = resolveResourcePressureThresholds(partialThresholds);
|
||||
let state = initialState();
|
||||
let pending: RawLevel | null = null;
|
||||
let previousOom: OomCounters | null = null;
|
||||
|
||||
return {
|
||||
observe(signals) {
|
||||
const events = signals.cgroup.events;
|
||||
const currentOom = events ? { oom: events.oom, oomKill: events.oom_kill } : null;
|
||||
let oomEvent = false;
|
||||
if (currentOom) {
|
||||
if (previousOom && !countersReset(previousOom, currentOom)) {
|
||||
oomEvent = hasCounterIncrease(previousOom, currentOom);
|
||||
}
|
||||
previousOom = currentOom;
|
||||
} else {
|
||||
previousOom = null;
|
||||
}
|
||||
|
||||
const raw = oomEvent
|
||||
? ({ severity: "critical", reason: "oom_event" } as const)
|
||||
: classifyAdaptiveResourcePressure(signals, thresholds);
|
||||
let { severity, reason, elevatedStreak, recoveryStreak } = state;
|
||||
|
||||
if (oomEvent) {
|
||||
severity = "critical";
|
||||
reason = "oom_event";
|
||||
elevatedStreak = 0;
|
||||
recoveryStreak = 0;
|
||||
pending = null;
|
||||
} else if (severity === "normal") {
|
||||
recoveryStreak = 0;
|
||||
if (raw.severity === "normal") {
|
||||
pending = null;
|
||||
elevatedStreak = 0;
|
||||
reason = "none";
|
||||
} else {
|
||||
const samePending = pending?.severity === raw.severity && pending.reason === raw.reason;
|
||||
pending = raw;
|
||||
elevatedStreak = samePending ? elevatedStreak + 1 : 1;
|
||||
const needed =
|
||||
raw.severity === "critical"
|
||||
? thresholds.sustainedSamplesCritical
|
||||
: thresholds.sustainedSamplesHigh;
|
||||
if (elevatedStreak >= needed) {
|
||||
severity = raw.severity;
|
||||
reason = raw.reason;
|
||||
elevatedStreak = 0;
|
||||
pending = null;
|
||||
}
|
||||
}
|
||||
} else if (severity === "high" && raw.severity === "critical") {
|
||||
recoveryStreak = 0;
|
||||
const samePending = pending?.severity === "critical" && pending.reason === raw.reason;
|
||||
pending = raw;
|
||||
elevatedStreak = samePending ? elevatedStreak + 1 : 1;
|
||||
if (elevatedStreak >= thresholds.sustainedSamplesCritical) {
|
||||
severity = "critical";
|
||||
reason = raw.reason;
|
||||
elevatedStreak = 0;
|
||||
pending = null;
|
||||
}
|
||||
} else if (raw.severity === severity) {
|
||||
reason = raw.reason;
|
||||
pending = null;
|
||||
elevatedStreak = 0;
|
||||
recoveryStreak = 0;
|
||||
} else if (isRecovered(signals, thresholds)) {
|
||||
pending = null;
|
||||
elevatedStreak = 0;
|
||||
recoveryStreak += 1;
|
||||
if (recoveryStreak >= thresholds.sustainedSamplesRecovery) {
|
||||
severity = "normal";
|
||||
reason = "none";
|
||||
recoveryStreak = 0;
|
||||
}
|
||||
} else {
|
||||
pending = null;
|
||||
elevatedStreak = 0;
|
||||
recoveryStreak = 0;
|
||||
}
|
||||
|
||||
const transitioned = severity !== state.severity || reason !== state.reason;
|
||||
state = {
|
||||
severity,
|
||||
reason,
|
||||
elevatedStreak,
|
||||
recoveryStreak,
|
||||
lastTransitionAtMs: transitioned ? signals.observedAtMs : state.lastTransitionAtMs,
|
||||
observedAtMs: signals.observedAtMs,
|
||||
};
|
||||
return state;
|
||||
},
|
||||
getState: () => state,
|
||||
};
|
||||
}
|
||||
257
open-sse/utils/resourcePressureSampler.ts
Normal file
257
open-sse/utils/resourcePressureSampler.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import v8 from "node:v8";
|
||||
import type { ResourceSignals } from "./resourcePressurePolicy.ts";
|
||||
|
||||
const DEFAULT_CGROUP_ROOT = "/sys/fs/cgroup";
|
||||
|
||||
export type ResourcePressureFs = {
|
||||
readText: (filePath: string) => Promise<string | null>;
|
||||
};
|
||||
|
||||
export type SampleResourceSignalsDeps = {
|
||||
nowMs?: () => number;
|
||||
memoryUsage?: () => NodeJS.MemoryUsage;
|
||||
heapStatistics?: () => { heap_size_limit: number; used_heap_size?: number };
|
||||
availableMemory?: () => number | undefined;
|
||||
constrainedMemory?: () => number | undefined;
|
||||
fs?: ResourcePressureFs;
|
||||
};
|
||||
|
||||
type Cgroup2Mount = { root: string; mountpoint: string };
|
||||
|
||||
async function defaultReadText(filePath: string): Promise<string | null> {
|
||||
try {
|
||||
return await fs.readFile(filePath, "utf8");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function sanitizeMemoryBytes(value: unknown): number | null {
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed || trimmed === "max" || !/^\d+$/.test(trimmed) || trimmed.length > 15) {
|
||||
return null;
|
||||
}
|
||||
value = Number(trimmed);
|
||||
}
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return null;
|
||||
if (value >= Number.MAX_SAFE_INTEGER) return null;
|
||||
return Math.floor(value);
|
||||
}
|
||||
|
||||
function safeNumber(call: (() => number | undefined) | undefined): number | null {
|
||||
try {
|
||||
return call ? sanitizeMemoryBytes(call()) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function decodeMountInfoPath(value: string): string | null {
|
||||
if (value.includes("\0")) return null;
|
||||
try {
|
||||
return value.replace(/\\([0-7]{3})/g, (_match, octal: string) =>
|
||||
String.fromCharCode(Number.parseInt(octal, 8))
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseCgroupV2Path(contents: string | null): string | null {
|
||||
if (!contents) return null;
|
||||
for (const rawLine of contents.split("\n")) {
|
||||
const line = rawLine.trim();
|
||||
if (!line.startsWith("0::")) continue;
|
||||
const relativePath = line.slice(3);
|
||||
if (!relativePath.startsWith("/") || relativePath.includes("\0")) return null;
|
||||
return relativePath;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function parseCgroup2Mount(contents: string | null): Cgroup2Mount | null {
|
||||
if (!contents) return null;
|
||||
for (const rawLine of contents.split("\n")) {
|
||||
const separator = rawLine.indexOf(" - ");
|
||||
if (separator < 0) continue;
|
||||
const left = rawLine.slice(0, separator).trim().split(/\s+/);
|
||||
const right = rawLine
|
||||
.slice(separator + 3)
|
||||
.trim()
|
||||
.split(/\s+/);
|
||||
if (right[0] !== "cgroup2" || left.length < 5) continue;
|
||||
const root = decodeMountInfoPath(left[3]);
|
||||
const mountpoint = decodeMountInfoPath(left[4]);
|
||||
if (!root?.startsWith("/") || !mountpoint?.startsWith("/")) return null;
|
||||
return { root, mountpoint };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isContained(root: string, candidate: string): boolean {
|
||||
const relative = path.relative(root, candidate);
|
||||
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
||||
}
|
||||
|
||||
function hasTraversalSegment(value: string): boolean {
|
||||
let decoded = value;
|
||||
try {
|
||||
decoded = decodeURIComponent(value);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
return decoded.split("/").some((segment) => segment === ".." || segment === ".");
|
||||
}
|
||||
|
||||
function resolveFromMount(cgroupPath: string, mount: Cgroup2Mount): string | null {
|
||||
if (
|
||||
cgroupPath.includes("\0") ||
|
||||
mount.root.includes("\0") ||
|
||||
mount.mountpoint.includes("\0") ||
|
||||
hasTraversalSegment(cgroupPath)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const resolvedRoot = path.resolve(mount.root);
|
||||
const resolvedCgroup = path.resolve(cgroupPath);
|
||||
if (!isContained(resolvedRoot, resolvedCgroup)) return null;
|
||||
const suffix = path.relative(resolvedRoot, resolvedCgroup);
|
||||
const resolvedMountpoint = path.resolve(mount.mountpoint);
|
||||
const candidate = path.resolve(resolvedMountpoint, suffix);
|
||||
return isContained(resolvedMountpoint, candidate) ? candidate : null;
|
||||
}
|
||||
|
||||
export async function resolveCgroupDirectory(
|
||||
readText: ResourcePressureFs["readText"],
|
||||
options: { allowDefaultFallback?: boolean } = {}
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const [cgroupContents, mountInfo] = await Promise.all([
|
||||
readText("/proc/self/cgroup"),
|
||||
readText("/proc/self/mountinfo"),
|
||||
]);
|
||||
const cgroupPath = parseCgroupV2Path(cgroupContents);
|
||||
const mount = parseCgroup2Mount(mountInfo);
|
||||
if (cgroupPath && mount) {
|
||||
const candidate = resolveFromMount(cgroupPath, mount);
|
||||
if (candidate && (await readText(path.join(candidate, "memory.current"))) != null) {
|
||||
return candidate;
|
||||
}
|
||||
if (!candidate) return null;
|
||||
}
|
||||
if (options.allowDefaultFallback === false) return null;
|
||||
return (await readText(path.join(DEFAULT_CGROUP_ROOT, "memory.current"))) != null
|
||||
? DEFAULT_CGROUP_ROOT
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function parseEventCounter(value: string): number | null {
|
||||
const parsed = Number(value.trim());
|
||||
return Number.isFinite(parsed) && parsed >= 0 && parsed < Number.MAX_SAFE_INTEGER
|
||||
? Math.floor(parsed)
|
||||
: null;
|
||||
}
|
||||
|
||||
function parseMemoryEvents(text: string | null): ResourceSignals["cgroup"]["events"] {
|
||||
if (!text) return null;
|
||||
const values = { low: null, high: null, max: null, oom: null, oom_kill: null } as Record<
|
||||
"low" | "high" | "max" | "oom" | "oom_kill",
|
||||
number | null
|
||||
>;
|
||||
let matched = false;
|
||||
for (const line of text.split("\n")) {
|
||||
const [key, rawValue] = line.trim().split(/\s+/, 2);
|
||||
if (!(key in values) || rawValue == null) continue;
|
||||
values[key as keyof typeof values] = parseEventCounter(rawValue);
|
||||
matched = true;
|
||||
}
|
||||
return matched ? values : null;
|
||||
}
|
||||
|
||||
function parsePsiNumber(line: string, name: string): number | null {
|
||||
const match = new RegExp(`(?:^|\\s)${name}=([0-9.]+)`).exec(line);
|
||||
const parsed = match ? Number(match[1]) : Number.NaN;
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
||||
}
|
||||
|
||||
function parsePsi(text: string | null): ResourceSignals["psi"] {
|
||||
if (!text) return null;
|
||||
const result: NonNullable<ResourceSignals["psi"]> = {
|
||||
someAvg10: null,
|
||||
someAvg60: null,
|
||||
someAvg300: null,
|
||||
fullAvg10: null,
|
||||
fullAvg60: null,
|
||||
fullAvg300: null,
|
||||
};
|
||||
let matched = false;
|
||||
for (const line of text.split("\n")) {
|
||||
const kind = line.startsWith("some ") ? "some" : line.startsWith("full ") ? "full" : null;
|
||||
if (!kind) continue;
|
||||
result[`${kind}Avg10`] = parsePsiNumber(line, "avg10");
|
||||
result[`${kind}Avg60`] = parsePsiNumber(line, "avg60");
|
||||
result[`${kind}Avg300`] = parsePsiNumber(line, "avg300");
|
||||
matched = true;
|
||||
}
|
||||
return matched ? result : null;
|
||||
}
|
||||
|
||||
export async function sampleResourceSignals(
|
||||
deps: SampleResourceSignalsDeps = {}
|
||||
): Promise<ResourceSignals> {
|
||||
const readText = deps.fs?.readText ?? defaultReadText;
|
||||
let memory: NodeJS.MemoryUsage;
|
||||
try {
|
||||
memory = (deps.memoryUsage ?? process.memoryUsage)();
|
||||
} catch {
|
||||
memory = { rss: 0, heapTotal: 0, heapUsed: 0, external: 0, arrayBuffers: 0 };
|
||||
}
|
||||
|
||||
let heapUsed = Math.max(0, Math.floor(memory.heapUsed || 0));
|
||||
let heapLimit = 0;
|
||||
try {
|
||||
const heap = (deps.heapStatistics ?? v8.getHeapStatistics)();
|
||||
heapLimit = sanitizeMemoryBytes(heap.heap_size_limit) ?? 0;
|
||||
if (Number.isFinite(heap.used_heap_size)) {
|
||||
heapUsed = Math.max(0, Math.floor(heap.used_heap_size ?? heapUsed));
|
||||
}
|
||||
} catch {
|
||||
/* retain process heap sample */
|
||||
}
|
||||
|
||||
const cgroupDirectory = await resolveCgroupDirectory(readText);
|
||||
const cgroupContents = cgroupDirectory
|
||||
? await Promise.all([
|
||||
readText(path.join(cgroupDirectory, "memory.current")),
|
||||
readText(path.join(cgroupDirectory, "memory.max")),
|
||||
readText(path.join(cgroupDirectory, "memory.high")),
|
||||
readText(path.join(cgroupDirectory, "memory.events")),
|
||||
])
|
||||
: [null, null, null, null];
|
||||
const psi = await readText("/proc/pressure/memory").catch(() => null);
|
||||
|
||||
return {
|
||||
observedAtMs: (deps.nowMs ?? Date.now)(),
|
||||
v8: { heapUsedBytes: heapUsed, heapLimitBytes: heapLimit },
|
||||
process: {
|
||||
rssBytes: Math.max(0, Math.floor(memory.rss || 0)),
|
||||
externalBytes: Math.max(0, Math.floor(memory.external || 0)),
|
||||
arrayBuffersBytes: Math.max(0, Math.floor(memory.arrayBuffers || 0)),
|
||||
availableBytes: safeNumber(deps.availableMemory ?? (() => process.availableMemory?.())),
|
||||
constrainedBytes: safeNumber(deps.constrainedMemory ?? (() => process.constrainedMemory?.())),
|
||||
},
|
||||
cgroup: {
|
||||
currentBytes: sanitizeMemoryBytes(cgroupContents[0]),
|
||||
maxBytes: sanitizeMemoryBytes(cgroupContents[1]),
|
||||
highBytes: sanitizeMemoryBytes(cgroupContents[2]),
|
||||
events: parseMemoryEvents(cgroupContents[3]),
|
||||
},
|
||||
psi: parsePsi(psi),
|
||||
};
|
||||
}
|
||||
@@ -38,6 +38,7 @@
|
||||
"scripts/build/runtime-env.mjs",
|
||||
"README.md",
|
||||
"LICENSE",
|
||||
"!**/node_modules/**",
|
||||
"!**/__tests__/**",
|
||||
"!**/*.test.ts",
|
||||
"!**/*.test.tsx",
|
||||
|
||||
@@ -209,6 +209,19 @@ export function normalizeArtifactPath(filePath: string): string {
|
||||
.replace(/\/{2,}/g, "/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Paths that are NEVER publishable, whatever the allowlist says.
|
||||
*
|
||||
* Existence reason: the allowlist grants whole prefixes (e.g.
|
||||
* `@omniroute/opencode-provider/`), so a nested `node_modules` inside an allowed
|
||||
* prefix used to be authorized by it. That shipped 79 MB of devDependencies
|
||||
* (tsup/esbuild/typescript) — 80% of the tarball — whenever the publish ran from
|
||||
* a machine where someone had installed inside that subpackage. `files[]` in
|
||||
* package.json now excludes it at the source; this is the gate that FAILS if it
|
||||
* ever comes back instead of silently allowing it.
|
||||
*/
|
||||
export const PACK_ARTIFACT_NEVER_ALLOWED_SEGMENTS: string[] = ["node_modules"];
|
||||
|
||||
export function findUnexpectedArtifactPaths(
|
||||
filePaths: string[],
|
||||
{ exactPaths = [], prefixPaths = [] }: { exactPaths?: string[]; prefixPaths?: string[] } = {}
|
||||
@@ -216,13 +229,17 @@ export function findUnexpectedArtifactPaths(
|
||||
const normalizedExact = new Set(exactPaths.map(normalizeArtifactPath));
|
||||
const normalizedPrefixes = prefixPaths.map(normalizeArtifactPath);
|
||||
|
||||
const hasForbiddenSegment = (filePath: string): boolean =>
|
||||
filePath.split("/").some((segment) => PACK_ARTIFACT_NEVER_ALLOWED_SEGMENTS.includes(segment));
|
||||
|
||||
return filePaths
|
||||
.map(normalizeArtifactPath)
|
||||
.filter(Boolean)
|
||||
.filter(
|
||||
(filePath) =>
|
||||
!normalizedExact.has(filePath) &&
|
||||
!normalizedPrefixes.some((prefix) => filePath.startsWith(prefix))
|
||||
hasForbiddenSegment(filePath) ||
|
||||
(!normalizedExact.has(filePath) &&
|
||||
!normalizedPrefixes.some((prefix) => filePath.startsWith(prefix)))
|
||||
)
|
||||
.sort();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
// igual ao próprio teto ficava presa no baseline para sempre — ver #8584.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
@@ -22,6 +23,7 @@ const BASELINE_PATH = path.resolve(
|
||||
getArg("--baseline", path.join(ROOT, "config/quality/file-size-baseline.json"))
|
||||
);
|
||||
const UPDATE = process.argv.includes("--update");
|
||||
const BASE_REF = getArg("--base-ref"); // SHA for PR base-relative mode (#8522)
|
||||
const SCAN_DIRS = ["src", "open-sse", "electron", "bin"];
|
||||
// Test files live under tests/ plus co-located *.test.ts(x) inside the source dirs.
|
||||
const TEST_SCAN_DIRS = ["tests", ...SCAN_DIRS];
|
||||
@@ -37,20 +39,39 @@ const SKIP_DIRS = new Set(["node_modules", "dist-electron", ".next", ".build", "
|
||||
* (loc < frozen), entao uma entrada igual ao proprio teto nunca saia da lista,
|
||||
* por mais abaixo do cap que estivesse (3 casos reais no v3.8.49).
|
||||
*
|
||||
* Quando `baseLocByFile` e fornecido (modo PR), a violacao e computada contra
|
||||
* o MAIOR entre o valor congelado e o valor na base -- assim um PR inocente
|
||||
* (head === base no arquivo) nao e penalizado por drift herdado (#8522).
|
||||
*
|
||||
* @param {Object} currentLocByFile — LOC atuais (head)
|
||||
* @param {Object} frozen — baseline congelado
|
||||
* @param {number} cap — teto para arquivos novos
|
||||
* @param {Object} [baseLocByFile] — LOC na branch base (opcional, modo PR)
|
||||
* @returns {{violations: string[], improvements: [string, number][], redundant: string[]}}
|
||||
*/
|
||||
export function evaluateFileSizes(currentLocByFile, frozen, cap) {
|
||||
export function evaluateFileSizes(currentLocByFile, frozen, cap, baseLocByFile) {
|
||||
const violations = [];
|
||||
const improvements = [];
|
||||
const redundant = [];
|
||||
for (const [file, loc] of Object.entries(currentLocByFile)) {
|
||||
if (file in frozen) {
|
||||
if (loc > frozen[file])
|
||||
const threshold = baseLocByFile
|
||||
? Math.max(frozen[file], baseLocByFile[file] ?? frozen[file])
|
||||
: frozen[file];
|
||||
if (loc > threshold)
|
||||
violations.push(`${file}: ${loc} > congelado ${frozen[file]} (não pode crescer)`);
|
||||
else if (loc < frozen[file]) improvements.push([file, loc]);
|
||||
else if (loc <= cap) redundant.push(file);
|
||||
} else if (loc > cap) {
|
||||
violations.push(`${file}: ${loc} > cap ${cap} (arquivo novo acima do limite)`);
|
||||
if (!baseLocByFile) {
|
||||
violations.push(`${file}: ${loc} > cap ${cap} (arquivo novo acima do limite)`);
|
||||
} else {
|
||||
// Modo PR: so viola se cresceu alem do que ja estava na base
|
||||
const baseLoc = baseLocByFile[file] ?? 0;
|
||||
const prThreshold = Math.max(cap, baseLoc);
|
||||
if (loc > prThreshold)
|
||||
violations.push(`${file}: ${loc} > cap ${cap} (arquivo novo acima do limite)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { violations, improvements, redundant };
|
||||
@@ -108,6 +129,30 @@ function collectTestLoc() {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computa LOC por arquivo a partir de um ref git (branch, SHA, tag).
|
||||
* Usado pelo modo --base-ref para obter a contagem na base do PR (#8522).
|
||||
* @param {string} ref — git ref (e.g. SHA da branch base)
|
||||
* @param {string[]} files — lista de paths relativos ao ROOT
|
||||
* @returns {Object} mapa file → line count
|
||||
*/
|
||||
function getBaseLoc(ref, files) {
|
||||
const out = {};
|
||||
for (const file of files) {
|
||||
try {
|
||||
const buf = execFileSync("git", ["show", `${ref}:${file}`], {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
timeout: 5000,
|
||||
});
|
||||
out[file] = buf.split("\n").length;
|
||||
} catch {
|
||||
// Arquivo nao existe na base (novo no PR) — tratado como 0
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!fs.existsSync(BASELINE_PATH)) {
|
||||
console.error(`[file-size] FAIL — ${path.basename(BASELINE_PATH)} ausente.`);
|
||||
@@ -117,7 +162,17 @@ function main() {
|
||||
const cap = baseline.cap;
|
||||
const frozen = baseline.frozen || {};
|
||||
const current = collectLoc();
|
||||
const { violations, improvements, redundant } = evaluateFileSizes(current, frozen, cap);
|
||||
|
||||
// Modo PR: computa LOC na branch base para comparacao relativa (#8522)
|
||||
const baseLoc = BASE_REF ? getBaseLoc(BASE_REF, Object.keys(current)) : undefined;
|
||||
if (BASE_REF) {
|
||||
const baseKeys = Object.keys(baseLoc).length;
|
||||
console.log(
|
||||
`[file-size] modo PR (--base-ref ${BASE_REF.slice(0, 12)}): ${baseKeys} arquivos da base computados`
|
||||
);
|
||||
}
|
||||
|
||||
const { violations, improvements, redundant } = evaluateFileSizes(current, frozen, cap, baseLoc);
|
||||
|
||||
// Test-file gate (Layer 1 anti-reinflation): same shrink-only + new-≤cap semantics,
|
||||
// reusing evaluateFileSizes against the testFrozen baseline + testCap.
|
||||
@@ -129,7 +184,7 @@ function main() {
|
||||
improvements: testImprovements,
|
||||
redundant: testRedundant,
|
||||
} = typeof testCap === "number"
|
||||
? evaluateFileSizes(currentTests, testFrozen, testCap)
|
||||
? evaluateFileSizes(currentTests, testFrozen, testCap, BASE_REF ? baseLoc : undefined)
|
||||
: { violations: [], improvements: [], redundant: [] };
|
||||
|
||||
if (UPDATE) {
|
||||
|
||||
@@ -106,9 +106,8 @@ function normalizeWhitespace(s) {
|
||||
*/
|
||||
export function countSignificantTokens(cond) {
|
||||
const tokens =
|
||||
(cond || "").match(
|
||||
/===|!==|==|!=|>=|<=|&&|\|\||[<>+\-*/%!]|[A-Za-z_$][\w$]*|\d+(?:\.\d+)?/g
|
||||
) || [];
|
||||
(cond || "").match(/===|!==|==|!=|>=|<=|&&|\|\||[<>+\-*/%!]|[A-Za-z_$][\w$]*|\d+(?:\.\d+)?/g) ||
|
||||
[];
|
||||
let count = 0;
|
||||
for (const tk of tokens) {
|
||||
if (/^[A-Za-z_$]/.test(tk)) {
|
||||
@@ -178,8 +177,7 @@ export function extractProdConditions(src) {
|
||||
}
|
||||
|
||||
// Comparison-bearing ternaries: `<lhs> <cmp> <rhs> ? … : …` (best-effort, low-noise).
|
||||
const ternRe =
|
||||
/([A-Za-z_$][\w$).\]]*\s*(?:===|!==|==|!=|>=|<=|>|<)\s*[^?;{}\n]+?)\s*\?/g;
|
||||
const ternRe = /([A-Za-z_$][\w$).\]]*\s*(?:===|!==|==|!=|>=|<=|>|<)\s*[^?;{}\n]+?)\s*\?/g;
|
||||
let t;
|
||||
while ((t = ternRe.exec(src))) {
|
||||
pushCond(t[1], ownerAt(t.index));
|
||||
@@ -199,7 +197,10 @@ export function extractImports(src) {
|
||||
if (!src) return names;
|
||||
const addModule = (mod) => {
|
||||
names.add(mod);
|
||||
const base = mod.split("/").pop().replace(/\.\w+$/, "");
|
||||
const base = mod
|
||||
.split("/")
|
||||
.pop()
|
||||
.replace(/\.\w+$/, "");
|
||||
if (base) names.add(base);
|
||||
};
|
||||
let m;
|
||||
@@ -227,8 +228,7 @@ export function extractImports(src) {
|
||||
export function findReimplementedConditions(prodSources, testSource, testImports) {
|
||||
const flags = [];
|
||||
if (!testSource) return flags;
|
||||
const imports =
|
||||
testImports instanceof Set ? testImports : new Set(testImports || []);
|
||||
const imports = testImports instanceof Set ? testImports : new Set(testImports || []);
|
||||
const squash = (s) => (s || "").replace(/\s+/g, "");
|
||||
const testSq = squash(testSource);
|
||||
const seen = new Set();
|
||||
@@ -251,10 +251,15 @@ export function findReimplementedConditions(prodSources, testSource, testImports
|
||||
* (filtro D do git diff --diff-filter=MDR).
|
||||
*
|
||||
* `deletionAllowlist` (`_deletedWithReplacement` no test-masking-allowlist.json)
|
||||
* isenta uma deleção SOMENTE quando o substituto declarado existe no HEAD e é
|
||||
* ele próprio um arquivo de teste — o caso "reescrito em outro path sem rename
|
||||
* detectável" (conteúdo novo demais para o -M do git). Qualquer entrada cujo
|
||||
* substituto não exista ou não seja teste continua flagada.
|
||||
* isenta uma deleção de duas formas, cada uma com sua própria verificação:
|
||||
* 1. `replacement` (path string) — o substituto declarado existe no HEAD e é
|
||||
* ele próprio um arquivo de teste — o caso "reescrito em outro path sem
|
||||
* rename detectável" (conteúdo novo demais para o -M do git).
|
||||
* 2. `sourceRemoved` (array de paths) — feature removida por completo: TODOS
|
||||
* os arquivos de produção listados precisam estar ausentes no HEAD (sem
|
||||
* substituto porque não há mais código a testar). Usar apenas quando a
|
||||
* remoção do código-fonte está confirmada na mesma commit/PR.
|
||||
* Qualquer entrada cuja condição declarada não se verifique continua flagada.
|
||||
*/
|
||||
export function evaluateDeletedFiles(
|
||||
deletedPaths,
|
||||
@@ -272,6 +277,14 @@ export function evaluateDeletedFiles(
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (entry && Array.isArray(entry.sourceRemoved) && entry.sourceRemoved.length > 0) {
|
||||
const stillPresent = entry.sourceRemoved.filter((p) => fileExists(p));
|
||||
if (stillPresent.length === 0) continue;
|
||||
flags.push(
|
||||
`${f}: deleção allowlistada como feature removida mas ${stillPresent.join(", ")} ainda existe(m) no HEAD`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
flags.push(
|
||||
`${f}: arquivo de teste deletado — revisão humana obrigatória (mascaramento alto-sinal)`
|
||||
);
|
||||
|
||||
@@ -56,6 +56,7 @@ export async function GET() {
|
||||
sessionManagerModule,
|
||||
credentialHealthModule,
|
||||
localHealthModule,
|
||||
adaptiveAdmissionModule,
|
||||
settingsResult,
|
||||
connectionsResult,
|
||||
] = await Promise.allSettled([
|
||||
@@ -67,6 +68,7 @@ export async function GET() {
|
||||
import("@omniroute/open-sse/services/sessionManager.ts"),
|
||||
import("@/lib/credentialHealth/cache"),
|
||||
import("@/lib/localHealthCheck"),
|
||||
import("@omniroute/open-sse/services/admission/runtime.ts"),
|
||||
getCachedSettings(),
|
||||
getProviderConnections(),
|
||||
]);
|
||||
@@ -145,6 +147,14 @@ export async function GET() {
|
||||
: {};
|
||||
const settings = settingsResult.status === "fulfilled" ? settingsResult.value : {};
|
||||
const connections = connectionsResult.status === "fulfilled" ? connectionsResult.value : [];
|
||||
const adaptiveAdmission =
|
||||
adaptiveAdmissionModule.status === "fulfilled"
|
||||
? readHealthValue(
|
||||
"adaptive admission",
|
||||
() => adaptiveAdmissionModule.value.getAdaptiveAdmissionRuntime().snapshot(),
|
||||
null
|
||||
)
|
||||
: null;
|
||||
|
||||
const payload = buildHealthPayload({
|
||||
appVersion: APP_CONFIG.version,
|
||||
@@ -169,6 +179,7 @@ export async function GET() {
|
||||
activeSessions,
|
||||
activeSessionsByKey,
|
||||
credentialHealth,
|
||||
adaptiveAdmission,
|
||||
});
|
||||
|
||||
healthPayloadCache = { payload, expiresAt: Date.now() + HEALTH_PAYLOAD_TTL_MS };
|
||||
@@ -186,6 +197,7 @@ export async function GET() {
|
||||
lockouts: [],
|
||||
quotaMonitor: { ...fallbackQuotaMonitorSummary, monitors: [] },
|
||||
sessions: { activeCount: 0, stickyBoundCount: 0, byApiKey: {}, top: [] },
|
||||
adaptiveAdmission: null,
|
||||
dedup: { inflightRequests: 0 },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ export async function POST(request: Request) {
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
body: JSON.stringify(normalized),
|
||||
signal: request.signal,
|
||||
});
|
||||
// #3571 — translate the chat-pipeline response back to the legacy
|
||||
// text-completion shape so OpenAI Completion clients (e.g. TabbyML) work.
|
||||
@@ -90,7 +91,7 @@ export async function POST(request: Request) {
|
||||
// echo the compression header on the way out.
|
||||
return withCompressionHeaderEcho(
|
||||
await asTextCompletionResponse(
|
||||
await handleChat(newRequest, buildClientRawRequest(request, body)),
|
||||
await handleChat(newRequest, () => buildClientRawRequest(request, body)),
|
||||
typeof body.model === "string" ? body.model : undefined
|
||||
),
|
||||
compressionRequestHeader
|
||||
@@ -106,7 +107,10 @@ export async function POST(request: Request) {
|
||||
// Re-read body.model so the response echoes the caller's requested identifier.
|
||||
let requestedModel: string | undefined;
|
||||
try {
|
||||
const bodyForModel = await request.clone().json().catch(() => null);
|
||||
const bodyForModel = await request
|
||||
.clone()
|
||||
.json()
|
||||
.catch(() => null);
|
||||
if (bodyForModel && typeof bodyForModel.model === "string") {
|
||||
requestedModel = bodyForModel.model;
|
||||
}
|
||||
|
||||
@@ -728,13 +728,17 @@ async function buildUnifiedModelsResponseCore(
|
||||
// the fix, a provider with any synced model silently dropped ALL its
|
||||
// static models.
|
||||
const syncedForProvider = syncedModelIdsByCanonicalProvider.get(canonicalProviderId);
|
||||
const hasDeclaredEffortTiers =
|
||||
Array.isArray(model.supportedThinkingEfforts) &&
|
||||
model.supportedThinkingEfforts.length > 0;
|
||||
if (
|
||||
shouldSuppressStaticModelBySyncedCoverage({
|
||||
providerHasSynced: syncedForProvider !== undefined && syncedForProvider.size > 0,
|
||||
staticModelId: model.id,
|
||||
syncedModelIds: syncedForProvider ? [...syncedForProvider] : [],
|
||||
}) &&
|
||||
!isRegisteredEffortVariant(providerModels, model.id)
|
||||
!isRegisteredEffortVariant(providerModels, model.id) &&
|
||||
!hasDeclaredEffortTiers
|
||||
)
|
||||
continue;
|
||||
if (!providerSupportsModel(canonicalProviderId, model.id)) continue;
|
||||
@@ -745,6 +749,18 @@ async function buildUnifiedModelsResponseCore(
|
||||
|
||||
const visionFields =
|
||||
getVisionCapabilityFields(aliasId) || getVisionCapabilityFields(model.id);
|
||||
const thinkingFields = getThinkingCapabilityFields(
|
||||
canonicalProviderId,
|
||||
model.id,
|
||||
model.supportsReasoning,
|
||||
model.supportedThinkingEfforts,
|
||||
// Skip the canonical fallback for static models without declared tiers —
|
||||
// otherwise the catalog synthesizes unresolvable `<prefix>/<model>-{tier}`
|
||||
// ids for every static reasoning model across all providers (#9485 review).
|
||||
!hasDeclaredEffortTiers
|
||||
);
|
||||
const thinkingCapabilities =
|
||||
Object.keys(thinkingFields).length > 0 ? { capabilities: thinkingFields } : {};
|
||||
if (includeAlias) {
|
||||
models.push({
|
||||
id: aliasId,
|
||||
@@ -755,6 +771,8 @@ async function buildUnifiedModelsResponseCore(
|
||||
root: model.id,
|
||||
parent: null,
|
||||
...(visionFields || {}),
|
||||
...thinkingFields,
|
||||
...thinkingCapabilities,
|
||||
});
|
||||
}
|
||||
if (
|
||||
@@ -775,6 +793,8 @@ async function buildUnifiedModelsResponseCore(
|
||||
root: model.id,
|
||||
parent: includeAlias ? aliasId : null,
|
||||
...(providerVisionFields || {}),
|
||||
...thinkingFields,
|
||||
...thinkingCapabilities,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,19 +84,24 @@ export function getThinkingCapabilityFields(
|
||||
providerId: string,
|
||||
modelId: string,
|
||||
resolvedThinking?: boolean | null,
|
||||
supportedThinkingEfforts?: readonly string[]
|
||||
supportedThinkingEfforts?: readonly string[],
|
||||
/** When true, skip the canonical effort-tier fallback — used for static registry
|
||||
* models that declare `supportsReasoning` but no explicit tier list, so the
|
||||
* catalog does not synthesize unresolvable `<prefix>/<model>-{tier}` ids. */
|
||||
skipCanonicalEffortFallback = false
|
||||
): Record<string, boolean | string[]> {
|
||||
const supportsThinking = resolvedThinking;
|
||||
if (typeof supportsThinking !== "boolean") return {};
|
||||
const hasDeclaredTiers =
|
||||
supportedThinkingEfforts && supportedThinkingEfforts.length > 0;
|
||||
return {
|
||||
thinking: supportsThinking,
|
||||
supportsThinking,
|
||||
...(supportsThinking
|
||||
...(supportsThinking && (hasDeclaredTiers || !skipCanonicalEffortFallback)
|
||||
? {
|
||||
effort_tiers:
|
||||
supportedThinkingEfforts && supportedThinkingEfforts.length > 0
|
||||
? [...supportedThinkingEfforts]
|
||||
: extendCodexGpt56EffortValues(providerId, modelId, CANONICAL_EFFORT_VALUES),
|
||||
effort_tiers: hasDeclaredTiers
|
||||
? [...supportedThinkingEfforts!]
|
||||
: extendCodexGpt56EffortValues(providerId, modelId, CANONICAL_EFFORT_VALUES),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
@@ -98,7 +98,8 @@ export async function POST(request, { params }) {
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
body: JSON.stringify(body),
|
||||
signal: request.signal,
|
||||
});
|
||||
|
||||
return await handleChat(newRequest, buildClientRawRequest(request, rawBody));
|
||||
return await handleChat(newRequest, () => buildClientRawRequest(request, rawBody));
|
||||
}
|
||||
|
||||
@@ -89,9 +89,7 @@ export async function POST(request, { params }) {
|
||||
action = modelAction.includes(":streamGenerateContent")
|
||||
? ":streamGenerateContent"
|
||||
: ":generateContent";
|
||||
model = modelAction
|
||||
.replace(":streamGenerateContent", "")
|
||||
.replace(":generateContent", "");
|
||||
model = modelAction.replace(":streamGenerateContent", "").replace(":generateContent", "");
|
||||
}
|
||||
|
||||
const validation = validateBody(v1betaGeminiGenerateSchema, rawBody);
|
||||
@@ -113,9 +111,10 @@ export async function POST(request, { params }) {
|
||||
method: "POST",
|
||||
headers: request.headers,
|
||||
body: JSON.stringify(convertedBody),
|
||||
signal: request.signal,
|
||||
});
|
||||
|
||||
const response = await handleChat(newRequest, buildClientRawRequest(request, rawBody));
|
||||
const response = await handleChat(newRequest, () => buildClientRawRequest(request, rawBody));
|
||||
|
||||
if (stream) {
|
||||
// Transform OpenAI SSE => Gemini SSE on the fly. The @google/genai SDK
|
||||
|
||||
@@ -1,5 +1,63 @@
|
||||
import type { AdaptiveAdmissionPublicSnapshot } from "@omniroute/open-sse/services/admission/runtime.ts";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
/** Low-card adaptive-admission health summary — no tenant/request/body/queue details. */
|
||||
export type AdaptiveAdmissionHealthSummary = {
|
||||
mode: AdaptiveAdmissionPublicSnapshot["mode"];
|
||||
currentLimit: number;
|
||||
minLimit: number;
|
||||
maxLimit: number;
|
||||
activeCost: number;
|
||||
activeCount: number;
|
||||
queuedCost: number;
|
||||
queuedCount: number;
|
||||
admittedCount: number;
|
||||
rejectedCount: number;
|
||||
wouldAdmitCount: number;
|
||||
wouldQueueCount: number;
|
||||
wouldRejectCount: number;
|
||||
utilization: number;
|
||||
pressure: AdaptiveAdmissionPublicSnapshot["pressure"];
|
||||
resourceSeverity: AdaptiveAdmissionPublicSnapshot["resourceSeverity"];
|
||||
resourceReason: AdaptiveAdmissionPublicSnapshot["resourceReason"];
|
||||
resourceObservedAtMs: number;
|
||||
pressureGuardRejectCount: number;
|
||||
shutdown: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Explicit allowlisted projection of the public adaptive-admission snapshot.
|
||||
* Never spreads the snapshot — extra keys (tenant, body, queue items, paths) are dropped.
|
||||
*/
|
||||
export function projectAdaptiveAdmissionSummary(
|
||||
snapshot: AdaptiveAdmissionPublicSnapshot | null | undefined
|
||||
): AdaptiveAdmissionHealthSummary | null {
|
||||
if (!snapshot || typeof snapshot !== "object") return null;
|
||||
return {
|
||||
mode: snapshot.mode,
|
||||
currentLimit: snapshot.currentLimit,
|
||||
minLimit: snapshot.minLimit,
|
||||
maxLimit: snapshot.maxLimit,
|
||||
activeCost: snapshot.activeCost,
|
||||
activeCount: snapshot.activeCount,
|
||||
queuedCost: snapshot.queuedCost,
|
||||
queuedCount: snapshot.queuedCount,
|
||||
admittedCount: snapshot.admittedCount,
|
||||
rejectedCount: snapshot.rejectedCount,
|
||||
wouldAdmitCount: snapshot.wouldAdmitCount,
|
||||
wouldQueueCount: snapshot.wouldQueueCount,
|
||||
wouldRejectCount: snapshot.wouldRejectCount,
|
||||
utilization: snapshot.utilization,
|
||||
pressure: snapshot.pressure,
|
||||
resourceSeverity: snapshot.resourceSeverity,
|
||||
resourceReason: snapshot.resourceReason,
|
||||
resourceObservedAtMs: snapshot.resourceObservedAtMs,
|
||||
pressureGuardRejectCount: snapshot.pressureGuardRejectCount,
|
||||
shutdown: snapshot.shutdown,
|
||||
};
|
||||
}
|
||||
|
||||
interface CircuitBreakerStatus {
|
||||
name: string;
|
||||
state: string;
|
||||
@@ -88,6 +146,8 @@ interface BuildHealthPayloadOptions {
|
||||
unknown: number;
|
||||
stale: number;
|
||||
};
|
||||
/** Optional injected public adaptive-admission snapshot; projected, never raw-spread. */
|
||||
adaptiveAdmission?: AdaptiveAdmissionPublicSnapshot | null;
|
||||
}
|
||||
|
||||
function limitMonitors(monitors: QuotaMonitorSnapshot[], maxItems = 8): QuotaMonitorSnapshot[] {
|
||||
@@ -227,6 +287,7 @@ export function buildHealthPayload({
|
||||
activeSessions,
|
||||
activeSessionsByKey = {},
|
||||
credentialHealth,
|
||||
adaptiveAdmission = null,
|
||||
}: BuildHealthPayloadOptions) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const system = {
|
||||
@@ -321,6 +382,7 @@ export function buildHealthPayload({
|
||||
},
|
||||
sessions: buildSessionsSummary({ activeSessions, activeSessionsByKey }),
|
||||
credentialHealth, // may be undefined if credentialHealth module not loaded
|
||||
adaptiveAdmission: projectAdaptiveAdmissionSummary(adaptiveAdmission),
|
||||
dedup: {
|
||||
inflightRequests,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import { closeSync, mkdirSync, openSync, existsSync } from "node:fs";
|
||||
import { closeSync, mkdirSync, openSync, existsSync, readFileSync } from "node:fs";
|
||||
import { access } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
@@ -7,15 +7,36 @@ import { homedir } from "node:os";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/**
|
||||
* Check whether a directory's package.json is a valid project-root marker by
|
||||
* requiring a non-empty `name` field. The Next.js standalone build writes a
|
||||
* synthetic `.build/next/package.json` = `{"type":"commonjs"}` that should not
|
||||
* be mistaken for the real project root.
|
||||
*
|
||||
* Swallows read / parse errors (missing file, invalid JSON) and returns false
|
||||
* so the walk-up continues.
|
||||
*
|
||||
* @internal — exported for testability.
|
||||
*/
|
||||
export function isValidPackageMarker(dir: string): boolean {
|
||||
try {
|
||||
const content = readFileSync(path.join(dir, "package.json"), "utf-8");
|
||||
const pkg = JSON.parse(content);
|
||||
return typeof pkg.name === "string" && pkg.name.length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal — exported for testability. */
|
||||
export function resolveProjectRoot(
|
||||
fallback: string,
|
||||
startDir: string = typeof __dirname !== "undefined" ? __dirname : process.cwd()
|
||||
): string {
|
||||
const markers = ["package.json", ".git"] as const;
|
||||
let dir = path.resolve(startDir);
|
||||
while (true) {
|
||||
if (markers.some((m) => existsSync(path.join(dir, m)))) return dir;
|
||||
if (existsSync(path.join(dir, ".git"))) return dir;
|
||||
if (existsSync(path.join(dir, "package.json")) && isValidPackageMarker(dir)) return dir;
|
||||
const parent = path.dirname(dir);
|
||||
if (parent === dir) break;
|
||||
dir = parent;
|
||||
|
||||
@@ -8,7 +8,11 @@ import { applyCorsHeaders } from "../cors/origins";
|
||||
import { validateBrowserMutationOrigin } from "../origin/publicOrigin";
|
||||
import { classifyRoute } from "./classify";
|
||||
import { validateDashboardCsrfToken } from "./csrf";
|
||||
import { classifyStampedPeerLocality } from "./peerStamp";
|
||||
import {
|
||||
classifyStampedPeerLocality,
|
||||
resolveStampedPeer,
|
||||
resolveStampedViaProxy,
|
||||
} from "./peerStamp";
|
||||
import { checkRequestIP } from "@omniroute/open-sse/services/ipFilter.ts";
|
||||
import { clientApiPolicy } from "./policies/clientApi";
|
||||
import { managementPolicy } from "./policies/management";
|
||||
@@ -347,8 +351,23 @@ export async function runAuthzPipeline(
|
||||
// external surface. Loopback is exempt so the local operator can never lock
|
||||
// themselves out of the dashboard (they can always fix the list from
|
||||
// localhost). checkIP is a no-op when the filter is disabled.
|
||||
//
|
||||
// D1 (#9033): on a direct connection the proxy runtime has no socket, so
|
||||
// checkRequestIP reads only forwarding headers + undefined request.ip and
|
||||
// falls to "unknown", never blocking the blacklisted client. Resolve the
|
||||
// trusted peer IP from the authenticated stamp and pass it to checkRequestIP,
|
||||
// but only when NOT behind a reverse proxy (the via-proxy marker means the
|
||||
// peer IP is the proxy hop, e.g. 127.0.0.1, and the real client is in XFF).
|
||||
if (peerLocality !== "loopback") {
|
||||
const ipVerdict = checkRequestIP(request);
|
||||
const trustedPeerIp = resolveStampedPeer(
|
||||
request.headers.get(PEER_IP_HEADER),
|
||||
process.env.OMNIROUTE_PEER_STAMP_TOKEN
|
||||
);
|
||||
const viaProxy = resolveStampedViaProxy(
|
||||
request.headers.get(VIA_PROXY_HEADER),
|
||||
process.env.OMNIROUTE_PEER_STAMP_TOKEN
|
||||
);
|
||||
const ipVerdict = checkRequestIP(request, viaProxy ? null : trustedPeerIp);
|
||||
if (!ipVerdict.allowed) {
|
||||
const blocked = NextResponse.json(
|
||||
{ error: ipVerdict.reason || "Access denied" },
|
||||
|
||||
@@ -108,7 +108,7 @@ export const resetStatsActionSchema = z.object({
|
||||
action: z.literal("reset-stats"),
|
||||
});
|
||||
|
||||
export const ipFilterModeSchema = z.enum(["blacklist", "whitelist"]);
|
||||
export const ipFilterModeSchema = z.enum(["blacklist", "whitelist", "whitelist-priority"]);
|
||||
|
||||
export const tempBanSchema = z.object({
|
||||
ip: z.string().trim().min(1),
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import { resolveChatRequestBody } from "./requestBody";
|
||||
import * as chatAdmission from "./chatAdmission.ts";
|
||||
import { buildClientRawRequest, resolveDispatchClientRawRequest } from "./chat/clientRawRequest.ts";
|
||||
export { buildClientRawRequest, resolveDispatchClientRawRequest };
|
||||
import { normalizeReasoningRequest } from "@/shared/reasoning/effortStandardization";
|
||||
import { resolveRoutingModel, RoutingModelOps } from "./resolveRoutingModel";
|
||||
import {
|
||||
@@ -64,6 +67,7 @@ import { guardrailRegistry, resolveDisabledGuardrails } from "@/lib/guardrails";
|
||||
import {
|
||||
resolveModelOrError,
|
||||
checkPipelineGates,
|
||||
checkResourcePressureBeforeProviderWork,
|
||||
executeChatWithBreaker,
|
||||
handleNoCredentials,
|
||||
safeResolveProxy,
|
||||
@@ -232,16 +236,12 @@ const comboPromoteDeps = { updateCombo, info: log.info, warn: log.warn };
|
||||
|
||||
export { shouldTripProviderBreakerForResult } from "./chatPredicates";
|
||||
|
||||
/**
|
||||
* Handle chat completion request
|
||||
* Supports: OpenAI, Claude, Gemini, OpenAI Responses API formats
|
||||
* Format detection and translation handled by translator
|
||||
*/
|
||||
export async function handleChat(
|
||||
async function handleChatImplementation(
|
||||
request: any,
|
||||
clientRawRequest: any = null,
|
||||
preParsedBody: any = null,
|
||||
correlationId?: string
|
||||
correlationId: string | undefined,
|
||||
admissionContext: chatAdmission.ChatAdmissionContext
|
||||
) {
|
||||
const peerRejection = rejectPeerRequest(request?.headers, log.warn, errorResponse);
|
||||
if (peerRejection) return peerRejection;
|
||||
@@ -357,11 +357,7 @@ export async function handleChat(
|
||||
}
|
||||
}
|
||||
|
||||
// buildClientRawRequest already deep-clones the body, so pass `body` directly — the
|
||||
// prior local clone was a redundant second full-body copy on the hot path (#5152).
|
||||
if (!clientRawRequest) {
|
||||
clientRawRequest = buildClientRawRequest(request, body);
|
||||
}
|
||||
const deferredClientRawBody = chatAdmission.captureDeferredClientRawBody(body);
|
||||
|
||||
// T01 — Accept-header streaming opt-in (#302 / #5305). A bare `Accept:
|
||||
// text/event-stream` with `stream` omitted opts a curl/httpx-style client into
|
||||
@@ -488,6 +484,12 @@ export async function handleChat(
|
||||
const bypassProviderQuotaPolicy = hasProviderQuotaBypassScope(apiKeyInfo?.scopes);
|
||||
telemetry.endPhase();
|
||||
|
||||
const admissionRejection = await admissionContext.acquire(apiKeyInfo?.id, request, body);
|
||||
if (admissionRejection) return admissionRejection;
|
||||
clientRawRequest = chatAdmission.resolveClientRawAfterAdmission(clientRawRequest, () =>
|
||||
deferredClientRawBody.withClientBody((clientBody) => buildClientRawRequest(request, clientBody))
|
||||
);
|
||||
|
||||
// Guardrail pre-call pipeline — prompt injection, PII masking, and future custom rules.
|
||||
telemetry.startPhase("validate");
|
||||
const preCallGuardrails = await guardrailRegistry.runPreCallHooks(body, {
|
||||
@@ -972,18 +974,9 @@ export async function handleChat(
|
||||
return withCorrelationId(withSessionHeader(response, sessionId), reqId);
|
||||
}
|
||||
|
||||
// The clientRawRequest envelope lives in ./chat/clientRawRequest.ts. Imported for local use
|
||||
// below and re-exported for the historical public surface.
|
||||
import { buildClientRawRequest, resolveDispatchClientRawRequest } from "./chat/clientRawRequest.ts";
|
||||
export { buildClientRawRequest, resolveDispatchClientRawRequest };
|
||||
export const handleChat = chatAdmission.withChatAdmission(handleChatImplementation);
|
||||
|
||||
/**
|
||||
* Handle single model chat request
|
||||
*
|
||||
* Refactored: model resolution, logging, pipeline gates, and chat execution
|
||||
* extracted to focused helpers. This function orchestrates the credential
|
||||
* retry loop.
|
||||
*/
|
||||
/** Handle one resolved model through gates, credentials, and retry/fallback. */
|
||||
async function handleSingleModelChat(
|
||||
body: any,
|
||||
modelStr: string,
|
||||
@@ -1147,7 +1140,9 @@ async function handleSingleModelChat(
|
||||
? "fixed combo step connection"
|
||||
: undefined;
|
||||
|
||||
// 2. Pipeline gates (availability + provider circuit breaker)
|
||||
// 2. Local pressure precedes availability/breaker gates and account selection.
|
||||
const pressureGuard = checkResourcePressureBeforeProviderWork();
|
||||
if (pressureGuard) return pressureGuard.response;
|
||||
const providerProfile = await getRuntimeProviderProfile(provider);
|
||||
const gate = await checkPipelineGates(provider, model, {
|
||||
ignoreCircuitBreaker: forceLiveComboTest || hasForcedConnection,
|
||||
@@ -1426,7 +1421,7 @@ async function handleSingleModelChat(
|
||||
clientRawRequest,
|
||||
runtimeOptions.modelAbortSignal
|
||||
);
|
||||
const { result, tlsFingerprintUsed } = await executeChatWithBreaker({
|
||||
const execution = await executeChatWithBreaker({
|
||||
bypassCircuitBreaker: forceLiveComboTest || hasForcedConnection,
|
||||
breaker,
|
||||
body: requestBody,
|
||||
@@ -1455,6 +1450,10 @@ async function handleSingleModelChat(
|
||||
routingComboId: runtimeOptions?.routingComboId ?? null,
|
||||
});
|
||||
if (telemetry) telemetry.endPhase();
|
||||
if ("localResourcePressureResult" in execution) {
|
||||
return execution.localResourcePressureResult.response;
|
||||
}
|
||||
const { result, tlsFingerprintUsed } = execution;
|
||||
|
||||
const proxyLatency = Date.now() - proxyStartTime;
|
||||
const providerAlias = PROVIDER_ID_TO_ALIAS[provider] || provider;
|
||||
|
||||
239
src/sse/handlers/chatAdmission.ts
Normal file
239
src/sse/handlers/chatAdmission.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* Shared handleChat adaptive-admission lifecycle wrapper.
|
||||
*
|
||||
* Owns a per-call context that acquires exactly once after API-key policy and
|
||||
* attaches/releases the admitted lease around the handler response or throw.
|
||||
* No AsyncLocalStorage, no route registry — one higher-order wrapper only.
|
||||
*/
|
||||
|
||||
import {
|
||||
getAdaptiveAdmissionRuntime,
|
||||
type AdaptiveAdmissionAdmitted,
|
||||
type AdaptiveAdmissionFailureOutcome,
|
||||
type AdaptiveAdmissionRuntime,
|
||||
} from "@omniroute/open-sse/services/admission/runtime.ts";
|
||||
|
||||
/** Single fairness bucket for unauthenticated / keyless traffic. Opaque; never a raw key. */
|
||||
export const ANONYMOUS_ADMISSION_TENANT_KEY = "anonymous";
|
||||
|
||||
export type ChatAdmissionContext = {
|
||||
/**
|
||||
* Acquire once against the process runtime.
|
||||
* Returns a sanitized rejection Response, or null when admitted / already acquired.
|
||||
*/
|
||||
acquire(
|
||||
apiKeyId: string | null | undefined,
|
||||
request: { signal?: AbortSignal | null },
|
||||
body: unknown
|
||||
): Promise<Response | null>;
|
||||
};
|
||||
|
||||
type AdmittedState = {
|
||||
runtime: AdaptiveAdmissionRuntime;
|
||||
admitted: AdaptiveAdmissionAdmitted;
|
||||
};
|
||||
|
||||
export function resolveAdmissionTenantKey(apiKeyId: string | null | undefined): string {
|
||||
return typeof apiKeyId === "string" && apiKeyId.length > 0
|
||||
? apiKeyId
|
||||
: ANONYMOUS_ADMISSION_TENANT_KEY;
|
||||
}
|
||||
|
||||
const CANCEL_NAMES = new Set(["AbortError"]);
|
||||
const CANCEL_CODES = new Set(["ABORT_ERR", "ERR_CANCELED"]);
|
||||
const TIMEOUT_NAMES = new Set(["TimeoutError"]);
|
||||
const TIMEOUT_CODES = new Set(["ETIMEDOUT", "ESOCKETTIMEDOUT", "TIMEOUT", "ERR_TIMEOUT"]);
|
||||
|
||||
function asStringField(err: object, key: string): string {
|
||||
const value = (err as Record<string, unknown>)[key];
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function asStatus(err: object): number | null {
|
||||
const status = (err as Record<string, unknown>).status;
|
||||
if (typeof status === "number") return status;
|
||||
const statusCode = (err as Record<string, unknown>).statusCode;
|
||||
return typeof statusCode === "number" ? statusCode : null;
|
||||
}
|
||||
|
||||
/** Classify thrown handler failure; never exposes raw errors to clients. */
|
||||
export function classifyHandlerFailure(
|
||||
err: unknown,
|
||||
signal?: AbortSignal | null
|
||||
): AdaptiveAdmissionFailureOutcome {
|
||||
if (signal?.aborted) return "cancelled";
|
||||
if (!err || typeof err !== "object") return "upstream_error";
|
||||
|
||||
const name = asStringField(err, "name");
|
||||
const code = asStringField(err, "code");
|
||||
if (CANCEL_NAMES.has(name) || CANCEL_CODES.has(code)) return "cancelled";
|
||||
if (TIMEOUT_NAMES.has(name) || TIMEOUT_CODES.has(code)) return "timeout";
|
||||
|
||||
const status = asStatus(err);
|
||||
if (status === 408 || status === 504) return "timeout";
|
||||
if (status !== null && status >= 400 && status < 500) return "local_reject";
|
||||
return "upstream_error";
|
||||
}
|
||||
|
||||
const CLIENT_RAW_MUTABLE_FIELDS = ["model", "reasoning", "reasoning_effort", "thinking"] as const;
|
||||
type ClientRawFieldState = {
|
||||
key: (typeof CLIENT_RAW_MUTABLE_FIELDS)[number];
|
||||
present: boolean;
|
||||
value: unknown;
|
||||
};
|
||||
|
||||
function captureClientRawFields(body: Record<string, unknown>): ClientRawFieldState[] {
|
||||
return CLIENT_RAW_MUTABLE_FIELDS.map((key) => {
|
||||
const present = Object.hasOwn(body, key);
|
||||
return { key, present, value: present ? body[key] : undefined };
|
||||
});
|
||||
}
|
||||
|
||||
function clientRawFieldsEqual(a: ClientRawFieldState[], b: ClientRawFieldState[]): boolean {
|
||||
return a.every(
|
||||
(field, index) =>
|
||||
field.key === b[index]?.key &&
|
||||
field.present === b[index]?.present &&
|
||||
Object.is(field.value, b[index]?.value)
|
||||
);
|
||||
}
|
||||
|
||||
function applyClientRawFields(body: Record<string, unknown>, fields: ClientRawFieldState[]): void {
|
||||
for (const field of fields) {
|
||||
if (field.present) body[field.key] = field.value;
|
||||
else delete body[field.key];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture only the fixed fields mutated before admission. The full bounded observability
|
||||
* snapshot is built after admission without enumerating or cloning the body beforehand.
|
||||
*/
|
||||
export function captureDeferredClientRawBody(body: unknown): {
|
||||
withClientBody<T>(build: (clientBody: unknown) => T): T;
|
||||
} {
|
||||
const target =
|
||||
body !== null && typeof body === "object" ? (body as Record<string, unknown>) : null;
|
||||
const originalFields = target ? captureClientRawFields(target) : null;
|
||||
|
||||
return {
|
||||
withClientBody(build) {
|
||||
if (!target || !originalFields) return build(body);
|
||||
const workingFields = captureClientRawFields(target);
|
||||
if (clientRawFieldsEqual(originalFields, workingFields)) return build(target);
|
||||
|
||||
applyClientRawFields(target, originalFields);
|
||||
try {
|
||||
return build(target);
|
||||
} finally {
|
||||
applyClientRawFields(target, workingFields);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Resolve lazy/eager client-raw after admission; invoke factories at most once. */
|
||||
export function resolveClientRawAfterAdmission(
|
||||
clientRawRequest: unknown,
|
||||
build: () => unknown
|
||||
): unknown {
|
||||
if (typeof clientRawRequest === "function") {
|
||||
return (clientRawRequest as () => unknown)();
|
||||
}
|
||||
if (clientRawRequest) return clientRawRequest;
|
||||
return build();
|
||||
}
|
||||
|
||||
export function createChatAdmissionContext(
|
||||
getRuntime: () => AdaptiveAdmissionRuntime = getAdaptiveAdmissionRuntime
|
||||
): ChatAdmissionContext & { getAdmittedState(): AdmittedState | null } {
|
||||
let state: AdmittedState | null = null;
|
||||
let acquireStarted = false;
|
||||
|
||||
return {
|
||||
getAdmittedState: () => state,
|
||||
async acquire(apiKeyId, request, body) {
|
||||
// Exactly once per logical request — never re-enter the runtime.
|
||||
if (state || acquireStarted) return null;
|
||||
acquireStarted = true;
|
||||
|
||||
const runtime = getRuntime();
|
||||
const streaming =
|
||||
body !== null && typeof body === "object" && (body as { stream?: unknown }).stream === true;
|
||||
|
||||
const result = await runtime.acquire({
|
||||
tenantKey: resolveAdmissionTenantKey(apiKeyId),
|
||||
body,
|
||||
signal: request?.signal ?? undefined,
|
||||
streaming,
|
||||
});
|
||||
|
||||
if (result.status === "rejected") {
|
||||
return result.response;
|
||||
}
|
||||
|
||||
state = { runtime, admitted: result };
|
||||
return null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type HandleChatImplementation = (
|
||||
request: any,
|
||||
clientRawRequest: any,
|
||||
preParsedBody: any,
|
||||
correlationId: string | undefined,
|
||||
admissionContext: ChatAdmissionContext
|
||||
) => Promise<Response>;
|
||||
|
||||
export type WithChatAdmissionOptions = {
|
||||
/** Test seam: override process-global runtime resolution. */
|
||||
getRuntime?: () => AdaptiveAdmissionRuntime;
|
||||
};
|
||||
|
||||
/**
|
||||
* Thin public wrapper: create per-call context, run implementation, attach/release lease.
|
||||
*/
|
||||
export function withChatAdmission(
|
||||
implementation: HandleChatImplementation,
|
||||
options: WithChatAdmissionOptions = {}
|
||||
) {
|
||||
return async function handleChat(
|
||||
request: any,
|
||||
clientRawRequest: any = null,
|
||||
preParsedBody: any = null,
|
||||
correlationId?: string
|
||||
): Promise<Response> {
|
||||
const admissionContext = createChatAdmissionContext(
|
||||
options.getRuntime ?? getAdaptiveAdmissionRuntime
|
||||
);
|
||||
try {
|
||||
const response = await implementation(
|
||||
request,
|
||||
clientRawRequest,
|
||||
preParsedBody,
|
||||
correlationId,
|
||||
admissionContext
|
||||
);
|
||||
const admittedState = admissionContext.getAdmittedState();
|
||||
if (!admittedState) return response;
|
||||
|
||||
const { runtime, admitted } = admittedState;
|
||||
return runtime.attachResponseLifecycle(response, admitted.lease, {
|
||||
admittedAtMs: admitted.admittedAtMs,
|
||||
signal: request?.signal ?? undefined,
|
||||
});
|
||||
} catch (err) {
|
||||
const admittedState = admissionContext.getAdmittedState();
|
||||
if (admittedState) {
|
||||
const { runtime, admitted } = admittedState;
|
||||
runtime.releaseHandlerFailure(
|
||||
admitted.lease,
|
||||
classifyHandlerFailure(err, request?.signal),
|
||||
{ admittedAtMs: admitted.admittedAtMs }
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -13,6 +13,10 @@ import {
|
||||
PROVIDER_ID_TO_ALIAS,
|
||||
} from "@omniroute/open-sse/config/providerModels.ts";
|
||||
import { handleChatCore } from "@omniroute/open-sse/handlers/chatCore.ts";
|
||||
import {
|
||||
checkResourcePressureGuard,
|
||||
type ResourcePressureGuardResult,
|
||||
} from "@omniroute/open-sse/utils/resourcePressure.ts";
|
||||
import {
|
||||
errorResponse,
|
||||
modelCooldownResponse,
|
||||
@@ -64,6 +68,10 @@ type ExecuteChatWithBreakerOptions = {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
type ExecuteChatWithBreakerResult =
|
||||
| { result: any; tlsFingerprintUsed: boolean }
|
||||
| { localResourcePressureResult: ResourcePressureGuardResult; tlsFingerprintUsed: false };
|
||||
|
||||
function getHeaderValue(headers: Record<string, unknown> | null | undefined, name: string) {
|
||||
if (!headers || typeof headers !== "object") return "";
|
||||
const lowerName = name.toLowerCase();
|
||||
@@ -368,6 +376,14 @@ export async function checkPipelineGates(
|
||||
return null;
|
||||
}
|
||||
|
||||
export function checkResourcePressureBeforeProviderWork(): ResourcePressureGuardResult | null {
|
||||
try {
|
||||
return checkResourcePressureGuard();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeChatWithBreaker({
|
||||
bypassCircuitBreaker,
|
||||
breaker,
|
||||
@@ -396,7 +412,7 @@ export async function executeChatWithBreaker({
|
||||
correlationId = null,
|
||||
modelPinned = false,
|
||||
routingComboId = null,
|
||||
}: ExecuteChatWithBreakerOptions): Promise<{ result: any; tlsFingerprintUsed: boolean }> {
|
||||
}: ExecuteChatWithBreakerOptions): Promise<ExecuteChatWithBreakerResult> {
|
||||
let tlsFingerprintUsed = false;
|
||||
const normalizedTrafficType: TrafficType =
|
||||
typeof trafficType === "string" && trafficType.trim().toLowerCase() === "shadow"
|
||||
@@ -410,6 +426,11 @@ export async function executeChatWithBreaker({
|
||||
const capture = <T>(fn: () => T): T =>
|
||||
appliedProxySink ? runWithAppliedProxyCapture(appliedProxySink, fn) : fn();
|
||||
|
||||
const pressureGuard = checkResourcePressureBeforeProviderWork();
|
||||
if (pressureGuard) {
|
||||
return { localResourcePressureResult: pressureGuard, tlsFingerprintUsed: false };
|
||||
}
|
||||
|
||||
try {
|
||||
const chatFn = () =>
|
||||
capture(() =>
|
||||
@@ -434,6 +455,7 @@ export async function executeChatWithBreaker({
|
||||
correlationId,
|
||||
modelPinned,
|
||||
routingComboId,
|
||||
skipResourcePressureGuard: true,
|
||||
onCredentialsRefreshed: async (newCreds: any) => {
|
||||
await updateProviderCredentials(credentials.connectionId, {
|
||||
accessToken: newCreds.accessToken,
|
||||
|
||||
@@ -121,6 +121,37 @@ function isSyncedEffortSkippedProvider(providerId: string): boolean {
|
||||
return SYNCED_EFFORT_SKIP_PROVIDER_PREFIXES.some((prefix) => providerId.startsWith(prefix));
|
||||
}
|
||||
|
||||
/** Resolve a suffix against an explicitly tiered static registry model. */
|
||||
function resolveRegistryModelIdAndEffort(
|
||||
providerId: string,
|
||||
modelId: string
|
||||
): { modelId: string; effort: string | null } {
|
||||
if (isSyncedEffortSkippedProvider(providerId)) return { modelId, effort: null };
|
||||
|
||||
const registryModels = REGISTRY[providerId]?.models;
|
||||
if (!Array.isArray(registryModels)) return { modelId, effort: null };
|
||||
if (registryModels.some((candidate) => candidate?.id === modelId)) {
|
||||
return { modelId, effort: null };
|
||||
}
|
||||
|
||||
for (const candidate of registryModels) {
|
||||
if (!Array.isArray(candidate?.supportedThinkingEfforts)) continue;
|
||||
const attempt = splitSyncedEffortSuffix(modelId, candidate.supportedThinkingEfforts);
|
||||
if (attempt.effort && attempt.baseModel === candidate.id) {
|
||||
return { modelId: attempt.baseModel, effort: attempt.effort };
|
||||
}
|
||||
}
|
||||
|
||||
return { modelId, effort: null };
|
||||
}
|
||||
|
||||
function findRegistryModel(providerId: string, modelId: string): any {
|
||||
const registryModels = REGISTRY[providerId]?.models;
|
||||
return Array.isArray(registryModels)
|
||||
? registryModels.find((candidate) => candidate?.id === modelId)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* #7694: when `modelId` has no direct synced-model match, try stripping a trailing
|
||||
* `-{effort}` token by testing it against each candidate synced model's OWN declared
|
||||
@@ -189,7 +220,11 @@ function copySyncedThinkingMetadata(metadata: RuntimeModelMeta, syncedMatch: any
|
||||
metadata.supportsThinking = syncedMatch.supportsThinking;
|
||||
}
|
||||
if (syncedMatch?.alwaysThinking === true) metadata.alwaysThinking = true;
|
||||
if (Array.isArray(syncedMatch?.supportedThinkingEfforts)) {
|
||||
// Only let a non-empty synced effort list override the static registry fallback;
|
||||
// an empty array from an incomplete synced discovery must not erase registry-declared
|
||||
// tiers (#9485 review).
|
||||
if (Array.isArray(syncedMatch?.supportedThinkingEfforts) &&
|
||||
syncedMatch.supportedThinkingEfforts.length > 0) {
|
||||
metadata.supportedThinkingEfforts = syncedMatch.supportedThinkingEfforts;
|
||||
}
|
||||
if (typeof syncedMatch?.defaultThinkingEffort === "string") {
|
||||
@@ -197,8 +232,22 @@ function copySyncedThinkingMetadata(metadata: RuntimeModelMeta, syncedMatch: any
|
||||
}
|
||||
}
|
||||
|
||||
function buildRuntimeModelMeta(customMatch: any, syncedMatch: any): RuntimeModelMeta {
|
||||
function copyRegistryThinkingMetadata(metadata: RuntimeModelMeta, registryMatch: any): void {
|
||||
if (typeof registryMatch?.supportsReasoning === "boolean") {
|
||||
metadata.supportsThinking = registryMatch.supportsReasoning;
|
||||
}
|
||||
if (Array.isArray(registryMatch?.supportedThinkingEfforts)) {
|
||||
metadata.supportedThinkingEfforts = [...registryMatch.supportedThinkingEfforts];
|
||||
}
|
||||
}
|
||||
|
||||
function buildRuntimeModelMeta(
|
||||
customMatch: any,
|
||||
syncedMatch: any,
|
||||
registryMatch: any
|
||||
): RuntimeModelMeta {
|
||||
const metadata = resolveRuntimeFormats(customMatch, syncedMatch);
|
||||
copyRegistryThinkingMetadata(metadata, registryMatch);
|
||||
copySyncedThinkingMetadata(metadata, syncedMatch);
|
||||
return metadata;
|
||||
}
|
||||
@@ -215,16 +264,34 @@ async function lookupModelMeta(
|
||||
// #7694: no direct match on the raw modelId? try a synced-declared `-{effort}`
|
||||
// suffix before falling back to the literal id, so `<prefix>/<model>-<tier>`
|
||||
// resolves to the real base model + a resolved effort.
|
||||
const { modelId: resolvedModelId, effort } = resolveSyncedModelIdAndEffort(
|
||||
// #7694: no direct match on the raw modelId? try a synced-declared `-{effort}`
|
||||
// suffix before falling back to the literal id, so `<prefix>/<model>-<tier>`
|
||||
// resolves to the real base model + a resolved effort.
|
||||
let { modelId: resolvedModelId, effort } = resolveSyncedModelIdAndEffort(
|
||||
providerId,
|
||||
modelId,
|
||||
syncedModels
|
||||
);
|
||||
// Short-circuit registry suffix resolution when the raw id is already a direct
|
||||
// custom or synced model — otherwise a model literally named
|
||||
// `deepseek-v4-flash-low` gets rewritten to `deepseek-v4-flash` + effort `low`
|
||||
// and its custom/synced metadata (apiFormat/targetFormat) is dropped (#9485 review).
|
||||
if (
|
||||
!effort &&
|
||||
resolvedModelId === modelId &&
|
||||
!findCustomModelMeta(customModels, modelId) &&
|
||||
!findSyncedModelMeta(syncedModels, modelId)
|
||||
) {
|
||||
const registryResolution = resolveRegistryModelIdAndEffort(providerId, modelId);
|
||||
resolvedModelId = registryResolution.modelId;
|
||||
effort = registryResolution.effort;
|
||||
}
|
||||
// #7364: exact match first; retain the case-insensitive custom-model fallback
|
||||
// while also consulting the API-synced catalog for Kimi runtime metadata.
|
||||
const customMatch = findCustomModelMeta(customModels, resolvedModelId);
|
||||
const syncedMatch = findSyncedModelMeta(syncedModels, resolvedModelId);
|
||||
const metadata = buildRuntimeModelMeta(customMatch, syncedMatch);
|
||||
const registryMatch = findRegistryModel(providerId, resolvedModelId);
|
||||
const metadata = buildRuntimeModelMeta(customMatch, syncedMatch, registryMatch);
|
||||
if (effort) metadata.resolvedThinkingEffort = effort;
|
||||
return { modelId: resolvedModelId, metadata };
|
||||
} catch {
|
||||
|
||||
@@ -39,9 +39,7 @@
|
||||
"incremental": true,
|
||||
"incrementalFile": "reports/mutation/stryker-incremental.json",
|
||||
"testRunner": "tap",
|
||||
"plugins": [
|
||||
"@stryker-mutator/tap-runner"
|
||||
],
|
||||
"plugins": ["@stryker-mutator/tap-runner"],
|
||||
"tap": {
|
||||
"testFiles": [
|
||||
"tests/unit/7993-noauth-proxy-routing.test.ts",
|
||||
@@ -59,6 +57,8 @@
|
||||
"tests/unit/account-fallback-route-restriction-403.test.ts",
|
||||
"tests/unit/account-fallback-service.test.ts",
|
||||
"tests/unit/accountfallback-ratelimit-400-4976.test.ts",
|
||||
"tests/unit/adaptive-admission-route-matrix.test.ts",
|
||||
"tests/unit/adaptive-admission-runtime.test.ts",
|
||||
"tests/unit/adobe-firefly.test.ts",
|
||||
"tests/unit/anthropic-thinking-signature-recovery.test.ts",
|
||||
"tests/unit/antigravity-429-quota-tdd.test.ts",
|
||||
@@ -84,6 +84,7 @@
|
||||
"tests/unit/bug-7940-gemini-retrydelay.test.ts",
|
||||
"tests/unit/build/check-circular-deps.test.ts",
|
||||
"tests/unit/cache-sweeps.test.ts",
|
||||
"tests/unit/chat-adaptive-admission-binding.test.ts",
|
||||
"tests/unit/cc-bridge-openai-image-7777.test.ts",
|
||||
"tests/unit/cc-compatible-provider.test.ts",
|
||||
"tests/unit/chat-combo-live-test.test.ts",
|
||||
@@ -184,6 +185,7 @@
|
||||
"tests/unit/combo/auto-quota-cutoff.test.ts",
|
||||
"tests/unit/combo/auto-status-penalty-4540.test.ts",
|
||||
"tests/unit/combo/combo-exhausted-skip.test.ts",
|
||||
"tests/unit/combo/combo-target-timeout-standards.test.ts",
|
||||
"tests/unit/combo/effective-max-concurrency.test.ts",
|
||||
"tests/unit/combo/recovery-hint.test.ts",
|
||||
"tests/unit/complexity-aware-scoring-wiring.test.ts",
|
||||
@@ -200,6 +202,7 @@
|
||||
"tests/unit/error-classification.test.ts",
|
||||
"tests/unit/error-message-sanitization.test.ts",
|
||||
"tests/unit/error-sensitive-redaction.test.ts",
|
||||
"tests/unit/execute-chat-resource-pressure-breaker.test.ts",
|
||||
"tests/unit/executor-antigravity.test.ts",
|
||||
"tests/unit/executor-web-cookie-sweep.test.ts",
|
||||
"tests/unit/format-provider-error-cause.test.ts",
|
||||
@@ -418,11 +421,7 @@
|
||||
".worktrees",
|
||||
".stryker-tmp"
|
||||
],
|
||||
"reporters": [
|
||||
"progress",
|
||||
"html",
|
||||
"json"
|
||||
],
|
||||
"reporters": ["progress", "html", "json"],
|
||||
"htmlReporter": {
|
||||
"fileName": "reports/mutation/mutation.html"
|
||||
},
|
||||
|
||||
@@ -24,8 +24,13 @@ const { GET, DELETE } = await import("../../src/app/api/monitoring/health/route.
|
||||
|
||||
async function healthTimestamp(): Promise<string> {
|
||||
const res = await GET();
|
||||
const body = (await res.json()) as { timestamp?: string };
|
||||
const body = (await res.json()) as {
|
||||
timestamp?: string;
|
||||
adaptiveAdmission?: unknown;
|
||||
};
|
||||
assert.ok(body.timestamp, "health payload should carry a timestamp");
|
||||
// Adaptive admission is always projected (summary object or null) — never omitted.
|
||||
assert.ok("adaptiveAdmission" in body, "health payload must include adaptiveAdmission");
|
||||
return body.timestamp as string;
|
||||
}
|
||||
|
||||
@@ -54,7 +59,7 @@ test("DELETE (circuit-breaker reset) invalidates the cache immediately", async (
|
||||
new Request("http://localhost/api/monitoring/health", {
|
||||
method: "DELETE",
|
||||
headers: { cookie: `auth_token=${authToken}` },
|
||||
}),
|
||||
})
|
||||
);
|
||||
assert.ok(delRes.status < 400, `DELETE should succeed, got ${delRes.status}`);
|
||||
await new Promise((r) => setTimeout(r, 5)); // ensure the clock advances past ms precision
|
||||
|
||||
@@ -326,20 +326,20 @@
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28,advisor-tool-2026-03-01,extended-cache-ttl-2025-04-11,cache-diagnosis-2026-04-07",
|
||||
"Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28,advisor-tool-2026-03-01,extended-cache-ttl-2025-04-11,cache-diagnosis-2026-04-07,code-execution-2025-08-25,skills-2025-10-02",
|
||||
"Anthropic-Version": "2023-06-01",
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": "<CRED>"
|
||||
},
|
||||
"nonStream": {
|
||||
"Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28,advisor-tool-2026-03-01,extended-cache-ttl-2025-04-11,cache-diagnosis-2026-04-07",
|
||||
"Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28,advisor-tool-2026-03-01,extended-cache-ttl-2025-04-11,cache-diagnosis-2026-04-07,code-execution-2025-08-25,skills-2025-10-02",
|
||||
"Anthropic-Version": "2023-06-01",
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": "<CRED>"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28,advisor-tool-2026-03-01,extended-cache-ttl-2025-04-11,cache-diagnosis-2026-04-07",
|
||||
"Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28,advisor-tool-2026-03-01,extended-cache-ttl-2025-04-11,cache-diagnosis-2026-04-07,code-execution-2025-08-25,skills-2025-10-02",
|
||||
"Anthropic-Version": "2023-06-01",
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": "<CRED>"
|
||||
@@ -870,7 +870,7 @@
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Anthropic-Beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28,advisor-tool-2026-03-01,extended-cache-ttl-2025-04-11,cache-diagnosis-2026-04-07",
|
||||
"Anthropic-Beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28,advisor-tool-2026-03-01,extended-cache-ttl-2025-04-11,cache-diagnosis-2026-04-07,code-execution-2025-08-25,skills-2025-10-02",
|
||||
"Anthropic-Dangerous-Direct-Browser-Access": "true",
|
||||
"Anthropic-Version": "2023-06-01",
|
||||
"Content-Type": "application/json",
|
||||
@@ -888,7 +888,7 @@
|
||||
"x-api-key": "<CRED>"
|
||||
},
|
||||
"nonStream": {
|
||||
"Anthropic-Beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28,advisor-tool-2026-03-01,extended-cache-ttl-2025-04-11,cache-diagnosis-2026-04-07",
|
||||
"Anthropic-Beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28,advisor-tool-2026-03-01,extended-cache-ttl-2025-04-11,cache-diagnosis-2026-04-07,code-execution-2025-08-25,skills-2025-10-02",
|
||||
"Anthropic-Dangerous-Direct-Browser-Access": "true",
|
||||
"Anthropic-Version": "2023-06-01",
|
||||
"Content-Type": "application/json",
|
||||
@@ -907,7 +907,7 @@
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Anthropic-Beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28,advisor-tool-2026-03-01,extended-cache-ttl-2025-04-11,cache-diagnosis-2026-04-07",
|
||||
"Anthropic-Beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28,advisor-tool-2026-03-01,extended-cache-ttl-2025-04-11,cache-diagnosis-2026-04-07,code-execution-2025-08-25,skills-2025-10-02",
|
||||
"Anthropic-Dangerous-Direct-Browser-Access": "true",
|
||||
"Anthropic-Version": "2023-06-01",
|
||||
"Content-Type": "application/json",
|
||||
|
||||
985
tests/unit/adaptive-admission-controller.test.ts
Normal file
985
tests/unit/adaptive-admission-controller.test.ts
Normal file
@@ -0,0 +1,985 @@
|
||||
import { describe, it, beforeEach, afterEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
AdaptiveAdmissionController,
|
||||
createAdmissionRejectError,
|
||||
type AdaptiveAdmissionConfig,
|
||||
type AdmissionLease,
|
||||
type AdmissionPressure,
|
||||
type AdmissionRequest,
|
||||
} from "../../open-sse/services/admission/index.ts";
|
||||
|
||||
class FakeClock {
|
||||
nowMs = 0;
|
||||
private nextId = 1;
|
||||
private timers = new Map<number, { due: number; fn: () => void }>();
|
||||
|
||||
now = () => this.nowMs;
|
||||
|
||||
setTimer = (fn: () => void, delayMs: number): number => {
|
||||
const id = this.nextId++;
|
||||
this.timers.set(id, { due: this.nowMs + Math.max(0, delayMs), fn });
|
||||
return id;
|
||||
};
|
||||
|
||||
clearTimer = (id: number): void => {
|
||||
this.timers.delete(id);
|
||||
};
|
||||
|
||||
get pendingTimerCount(): number {
|
||||
return this.timers.size;
|
||||
}
|
||||
|
||||
advance(ms: number): void {
|
||||
const target = this.nowMs + ms;
|
||||
while (true) {
|
||||
let nextId: number | undefined;
|
||||
let nextDue = Number.POSITIVE_INFINITY;
|
||||
for (const [id, t] of this.timers) {
|
||||
if (t.due <= target && t.due < nextDue) {
|
||||
nextDue = t.due;
|
||||
nextId = id;
|
||||
}
|
||||
}
|
||||
if (nextId === undefined) {
|
||||
this.nowMs = target;
|
||||
return;
|
||||
}
|
||||
const timer = this.timers.get(nextId)!;
|
||||
this.timers.delete(nextId);
|
||||
this.nowMs = timer.due;
|
||||
timer.fn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function baseConfig(overrides: Partial<AdaptiveAdmissionConfig> = {}): AdaptiveAdmissionConfig {
|
||||
return {
|
||||
mode: "enforce",
|
||||
minLimit: 10,
|
||||
maxLimit: 100,
|
||||
initialLimit: 20,
|
||||
maxQueueCount: 4,
|
||||
maxQueueCost: 40,
|
||||
defaultMaxWaitMs: 1000,
|
||||
windowMs: 100,
|
||||
shortLatencyAlpha: 0.5,
|
||||
longLatencyAlpha: 0.1,
|
||||
increaseStep: 2,
|
||||
decreaseFactor: 0.8,
|
||||
criticalDecreaseFactor: 0.5,
|
||||
highUtilizationThreshold: 0.7,
|
||||
lowUtilizationThreshold: 0.3,
|
||||
latencyGradientThreshold: 0.25,
|
||||
maxIncreasePerWindow: 4,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function req(partial: Partial<AdmissionRequest> & { cost: number }): AdmissionRequest {
|
||||
return {
|
||||
tenantKey: "t-default",
|
||||
...partial,
|
||||
};
|
||||
}
|
||||
|
||||
async function mustAdmit(
|
||||
controller: AdaptiveAdmissionController,
|
||||
request: AdmissionRequest
|
||||
): Promise<AdmissionLease> {
|
||||
const result = await controller.acquire(request);
|
||||
assert.equal(result.status, "admitted");
|
||||
if (result.status !== "admitted") throw new Error("expected admitted");
|
||||
return result.lease;
|
||||
}
|
||||
|
||||
describe("AdaptiveAdmissionController config and modes", () => {
|
||||
let clock: FakeClock;
|
||||
beforeEach(() => {
|
||||
clock = new FakeClock();
|
||||
});
|
||||
|
||||
function make(overrides: Partial<AdaptiveAdmissionConfig> = {}) {
|
||||
return new AdaptiveAdmissionController(baseConfig(overrides), {
|
||||
now: clock.now,
|
||||
setTimer: clock.setTimer,
|
||||
clearTimer: clock.clearTimer,
|
||||
});
|
||||
}
|
||||
|
||||
it("validates safe-integer bounds and ordered adaptation parameters", () => {
|
||||
assert.throws(() => make({ minLimit: 50, maxLimit: 10 }), /minLimit/);
|
||||
for (const invalid of [0.5, Number.POSITIVE_INFINITY, Number.MAX_VALUE]) {
|
||||
assert.throws(() => make({ maxQueueCount: invalid }), /maxQueueCount/);
|
||||
assert.throws(() => make({ initialLimit: invalid }), /initialLimit/);
|
||||
}
|
||||
assert.throws(() => make({ decreaseFactor: 1.2 }), /decreaseFactor/);
|
||||
assert.throws(
|
||||
() => make({ decreaseFactor: 0.5, criticalDecreaseFactor: 0.8 }),
|
||||
/criticalDecreaseFactor/
|
||||
);
|
||||
assert.throws(
|
||||
() => make({ lowUtilizationThreshold: 0.8, highUtilizationThreshold: 0.7 }),
|
||||
/lowUtilizationThreshold/
|
||||
);
|
||||
assert.throws(
|
||||
() => make({ shortLatencyAlpha: 0.1, longLatencyAlpha: 0.5 }),
|
||||
/shortLatencyAlpha/
|
||||
);
|
||||
});
|
||||
|
||||
it("clamps initial limit into [minLimit, maxLimit]", () => {
|
||||
const low = make({ initialLimit: 1, minLimit: 10 });
|
||||
assert.equal(low.snapshot().currentLimit, 10);
|
||||
low.shutdown();
|
||||
const high = make({ initialLimit: 999, maxLimit: 100 });
|
||||
assert.equal(high.snapshot().currentLimit, 100);
|
||||
high.shutdown();
|
||||
});
|
||||
|
||||
it("mode off never accounts cost or rejects", async () => {
|
||||
const c = make({ mode: "off", initialLimit: 5 });
|
||||
const a = await c.acquire(req({ cost: 100 }));
|
||||
const b = await c.acquire(req({ cost: 100 }));
|
||||
assert.equal(a.status, "admitted");
|
||||
assert.equal(b.status, "admitted");
|
||||
const snap = c.snapshot();
|
||||
assert.equal(snap.activeCost, 0);
|
||||
assert.equal(snap.activeCount, 0);
|
||||
assert.equal(snap.rejectedCount, 0);
|
||||
c.shutdown();
|
||||
});
|
||||
|
||||
it("defaults to shadow mode when mode omitted", () => {
|
||||
const c = new AdaptiveAdmissionController(
|
||||
{
|
||||
minLimit: 10,
|
||||
maxLimit: 100,
|
||||
initialLimit: 20,
|
||||
maxQueueCount: 2,
|
||||
maxQueueCost: 20,
|
||||
} as AdaptiveAdmissionConfig,
|
||||
{ now: clock.now, setTimer: clock.setTimer, clearTimer: clock.clearTimer }
|
||||
);
|
||||
assert.equal(c.snapshot().mode, "shadow");
|
||||
c.shutdown();
|
||||
});
|
||||
});
|
||||
|
||||
describe("shadow mode semantics", () => {
|
||||
let clock: FakeClock;
|
||||
beforeEach(() => {
|
||||
clock = new FakeClock();
|
||||
});
|
||||
|
||||
it("never rejects or delays while recording would-decisions and real active cost", async () => {
|
||||
const c = new AdaptiveAdmissionController(
|
||||
baseConfig({ mode: "shadow", initialLimit: 10, maxQueueCount: 1, maxQueueCost: 10 }),
|
||||
{ now: clock.now, setTimer: clock.setTimer, clearTimer: clock.clearTimer }
|
||||
);
|
||||
|
||||
const first = await c.acquire(req({ cost: 8 }));
|
||||
assert.equal(first.status, "admitted");
|
||||
if (first.status !== "admitted") return;
|
||||
assert.equal(first.shadowDecision, "would-admit");
|
||||
assert.equal(c.snapshot().activeCost, 8);
|
||||
|
||||
const second = await c.acquire(req({ cost: 8 }));
|
||||
assert.equal(second.status, "admitted");
|
||||
if (second.status !== "admitted") return;
|
||||
// Would have queued under enforce (active 8 + 8 > 10) but shadow admits immediately.
|
||||
assert.equal(second.shadowDecision, "would-queue");
|
||||
assert.equal(c.snapshot().activeCost, 16);
|
||||
assert.equal(c.snapshot().queuedCount, 0);
|
||||
assert.ok((c.snapshot().wouldQueueCount ?? 0) >= 1);
|
||||
|
||||
const oversized = await c.acquire(req({ cost: 50 }));
|
||||
assert.equal(oversized.status, "admitted");
|
||||
if (oversized.status !== "admitted") return;
|
||||
assert.equal(oversized.shadowDecision, "would-reject");
|
||||
assert.ok((c.snapshot().wouldRejectCount ?? 0) >= 1);
|
||||
|
||||
first.lease.release("success");
|
||||
second.lease.release("success");
|
||||
oversized.lease.release("success");
|
||||
assert.equal(c.snapshot().activeCost, 0);
|
||||
c.shutdown();
|
||||
});
|
||||
|
||||
it("simulates virtual queue saturation and promotes queued work on release", async () => {
|
||||
const c = new AdaptiveAdmissionController(
|
||||
baseConfig({ mode: "shadow", initialLimit: 10, maxQueueCount: 1, maxQueueCost: 8 }),
|
||||
{ now: clock.now, setTimer: clock.setTimer, clearTimer: clock.clearTimer }
|
||||
);
|
||||
const active = await c.acquire(req({ cost: 8, tenantKey: "active" }));
|
||||
const queued = await c.acquire(req({ cost: 8, tenantKey: "queued" }));
|
||||
const saturated = await c.acquire(req({ cost: 8, tenantKey: "saturated" }));
|
||||
assert.equal(active.status, "admitted");
|
||||
assert.equal(queued.status, "admitted");
|
||||
assert.equal(saturated.status, "admitted");
|
||||
if (
|
||||
active.status !== "admitted" ||
|
||||
queued.status !== "admitted" ||
|
||||
saturated.status !== "admitted"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
assert.equal(active.shadowDecision, "would-admit");
|
||||
assert.equal(queued.shadowDecision, "would-queue");
|
||||
assert.equal(saturated.shadowDecision, "would-reject");
|
||||
assert.deepEqual(
|
||||
{
|
||||
activeCost: c.snapshot().virtualActiveCost,
|
||||
activeCount: c.snapshot().virtualActiveCount,
|
||||
queuedCost: c.snapshot().virtualQueuedCost,
|
||||
queuedCount: c.snapshot().virtualQueuedCount,
|
||||
},
|
||||
{ activeCost: 8, activeCount: 1, queuedCost: 8, queuedCount: 1 }
|
||||
);
|
||||
|
||||
active.lease.release();
|
||||
assert.deepEqual(
|
||||
{
|
||||
activeCost: c.snapshot().virtualActiveCost,
|
||||
activeCount: c.snapshot().virtualActiveCount,
|
||||
queuedCost: c.snapshot().virtualQueuedCost,
|
||||
queuedCount: c.snapshot().virtualQueuedCount,
|
||||
},
|
||||
{ activeCost: 8, activeCount: 1, queuedCost: 0, queuedCount: 0 }
|
||||
);
|
||||
queued.lease.release();
|
||||
saturated.lease.release();
|
||||
c.shutdown();
|
||||
});
|
||||
|
||||
it("promotes shadow virtual queue after adaptation raises the limit", async () => {
|
||||
const c = new AdaptiveAdmissionController(
|
||||
baseConfig({
|
||||
mode: "shadow",
|
||||
minLimit: 10,
|
||||
maxLimit: 20,
|
||||
initialLimit: 10,
|
||||
maxQueueCount: 4,
|
||||
maxQueueCost: 40,
|
||||
windowMs: 100,
|
||||
increaseStep: 5,
|
||||
maxIncreasePerWindow: 5,
|
||||
highUtilizationThreshold: 0.5,
|
||||
}),
|
||||
{ now: clock.now, setTimer: clock.setTimer, clearTimer: clock.clearTimer }
|
||||
);
|
||||
|
||||
const active = await c.acquire(req({ cost: 10, tenantKey: "active" }));
|
||||
const queued = await c.acquire(req({ cost: 5, tenantKey: "queued" }));
|
||||
assert.equal(active.status, "admitted");
|
||||
assert.equal(queued.status, "admitted");
|
||||
if (active.status !== "admitted" || queued.status !== "admitted") return;
|
||||
assert.equal(active.shadowDecision, "would-admit");
|
||||
assert.equal(queued.shadowDecision, "would-queue");
|
||||
assert.equal(c.snapshot().virtualActiveCost, 10);
|
||||
assert.equal(c.snapshot().virtualQueuedCost, 5);
|
||||
|
||||
// Raise the adaptive limit once while both leases remain open. Shadow admits a
|
||||
// probe for completion evidence; active integral is capped at the current limit.
|
||||
const probe = await c.acquire(req({ cost: 1, tenantKey: "probe" }));
|
||||
assert.equal(probe.status, "admitted");
|
||||
if (probe.status === "admitted") {
|
||||
clock.advance(80);
|
||||
probe.lease.release("success", { latencyMs: 10 });
|
||||
clock.advance(20);
|
||||
c.tick();
|
||||
}
|
||||
|
||||
assert.equal(c.snapshot().currentLimit, 15);
|
||||
// Queued virtual work must be promoted before newer arrivals are classified.
|
||||
assert.equal(c.snapshot().virtualActiveCost, 15);
|
||||
assert.equal(c.snapshot().virtualQueuedCost, 0);
|
||||
|
||||
const later = await c.acquire(req({ cost: 5, tenantKey: "later" }));
|
||||
assert.equal(later.status, "admitted");
|
||||
if (later.status !== "admitted") return;
|
||||
// With virtual active already 15 at limit 15, a later cost-5 cannot would-admit.
|
||||
assert.notEqual(later.shadowDecision, "would-admit");
|
||||
|
||||
active.lease.release();
|
||||
queued.lease.release();
|
||||
later.lease.release();
|
||||
c.shutdown();
|
||||
});
|
||||
});
|
||||
|
||||
describe("weighted enforce, queue, fairness, and races", () => {
|
||||
let clock: FakeClock;
|
||||
const live: AdaptiveAdmissionController[] = [];
|
||||
beforeEach(() => {
|
||||
clock = new FakeClock();
|
||||
live.length = 0;
|
||||
});
|
||||
afterEach(() => {
|
||||
for (const c of live) c.shutdown();
|
||||
live.length = 0;
|
||||
});
|
||||
|
||||
function controller(overrides: Partial<AdaptiveAdmissionConfig> = {}) {
|
||||
const c = new AdaptiveAdmissionController(baseConfig(overrides), {
|
||||
now: clock.now,
|
||||
setTimer: clock.setTimer,
|
||||
clearTimer: clock.clearTimer,
|
||||
});
|
||||
live.push(c);
|
||||
return c;
|
||||
}
|
||||
|
||||
it("enforces weighted active-cost budget and rejects oversized requests immediately", async () => {
|
||||
const c = controller({ initialLimit: 20 });
|
||||
const a = await mustAdmit(c, req({ cost: 12 }));
|
||||
const b = await c.acquire(req({ cost: 12 }));
|
||||
assert.equal(b.status, "queued");
|
||||
|
||||
const over = await c.acquire(req({ cost: 25 }));
|
||||
assert.equal(over.status, "rejected");
|
||||
if (over.status === "rejected") {
|
||||
assert.equal(over.code, "ADMISSION_OVERSIZED");
|
||||
}
|
||||
|
||||
a.release("success");
|
||||
if (b.status === "queued") {
|
||||
const admitted = await b.promise;
|
||||
assert.equal(admitted.status, "admitted");
|
||||
admitted.lease.release("success");
|
||||
}
|
||||
});
|
||||
|
||||
it("bounds queue by count and total queued cost", async () => {
|
||||
const c = controller({
|
||||
minLimit: 10,
|
||||
initialLimit: 10,
|
||||
maxLimit: 10,
|
||||
maxQueueCount: 2,
|
||||
maxQueueCost: 15,
|
||||
});
|
||||
const held = await mustAdmit(c, req({ cost: 10 }));
|
||||
|
||||
const q1 = await c.acquire(req({ cost: 5, tenantKey: "a" }));
|
||||
const q2 = await c.acquire(req({ cost: 5, tenantKey: "b" }));
|
||||
assert.equal(q1.status, "queued");
|
||||
assert.equal(q2.status, "queued");
|
||||
assert.equal(c.snapshot().queuedCount, 2);
|
||||
assert.equal(c.snapshot().queuedCost, 10);
|
||||
|
||||
const byCount = await c.acquire(req({ cost: 1, tenantKey: "c" }));
|
||||
assert.equal(byCount.status, "rejected");
|
||||
if (byCount.status === "rejected") assert.equal(byCount.code, "ADMISSION_QUEUE_FULL");
|
||||
|
||||
held.release("success");
|
||||
if (q1.status === "queued") (await q1.promise).lease.release("success");
|
||||
if (q2.status === "queued") (await q2.promise).lease.release("success");
|
||||
|
||||
const c2 = controller({
|
||||
minLimit: 5,
|
||||
initialLimit: 5,
|
||||
maxLimit: 5,
|
||||
maxQueueCount: 10,
|
||||
maxQueueCost: 7,
|
||||
});
|
||||
const h = await mustAdmit(c2, req({ cost: 5 }));
|
||||
// cost 3 fits limit but not active budget → queued (queuedCost=3).
|
||||
// Another cost 5 fits the budget but 3+5 > maxQueueCost=7 → QUEUE_FULL.
|
||||
const ok = await c2.acquire(req({ cost: 3 }));
|
||||
assert.equal(ok.status, "queued");
|
||||
const costFull = await c2.acquire(req({ cost: 5 }));
|
||||
assert.equal(costFull.status, "rejected");
|
||||
if (costFull.status === "rejected") assert.equal(costFull.code, "ADMISSION_QUEUE_FULL");
|
||||
h.release("success");
|
||||
if (ok.status === "queued") (await ok.promise).lease.release("success");
|
||||
});
|
||||
|
||||
it("expires deadline and abort without leaking queue slots", async () => {
|
||||
const c = controller({
|
||||
minLimit: 5,
|
||||
initialLimit: 5,
|
||||
maxLimit: 5,
|
||||
maxQueueCount: 4,
|
||||
maxQueueCost: 40,
|
||||
defaultMaxWaitMs: 50,
|
||||
});
|
||||
const held = await mustAdmit(c, req({ cost: 5 }));
|
||||
|
||||
const timed = await c.acquire(req({ cost: 3, maxWaitMs: 30 }));
|
||||
assert.equal(timed.status, "queued");
|
||||
clock.advance(31);
|
||||
if (timed.status === "queued") {
|
||||
await assert.rejects(timed.promise, (err: unknown) => {
|
||||
assert.equal((err as { code?: string }).code, "ADMISSION_DEADLINE");
|
||||
return true;
|
||||
});
|
||||
}
|
||||
assert.equal(c.snapshot().queuedCount, 0);
|
||||
|
||||
const ac = new AbortController();
|
||||
const aborted = await c.acquire(req({ cost: 3, signal: ac.signal }));
|
||||
assert.equal(aborted.status, "queued");
|
||||
ac.abort();
|
||||
if (aborted.status === "queued") {
|
||||
await assert.rejects(aborted.promise, (err: unknown) => {
|
||||
assert.equal((err as { code?: string }).code, "ADMISSION_ABORTED");
|
||||
return true;
|
||||
});
|
||||
}
|
||||
assert.equal(c.snapshot().queuedCount, 0);
|
||||
held.release("success");
|
||||
});
|
||||
|
||||
it("treats the exact deadline as expired and settles abort/release races once", async () => {
|
||||
const c = controller({ minLimit: 5, initialLimit: 5, maxLimit: 5, defaultMaxWaitMs: 30 });
|
||||
const held = await mustAdmit(c, req({ cost: 5 }));
|
||||
const ac = new AbortController();
|
||||
const queued = await c.acquire(req({ cost: 3, maxWaitMs: 30, signal: ac.signal }));
|
||||
assert.equal(queued.status, "queued");
|
||||
|
||||
clock.advance(30);
|
||||
ac.abort();
|
||||
held.release("success");
|
||||
|
||||
if (queued.status === "queued") {
|
||||
await assert.rejects(queued.promise, (err: unknown) => {
|
||||
assert.equal((err as { code?: string }).code, "ADMISSION_DEADLINE");
|
||||
return true;
|
||||
});
|
||||
}
|
||||
assert.equal(c.snapshot().queuedCount, 0);
|
||||
assert.equal(c.snapshot().rejectedCount, 1);
|
||||
});
|
||||
|
||||
it("shutdown rejects queued work and clears every fake-clock timer", async () => {
|
||||
const c = controller({
|
||||
minLimit: 5,
|
||||
initialLimit: 5,
|
||||
maxLimit: 5,
|
||||
maxQueueCount: 4,
|
||||
maxQueueCost: 40,
|
||||
});
|
||||
const held = await mustAdmit(c, req({ cost: 5 }));
|
||||
const q = await c.acquire(req({ cost: 3, maxWaitMs: 5000 }));
|
||||
assert.equal(q.status, "queued");
|
||||
assert.ok(clock.pendingTimerCount >= 2);
|
||||
c.shutdown();
|
||||
if (q.status === "queued") {
|
||||
await assert.rejects(q.promise, (err: unknown) => {
|
||||
assert.equal((err as { code?: string }).code, "ADMISSION_SHUTDOWN");
|
||||
return true;
|
||||
});
|
||||
}
|
||||
assert.equal(c.snapshot().queuedCount, 0);
|
||||
assert.equal(clock.pendingTimerCount, 0);
|
||||
held.release("success");
|
||||
const after = await c.acquire(req({ cost: 1 }));
|
||||
assert.equal(after.status, "rejected");
|
||||
if (after.status === "rejected") assert.equal(after.code, "ADMISSION_SHUTDOWN");
|
||||
});
|
||||
|
||||
it("updateConfig atomically settles queues, dispatches raised capacity, and respects decreases", async () => {
|
||||
const c = controller({ minLimit: 5, initialLimit: 5, maxLimit: 20, windowMs: 100 });
|
||||
const held = await mustAdmit(c, req({ cost: 5 }));
|
||||
const queued = await c.acquire(req({ cost: 5 }));
|
||||
assert.equal(queued.status, "queued");
|
||||
|
||||
c.updateConfig(baseConfig({ minLimit: 10, initialLimit: 10, maxLimit: 20, windowMs: 50 }));
|
||||
assert.equal(clock.pendingTimerCount, 1);
|
||||
if (queued.status === "queued") {
|
||||
const admitted = await queued.promise;
|
||||
assert.equal(c.snapshot().activeCost, 10);
|
||||
|
||||
c.updateConfig(baseConfig({ minLimit: 5, initialLimit: 5, maxLimit: 5, windowMs: 50 }));
|
||||
const afterDecrease = await c.acquire(req({ cost: 1 }));
|
||||
assert.equal(afterDecrease.status, "queued");
|
||||
|
||||
c.updateConfig(baseConfig({ mode: "shadow", minLimit: 5, initialLimit: 5, maxLimit: 5 }));
|
||||
if (afterDecrease.status === "queued") {
|
||||
const settled = await afterDecrease.promise;
|
||||
assert.equal(settled.status, "admitted");
|
||||
settled.lease.release();
|
||||
}
|
||||
admitted.lease.release();
|
||||
}
|
||||
held.release();
|
||||
});
|
||||
|
||||
it("queue shrink rejects deterministic round-robin excess and preserves fitting entries", async () => {
|
||||
const c = controller({
|
||||
minLimit: 5,
|
||||
initialLimit: 5,
|
||||
maxLimit: 5,
|
||||
maxQueueCount: 4,
|
||||
maxQueueCost: 20,
|
||||
});
|
||||
const held = await mustAdmit(c, req({ cost: 5 }));
|
||||
const first = await c.acquire(req({ cost: 2, tenantKey: "a" }));
|
||||
const second = await c.acquire(req({ cost: 2, tenantKey: "b" }));
|
||||
const third = await c.acquire(req({ cost: 2, tenantKey: "a" }));
|
||||
assert.equal(first.status, "queued");
|
||||
assert.equal(second.status, "queued");
|
||||
assert.equal(third.status, "queued");
|
||||
|
||||
c.updateConfig(
|
||||
baseConfig({
|
||||
minLimit: 5,
|
||||
initialLimit: 5,
|
||||
maxLimit: 5,
|
||||
maxQueueCount: 2,
|
||||
maxQueueCost: 4,
|
||||
})
|
||||
);
|
||||
assert.equal(c.snapshot().queuedCount, 2);
|
||||
if (third.status === "queued") {
|
||||
await assert.rejects(third.promise, (err: unknown) => {
|
||||
assert.equal((err as { code?: string }).code, "ADMISSION_QUEUE_FULL");
|
||||
return true;
|
||||
});
|
||||
}
|
||||
held.release();
|
||||
if (first.status === "queued") (await first.promise).lease.release();
|
||||
if (second.status === "queued") (await second.promise).lease.release();
|
||||
});
|
||||
|
||||
it("release is idempotent under race with abort", async () => {
|
||||
const c = controller({ initialLimit: 10 });
|
||||
const lease = await mustAdmit(c, req({ cost: 4 }));
|
||||
lease.release("success");
|
||||
lease.release("timeout");
|
||||
lease.release("success");
|
||||
assert.equal(c.snapshot().activeCost, 0);
|
||||
assert.equal(c.snapshot().activeCount, 0);
|
||||
});
|
||||
|
||||
it("fairly schedules across tenants under skew without exposing tenant ids", async () => {
|
||||
const c = controller({
|
||||
minLimit: 5,
|
||||
initialLimit: 5,
|
||||
maxLimit: 5,
|
||||
maxQueueCount: 10,
|
||||
maxQueueCost: 100,
|
||||
});
|
||||
const held = await mustAdmit(c, req({ cost: 5, tenantKey: "hold" }));
|
||||
|
||||
const order: string[] = [];
|
||||
const queued: Array<Promise<void>> = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const r = await c.acquire(req({ cost: 5, tenantKey: "heavy" }));
|
||||
assert.equal(r.status, "queued");
|
||||
if (r.status === "queued") {
|
||||
queued.push(
|
||||
r.promise.then((admitted) => {
|
||||
order.push("heavy");
|
||||
admitted.lease.release("success");
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
const light = await c.acquire(req({ cost: 5, tenantKey: "light" }));
|
||||
assert.equal(light.status, "queued");
|
||||
if (light.status === "queued") {
|
||||
queued.push(
|
||||
light.promise.then((admitted) => {
|
||||
order.push("light");
|
||||
admitted.lease.release("success");
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Free capacity one slot at a time.
|
||||
held.release("success");
|
||||
await Promise.resolve();
|
||||
// After first release, one request should admit; keep draining by waiting microtasks between releases.
|
||||
// Drain remaining by letting each admitted release free the next.
|
||||
await Promise.all(queued);
|
||||
|
||||
// Light must not be starved behind all four heavy requests.
|
||||
const lightIndex = order.indexOf("light");
|
||||
assert.ok(lightIndex >= 0);
|
||||
assert.ok(lightIndex < 4, `light scheduled too late: ${order.join(",")}`);
|
||||
|
||||
const snap = c.snapshot();
|
||||
const json = JSON.stringify(snap);
|
||||
assert.equal(json.includes("heavy"), false);
|
||||
assert.equal(json.includes("light"), false);
|
||||
assert.equal(json.includes("hold"), false);
|
||||
});
|
||||
|
||||
it("dispatches a fitting tenant when another tenant's queue head cannot fit", async () => {
|
||||
const c = controller({
|
||||
minLimit: 10,
|
||||
initialLimit: 10,
|
||||
maxLimit: 10,
|
||||
maxQueueCount: 10,
|
||||
maxQueueCost: 100,
|
||||
});
|
||||
const heldSix = await mustAdmit(c, req({ cost: 6, tenantKey: "holder" }));
|
||||
const heldFour = await mustAdmit(c, req({ cost: 4, tenantKey: "holder" }));
|
||||
const expensive = await c.acquire(req({ cost: 6, tenantKey: "expensive" }));
|
||||
const fitting = await c.acquire(req({ cost: 4, tenantKey: "fitting" }));
|
||||
assert.equal(expensive.status, "queued");
|
||||
assert.equal(fitting.status, "queued");
|
||||
|
||||
heldFour.release("success");
|
||||
if (fitting.status === "queued") {
|
||||
const admitted = await fitting.promise;
|
||||
assert.equal(admitted.lease.cost, 4);
|
||||
admitted.lease.release("success");
|
||||
}
|
||||
assert.equal(c.snapshot().queuedCount, 1);
|
||||
|
||||
heldSix.release("success");
|
||||
if (expensive.status === "queued") (await expensive.promise).lease.release("success");
|
||||
});
|
||||
|
||||
it("bounds starvation of an older unfittable cost-6 behind a stream of cost-2 work", async () => {
|
||||
const c = controller({
|
||||
minLimit: 10,
|
||||
initialLimit: 10,
|
||||
maxLimit: 10,
|
||||
maxQueueCount: 20,
|
||||
maxQueueCost: 100,
|
||||
defaultMaxWaitMs: 10_000,
|
||||
});
|
||||
// Hold 6 so available=4: cost-2 can pass over cost-6 until reservation engages.
|
||||
const held = await mustAdmit(c, req({ cost: 6, tenantKey: "holder" }));
|
||||
|
||||
const expensive = await c.acquire(req({ cost: 6, tenantKey: "expensive" }));
|
||||
assert.equal(expensive.status, "queued");
|
||||
|
||||
// Two actual smaller dequeues through queued promises (pass-overs that age the head).
|
||||
const passOvers: AdmissionLease[] = [];
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const r = await c.acquire(req({ cost: 2, tenantKey: `small-pass-${i}` }));
|
||||
assert.equal(r.status, "queued", `pass-over ${i} should join the non-empty queue`);
|
||||
if (r.status !== "queued") throw new Error("expected queued");
|
||||
const admitted = await r.promise;
|
||||
assert.equal(admitted.lease.cost, 2);
|
||||
passOvers.push(admitted.lease);
|
||||
admitted.lease.release("success");
|
||||
assert.equal(c.snapshot().activeCost, 6);
|
||||
}
|
||||
assert.equal(passOvers.length, 2);
|
||||
|
||||
// A subsequent fitting cost-2 must remain queued: capacity is reserved for cost-6.
|
||||
// Without reservation accounting this would admit immediately and the assertion fails.
|
||||
const blocked = await c.acquire(req({ cost: 2, tenantKey: "small-blocked" }));
|
||||
assert.equal(blocked.status, "queued");
|
||||
await Promise.resolve();
|
||||
assert.equal(c.snapshot().activeCost, 6, "reserved head must block fitting smaller work");
|
||||
assert.equal(c.snapshot().queuedCount, 2);
|
||||
|
||||
const order: number[] = [];
|
||||
assert.equal(expensive.status, "queued");
|
||||
assert.equal(blocked.status, "queued");
|
||||
const expensiveDone = expensive.promise.then((admitted) => {
|
||||
order.push(admitted.lease.cost);
|
||||
return admitted;
|
||||
});
|
||||
const blockedDone = blocked.promise.then((admitted) => {
|
||||
order.push(admitted.lease.cost);
|
||||
return admitted;
|
||||
});
|
||||
|
||||
// Free enough capacity for cost-6; the older reserved request must admit first.
|
||||
// With activeCost back at 0 both may fit in one dispatch turn, so only order is asserted.
|
||||
held.release("success");
|
||||
const [expAdmitted, blockedAdmitted] = await Promise.all([expensiveDone, blockedDone]);
|
||||
assert.equal(expAdmitted.lease.cost, 6);
|
||||
assert.equal(blockedAdmitted.lease.cost, 2);
|
||||
assert.deepEqual(order, [6, 2]);
|
||||
expAdmitted.lease.release("success");
|
||||
blockedAdmitted.lease.release("success");
|
||||
assert.equal(c.snapshot().queuedCount, 0);
|
||||
});
|
||||
|
||||
async function ageReservedCost6(
|
||||
c: AdaptiveAdmissionController,
|
||||
expensiveSignal?: AbortSignal,
|
||||
expensiveMaxWaitMs?: number
|
||||
) {
|
||||
const held = await mustAdmit(c, req({ cost: 6, tenantKey: "holder" }));
|
||||
const expensive = await c.acquire(
|
||||
req({
|
||||
cost: 6,
|
||||
tenantKey: "expensive",
|
||||
signal: expensiveSignal,
|
||||
maxWaitMs: expensiveMaxWaitMs,
|
||||
})
|
||||
);
|
||||
assert.equal(expensive.status, "queued");
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const r = await c.acquire(req({ cost: 2, tenantKey: `age-pass-${i}` }));
|
||||
assert.equal(r.status, "queued");
|
||||
if (r.status !== "queued") throw new Error("expected queued");
|
||||
(await r.promise).lease.release("success");
|
||||
}
|
||||
const blocked = await c.acquire(req({ cost: 2, tenantKey: "age-blocked" }));
|
||||
assert.equal(blocked.status, "queued");
|
||||
await Promise.resolve();
|
||||
assert.equal(c.snapshot().activeCost, 6);
|
||||
assert.equal(c.snapshot().queuedCount, 2);
|
||||
return { held, expensive, blocked };
|
||||
}
|
||||
|
||||
it("aborting a reserved head immediately admits the next fitting request", async () => {
|
||||
const c = controller({
|
||||
minLimit: 10,
|
||||
initialLimit: 10,
|
||||
maxLimit: 10,
|
||||
maxQueueCount: 20,
|
||||
maxQueueCost: 100,
|
||||
defaultMaxWaitMs: 10_000,
|
||||
});
|
||||
const ac = new AbortController();
|
||||
const { held, expensive, blocked } = await ageReservedCost6(c, ac.signal);
|
||||
assert.equal(expensive.status, "queued");
|
||||
assert.equal(blocked.status, "queued");
|
||||
|
||||
ac.abort();
|
||||
// No tick / new arrival / release / config update — only the abort path.
|
||||
if (expensive.status === "queued") {
|
||||
await assert.rejects(expensive.promise, (err: unknown) => {
|
||||
assert.equal((err as { code?: string }).code, "ADMISSION_ABORTED");
|
||||
return true;
|
||||
});
|
||||
}
|
||||
if (blocked.status !== "queued") throw new Error("expected queued blocked request");
|
||||
const admitted = await blocked.promise;
|
||||
assert.equal(admitted.lease.cost, 2);
|
||||
assert.equal(c.snapshot().activeCost, 8);
|
||||
assert.equal(c.snapshot().queuedCount, 0);
|
||||
admitted.lease.release("success");
|
||||
held.release("success");
|
||||
});
|
||||
|
||||
it("deadline-expiring a reserved head immediately admits the next fitting request", async () => {
|
||||
const c = controller({
|
||||
minLimit: 10,
|
||||
initialLimit: 10,
|
||||
maxLimit: 10,
|
||||
maxQueueCount: 20,
|
||||
maxQueueCost: 100,
|
||||
defaultMaxWaitMs: 10_000,
|
||||
});
|
||||
const { held, expensive, blocked } = await ageReservedCost6(c, undefined, 40);
|
||||
assert.equal(expensive.status, "queued");
|
||||
assert.equal(blocked.status, "queued");
|
||||
|
||||
clock.advance(40);
|
||||
// No tick / new arrival / release / config update — only the deadline timer.
|
||||
if (expensive.status === "queued") {
|
||||
await assert.rejects(expensive.promise, (err: unknown) => {
|
||||
assert.equal((err as { code?: string }).code, "ADMISSION_DEADLINE");
|
||||
return true;
|
||||
});
|
||||
}
|
||||
if (blocked.status !== "queued") throw new Error("expected queued blocked request");
|
||||
const admitted = await blocked.promise;
|
||||
assert.equal(admitted.lease.cost, 2);
|
||||
assert.equal(c.snapshot().activeCost, 8);
|
||||
assert.equal(c.snapshot().queuedCount, 0);
|
||||
admitted.lease.release("success");
|
||||
held.release("success");
|
||||
});
|
||||
});
|
||||
|
||||
describe("adaptive algorithm", () => {
|
||||
let clock: FakeClock;
|
||||
const live: AdaptiveAdmissionController[] = [];
|
||||
beforeEach(() => {
|
||||
clock = new FakeClock();
|
||||
live.length = 0;
|
||||
});
|
||||
afterEach(() => {
|
||||
for (const c of live) c.shutdown();
|
||||
live.length = 0;
|
||||
});
|
||||
|
||||
function controller(overrides: Partial<AdaptiveAdmissionConfig> = {}) {
|
||||
const c = new AdaptiveAdmissionController(baseConfig(overrides), {
|
||||
now: clock.now,
|
||||
setTimer: clock.setTimer,
|
||||
clearTimer: clock.clearTimer,
|
||||
});
|
||||
live.push(c);
|
||||
return c;
|
||||
}
|
||||
|
||||
async function complete(
|
||||
c: AdaptiveAdmissionController,
|
||||
cost: number,
|
||||
latencyMs: number,
|
||||
outcome: "success" | "upstream_error" | "timeout" = "success",
|
||||
pressure: AdmissionPressure = "normal"
|
||||
) {
|
||||
const lease = await mustAdmit(c, req({ cost, pressure }));
|
||||
clock.advance(latencyMs);
|
||||
lease.release(outcome, { latencyMs, pressure });
|
||||
}
|
||||
|
||||
it("keeps currentLimit within validated bounds", async () => {
|
||||
const c = controller({ initialLimit: 20, minLimit: 10, maxLimit: 30, increaseStep: 50 });
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await complete(c, 5, 5, "success", "normal");
|
||||
clock.advance(100);
|
||||
}
|
||||
assert.ok(c.snapshot().currentLimit <= 30);
|
||||
assert.ok(c.snapshot().currentLimit >= 10);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await complete(c, 5, 5, "success", "critical");
|
||||
clock.advance(100);
|
||||
}
|
||||
assert.ok(c.snapshot().currentLimit >= 10);
|
||||
});
|
||||
|
||||
it("decreases rapidly under critical pressure", async () => {
|
||||
const c = controller({ initialLimit: 80, minLimit: 10, maxLimit: 100 });
|
||||
const before = c.snapshot().currentLimit;
|
||||
await complete(c, 10, 10, "success", "critical");
|
||||
clock.advance(100);
|
||||
// Force a window tick with pressure observation.
|
||||
c.observePressure("critical");
|
||||
clock.advance(100);
|
||||
assert.ok(c.snapshot().currentLimit < before);
|
||||
assert.ok(c.snapshot().currentLimit <= Math.ceil(before * 0.5) + 1);
|
||||
});
|
||||
|
||||
it("applies criticalDecreaseFactor once for a single observePressure(critical)", () => {
|
||||
const c = controller({
|
||||
initialLimit: 80,
|
||||
minLimit: 10,
|
||||
maxLimit: 100,
|
||||
criticalDecreaseFactor: 0.5,
|
||||
decreaseFactor: 0.8,
|
||||
windowMs: 100,
|
||||
});
|
||||
assert.equal(c.snapshot().currentLimit, 80);
|
||||
|
||||
c.observePressure("critical");
|
||||
// Immediate fast decrease: 80 * 0.5 = 40.
|
||||
assert.equal(c.snapshot().currentLimit, 40);
|
||||
|
||||
// Closing the same window must not multiply again (would become 20).
|
||||
clock.advance(100);
|
||||
c.tick();
|
||||
assert.equal(c.snapshot().currentLimit, 40);
|
||||
|
||||
// A fresh critical observation in a later window still decreases once.
|
||||
c.observePressure("critical");
|
||||
assert.equal(c.snapshot().currentLimit, 20);
|
||||
clock.advance(100);
|
||||
c.tick();
|
||||
assert.equal(c.snapshot().currentLimit, 20);
|
||||
});
|
||||
|
||||
it("decreases on high pressure or sustained latency gradient", async () => {
|
||||
const c = controller({
|
||||
initialLimit: 50,
|
||||
shortLatencyAlpha: 0.8,
|
||||
longLatencyAlpha: 0.1,
|
||||
latencyGradientThreshold: 0.2,
|
||||
});
|
||||
// Seed long baseline with low latency.
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await complete(c, 8, 10, "success", "normal");
|
||||
clock.advance(100);
|
||||
}
|
||||
const mid = c.snapshot().currentLimit;
|
||||
// Spike short latency relative to long.
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await complete(c, 8, 200, "success", "normal");
|
||||
clock.advance(100);
|
||||
}
|
||||
assert.ok(c.snapshot().currentLimit <= mid);
|
||||
|
||||
const beforeHigh = c.snapshot().currentLimit;
|
||||
c.observePressure("high");
|
||||
await complete(c, 8, 20, "success", "high");
|
||||
clock.advance(100);
|
||||
assert.ok(c.snapshot().currentLimit <= beforeHigh);
|
||||
});
|
||||
|
||||
it("increases slowly when healthy and highly utilized, and does not inflate when idle", async () => {
|
||||
const c = controller({
|
||||
minLimit: 20,
|
||||
maxLimit: 40,
|
||||
initialLimit: 20,
|
||||
increaseStep: 2,
|
||||
maxIncreasePerWindow: 2,
|
||||
highUtilizationThreshold: 0.5,
|
||||
windowMs: 100,
|
||||
});
|
||||
|
||||
// Idle windows should not inflate.
|
||||
clock.advance(500);
|
||||
c.tick();
|
||||
clock.advance(500);
|
||||
c.tick();
|
||||
assert.equal(c.snapshot().currentLimit, 20);
|
||||
|
||||
// Healthy high utilization: hold nearly full budget across most of each window.
|
||||
for (let w = 0; w < 5; w++) {
|
||||
const lease = await mustAdmit(c, req({ cost: 16, pressure: "normal" }));
|
||||
clock.advance(80);
|
||||
lease.release("success", { latencyMs: 10, pressure: "normal" });
|
||||
clock.advance(20);
|
||||
c.tick();
|
||||
}
|
||||
assert.ok(c.snapshot().currentLimit > 20);
|
||||
assert.ok(c.snapshot().currentLimit <= 20 + 2 * 5);
|
||||
});
|
||||
|
||||
it("does not collapse capacity on a single upstream business error", async () => {
|
||||
const c = controller({ initialLimit: 40, decreaseFactor: 0.5, criticalDecreaseFactor: 0.5 });
|
||||
await complete(c, 10, 15, "upstream_error", "normal");
|
||||
clock.advance(100);
|
||||
c.tick();
|
||||
// One business error may freeze growth but must not apply critical collapse.
|
||||
assert.ok(c.snapshot().currentLimit >= 30);
|
||||
});
|
||||
|
||||
it("integrates active utilization exactly once over a full window", async () => {
|
||||
const c = controller({
|
||||
minLimit: 20,
|
||||
initialLimit: 20,
|
||||
maxLimit: 20,
|
||||
highUtilizationThreshold: 0.9,
|
||||
windowMs: 100,
|
||||
});
|
||||
const lease = await mustAdmit(c, req({ cost: 8 }));
|
||||
clock.advance(100);
|
||||
assert.equal(c.snapshot().utilization, 0.4);
|
||||
lease.release("success");
|
||||
});
|
||||
|
||||
it("consumes latency and pressure evidence only in the window where it was observed", async () => {
|
||||
const c = controller({
|
||||
initialLimit: 80,
|
||||
minLimit: 10,
|
||||
maxLimit: 100,
|
||||
decreaseFactor: 0.5,
|
||||
criticalDecreaseFactor: 0.25,
|
||||
windowMs: 100,
|
||||
});
|
||||
|
||||
await complete(c, 8, 200, "success", "high");
|
||||
clock.advance(100);
|
||||
const afterObservedWindow = c.snapshot().currentLimit;
|
||||
assert.ok(afterObservedWindow < 80);
|
||||
|
||||
clock.advance(500);
|
||||
assert.equal(c.snapshot().currentLimit, afterObservedWindow);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createAdmissionRejectError", () => {
|
||||
it("builds typed rejection errors", () => {
|
||||
const err = createAdmissionRejectError("ADMISSION_QUEUE_FULL", "queue full");
|
||||
assert.equal(err.code, "ADMISSION_QUEUE_FULL");
|
||||
assert.equal(err.name, "AdmissionRejectError");
|
||||
assert.match(err.message, /queue full/);
|
||||
});
|
||||
});
|
||||
143
tests/unit/adaptive-admission-cost.test.ts
Normal file
143
tests/unit/adaptive-admission-cost.test.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
estimateAdmissionCost,
|
||||
DEFAULT_ADMISSION_COST_CONFIG,
|
||||
MAX_ADMISSION_COST_OR_LIMIT,
|
||||
normalizeRequestCost,
|
||||
resolveCostConfig,
|
||||
type AdmissionCostConfig,
|
||||
type AdmissionCostFeatures,
|
||||
} from "../../open-sse/services/admission/index.ts";
|
||||
|
||||
function features(overrides: Partial<AdmissionCostFeatures> = {}): AdmissionCostFeatures {
|
||||
return {
|
||||
bodyBytes: 0,
|
||||
estimatedInputTokens: 0,
|
||||
messageCount: 0,
|
||||
toolCount: 0,
|
||||
requestedFanout: 1,
|
||||
streaming: true,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("estimateAdmissionCost", () => {
|
||||
it("returns a positive integer at least the base cost", () => {
|
||||
const cost = estimateAdmissionCost(features());
|
||||
assert.equal(Number.isSafeInteger(cost), true);
|
||||
assert.ok(cost >= DEFAULT_ADMISSION_COST_CONFIG.baseCost);
|
||||
assert.ok(cost > 0);
|
||||
});
|
||||
|
||||
it("is monotonic in body bytes, tokens, messages, tools, and fanout", () => {
|
||||
const base = estimateAdmissionCost(features());
|
||||
assert.ok(estimateAdmissionCost(features({ bodyBytes: 50_000 })) >= base);
|
||||
assert.ok(estimateAdmissionCost(features({ estimatedInputTokens: 8_000 })) >= base);
|
||||
assert.ok(estimateAdmissionCost(features({ messageCount: 40 })) >= base);
|
||||
assert.ok(estimateAdmissionCost(features({ toolCount: 20 })) >= base);
|
||||
assert.ok(estimateAdmissionCost(features({ requestedFanout: 8 })) >= base);
|
||||
});
|
||||
|
||||
it("rejects fractional, infinite, and unsafe cost configuration", () => {
|
||||
for (const invalid of [0.5, Number.POSITIVE_INFINITY, Number.MAX_VALUE]) {
|
||||
assert.throws(
|
||||
() => resolveCostConfig({ bodyBytesPerUnit: invalid }),
|
||||
/positive safe integer/
|
||||
);
|
||||
assert.throws(() => resolveCostConfig({ maxRequestCost: invalid }), /positive safe integer/);
|
||||
assert.throws(
|
||||
() => resolveCostConfig({ streamingClassCost: invalid }),
|
||||
/positive safe integer/
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("normalizes caller costs only when they are positive safe integers", () => {
|
||||
assert.equal(normalizeRequestCost(7, 10), 7);
|
||||
for (const invalid of [0, -1, 0.5, Number.POSITIVE_INFINITY, Number.MAX_VALUE]) {
|
||||
assert.throws(() => normalizeRequestCost(invalid, 10), /positive safe integer/);
|
||||
}
|
||||
assert.throws(() => normalizeRequestCost(1, Number.MAX_VALUE), /positive safe integer/);
|
||||
assert.throws(
|
||||
() => normalizeRequestCost(1, MAX_ADMISSION_COST_OR_LIMIT + 1),
|
||||
/maxRequestCost|must be <=/
|
||||
);
|
||||
assert.equal(
|
||||
normalizeRequestCost(MAX_ADMISSION_COST_OR_LIMIT, MAX_ADMISSION_COST_OR_LIMIT),
|
||||
MAX_ADMISSION_COST_OR_LIMIT
|
||||
);
|
||||
});
|
||||
|
||||
it("clamps multiple overflowing feature contributions without unsafe arithmetic", () => {
|
||||
const config: AdmissionCostConfig = {
|
||||
...DEFAULT_ADMISSION_COST_CONFIG,
|
||||
maxRequestCost: 25,
|
||||
};
|
||||
const cost = estimateAdmissionCost(
|
||||
features({
|
||||
bodyBytes: Number.MAX_SAFE_INTEGER,
|
||||
estimatedInputTokens: Number.MAX_SAFE_INTEGER,
|
||||
messageCount: Number.MAX_SAFE_INTEGER,
|
||||
toolCount: Number.MAX_SAFE_INTEGER,
|
||||
requestedFanout: Number.MAX_SAFE_INTEGER,
|
||||
}),
|
||||
config
|
||||
);
|
||||
assert.equal(cost, 25);
|
||||
});
|
||||
|
||||
it("normalizes invalid, negative, and NaN inputs safely", () => {
|
||||
const cost = estimateAdmissionCost({
|
||||
bodyBytes: Number.NaN,
|
||||
estimatedInputTokens: -12,
|
||||
messageCount: Number.POSITIVE_INFINITY,
|
||||
toolCount: undefined,
|
||||
requestedFanout: 0,
|
||||
streaming: undefined,
|
||||
} as AdmissionCostFeatures);
|
||||
assert.equal(Number.isSafeInteger(cost), true);
|
||||
assert.ok(cost >= 1);
|
||||
assert.ok(cost <= DEFAULT_ADMISSION_COST_CONFIG.maxRequestCost);
|
||||
});
|
||||
|
||||
it("uses transparent configurable quanta without a fixed-MB claim", () => {
|
||||
const config: AdmissionCostConfig = {
|
||||
baseCost: 1,
|
||||
bodyBytesPerUnit: 1000,
|
||||
tokensPerUnit: 100,
|
||||
messagesPerUnit: 10,
|
||||
toolsPerUnit: 5,
|
||||
fanoutPerUnit: 1,
|
||||
streamingClassCost: 1,
|
||||
nonStreamingClassCost: 3,
|
||||
maxRequestCost: 1000,
|
||||
};
|
||||
// 2500 bytes → 3 units (ceil), 250 tokens → 3 units, 1 message → 1, 0 tools, fanout 1 → 1, streaming class 1
|
||||
const cost = estimateAdmissionCost(
|
||||
features({
|
||||
bodyBytes: 2500,
|
||||
estimatedInputTokens: 250,
|
||||
messageCount: 1,
|
||||
toolCount: 0,
|
||||
requestedFanout: 1,
|
||||
streaming: true,
|
||||
}),
|
||||
config
|
||||
);
|
||||
assert.equal(cost, 1 + 3 + 3 + 1 + 0 + 1 + 1);
|
||||
|
||||
const nonStream = estimateAdmissionCost(
|
||||
features({
|
||||
bodyBytes: 0,
|
||||
estimatedInputTokens: 0,
|
||||
messageCount: 0,
|
||||
toolCount: 0,
|
||||
requestedFanout: 1,
|
||||
streaming: false,
|
||||
}),
|
||||
config
|
||||
);
|
||||
assert.equal(nonStream, 1 + 0 + 0 + 0 + 0 + 1 + 3);
|
||||
});
|
||||
});
|
||||
405
tests/unit/adaptive-admission-domain.test.ts
Normal file
405
tests/unit/adaptive-admission-domain.test.ts
Normal file
@@ -0,0 +1,405 @@
|
||||
import { describe, it, beforeEach, afterEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
AdaptiveAdmissionController,
|
||||
MAX_ADMISSION_COST_OR_LIMIT,
|
||||
MAX_ADMISSION_WINDOW_MS,
|
||||
type AdaptiveAdmissionConfig,
|
||||
type AdmissionLease,
|
||||
type AdmissionRequest,
|
||||
} from "../../open-sse/services/admission/index.ts";
|
||||
|
||||
class FakeClock {
|
||||
nowMs = 0;
|
||||
private nextId = 1;
|
||||
private timers = new Map<number, { due: number; fn: () => void }>();
|
||||
|
||||
now = () => this.nowMs;
|
||||
|
||||
setTimer = (fn: () => void, delayMs: number): number => {
|
||||
const id = this.nextId++;
|
||||
this.timers.set(id, { due: this.nowMs + Math.max(0, delayMs), fn });
|
||||
return id;
|
||||
};
|
||||
|
||||
clearTimer = (id: number): void => {
|
||||
this.timers.delete(id);
|
||||
};
|
||||
|
||||
get pendingTimerCount(): number {
|
||||
return this.timers.size;
|
||||
}
|
||||
|
||||
advance(ms: number): void {
|
||||
const target = this.nowMs + ms;
|
||||
while (true) {
|
||||
let nextId: number | undefined;
|
||||
let nextDue = Number.POSITIVE_INFINITY;
|
||||
for (const [id, t] of this.timers) {
|
||||
if (t.due <= target && t.due < nextDue) {
|
||||
nextDue = t.due;
|
||||
nextId = id;
|
||||
}
|
||||
}
|
||||
if (nextId === undefined) {
|
||||
this.nowMs = target;
|
||||
return;
|
||||
}
|
||||
const timer = this.timers.get(nextId)!;
|
||||
this.timers.delete(nextId);
|
||||
this.nowMs = timer.due;
|
||||
timer.fn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function baseConfig(overrides: Partial<AdaptiveAdmissionConfig> = {}): AdaptiveAdmissionConfig {
|
||||
return {
|
||||
mode: "enforce",
|
||||
minLimit: 10,
|
||||
maxLimit: 100,
|
||||
initialLimit: 20,
|
||||
maxQueueCount: 4,
|
||||
maxQueueCost: 40,
|
||||
defaultMaxWaitMs: 1000,
|
||||
windowMs: 100,
|
||||
shortLatencyAlpha: 0.5,
|
||||
longLatencyAlpha: 0.1,
|
||||
increaseStep: 2,
|
||||
decreaseFactor: 0.8,
|
||||
criticalDecreaseFactor: 0.5,
|
||||
highUtilizationThreshold: 0.7,
|
||||
lowUtilizationThreshold: 0.3,
|
||||
latencyGradientThreshold: 0.25,
|
||||
maxIncreasePerWindow: 4,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function req(partial: Partial<AdmissionRequest> & { cost: number }): AdmissionRequest {
|
||||
return {
|
||||
tenantKey: "t-default",
|
||||
...partial,
|
||||
};
|
||||
}
|
||||
|
||||
async function mustAdmit(
|
||||
controller: AdaptiveAdmissionController,
|
||||
request: AdmissionRequest
|
||||
): Promise<AdmissionLease> {
|
||||
const result = await controller.acquire(request);
|
||||
assert.equal(result.status, "admitted");
|
||||
if (result.status !== "admitted") throw new Error("expected admitted");
|
||||
return result.lease;
|
||||
}
|
||||
|
||||
describe("admission operational domain", () => {
|
||||
let clock: FakeClock;
|
||||
beforeEach(() => {
|
||||
clock = new FakeClock();
|
||||
});
|
||||
|
||||
function make(overrides: Partial<AdaptiveAdmissionConfig> = {}) {
|
||||
return new AdaptiveAdmissionController(baseConfig(overrides), {
|
||||
now: clock.now,
|
||||
setTimer: clock.setTimer,
|
||||
clearTimer: clock.clearTimer,
|
||||
});
|
||||
}
|
||||
|
||||
it("exports an operational domain that rejects max+1 and accepts exact max", () => {
|
||||
assert.ok(Number.isSafeInteger(MAX_ADMISSION_COST_OR_LIMIT));
|
||||
assert.ok(Number.isSafeInteger(MAX_ADMISSION_WINDOW_MS));
|
||||
assert.ok(
|
||||
Number.isSafeInteger(MAX_ADMISSION_COST_OR_LIMIT * MAX_ADMISSION_WINDOW_MS),
|
||||
"limit×window must remain a safe integer"
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
make({
|
||||
minLimit: MAX_ADMISSION_COST_OR_LIMIT + 1,
|
||||
maxLimit: MAX_ADMISSION_COST_OR_LIMIT + 1,
|
||||
initialLimit: MAX_ADMISSION_COST_OR_LIMIT + 1,
|
||||
}),
|
||||
/minLimit|must be <=/
|
||||
);
|
||||
assert.throws(
|
||||
() => make({ maxQueueCost: MAX_ADMISSION_COST_OR_LIMIT + 1 }),
|
||||
/maxQueueCost|must be <=/
|
||||
);
|
||||
assert.throws(() => make({ windowMs: MAX_ADMISSION_WINDOW_MS + 1 }), /windowMs|must be <=/);
|
||||
assert.throws(
|
||||
() => make({ cost: { maxRequestCost: MAX_ADMISSION_COST_OR_LIMIT + 1 } }),
|
||||
/maxRequestCost|must be <=/
|
||||
);
|
||||
assert.throws(() => make({ maxLimit: Number.MAX_SAFE_INTEGER }), /maxLimit|must be <=/);
|
||||
|
||||
const max = MAX_ADMISSION_COST_OR_LIMIT;
|
||||
const c = make({
|
||||
minLimit: max,
|
||||
maxLimit: max,
|
||||
initialLimit: max,
|
||||
maxQueueCount: 2,
|
||||
maxQueueCost: max,
|
||||
windowMs: 1000,
|
||||
cost: { maxRequestCost: max },
|
||||
});
|
||||
assert.equal(c.snapshot().currentLimit, max);
|
||||
c.shutdown();
|
||||
});
|
||||
|
||||
it("keeps full utilization and multi-lease accounting exact at the domain max", async () => {
|
||||
const max = MAX_ADMISSION_COST_OR_LIMIT;
|
||||
const c = make({
|
||||
mode: "enforce",
|
||||
minLimit: max,
|
||||
maxLimit: max,
|
||||
initialLimit: max,
|
||||
maxQueueCount: 4,
|
||||
maxQueueCost: max,
|
||||
windowMs: 1000,
|
||||
cost: { maxRequestCost: max },
|
||||
});
|
||||
|
||||
const full = await mustAdmit(c, req({ cost: max }));
|
||||
assert.equal(c.snapshot().activeCost, max);
|
||||
assert.equal(Number.isSafeInteger(c.snapshot().activeCost), true);
|
||||
clock.advance(1000);
|
||||
assert.equal(c.snapshot().utilization, 1);
|
||||
full.release("success");
|
||||
assert.equal(c.snapshot().activeCost, 0);
|
||||
assert.equal(c.snapshot().activeCount, 0);
|
||||
|
||||
const left = Math.floor(max / 2);
|
||||
const right = max - left;
|
||||
const a = await mustAdmit(c, req({ cost: left }));
|
||||
const b = await mustAdmit(c, req({ cost: right }));
|
||||
assert.equal(c.snapshot().activeCost, max);
|
||||
assert.equal(c.snapshot().activeCount, 2);
|
||||
clock.advance(1000);
|
||||
assert.equal(c.snapshot().utilization, 1);
|
||||
a.release("success");
|
||||
assert.equal(c.snapshot().activeCost, right);
|
||||
b.release("success");
|
||||
assert.equal(c.snapshot().activeCost, 0);
|
||||
assert.equal(c.snapshot().activeCount, 0);
|
||||
c.shutdown();
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateConfig re-evaluation", () => {
|
||||
let clock: FakeClock;
|
||||
const live: AdaptiveAdmissionController[] = [];
|
||||
beforeEach(() => {
|
||||
clock = new FakeClock();
|
||||
live.length = 0;
|
||||
});
|
||||
afterEach(() => {
|
||||
for (const c of live) c.shutdown();
|
||||
live.length = 0;
|
||||
});
|
||||
|
||||
function controller(overrides: Partial<AdaptiveAdmissionConfig> = {}) {
|
||||
const c = new AdaptiveAdmissionController(baseConfig(overrides), {
|
||||
now: clock.now,
|
||||
setTimer: clock.setTimer,
|
||||
clearTimer: clock.clearTimer,
|
||||
});
|
||||
live.push(c);
|
||||
return c;
|
||||
}
|
||||
|
||||
it("updateConfig rejects queued work above the new enforce limit immediately", async () => {
|
||||
const c = controller({
|
||||
minLimit: 5,
|
||||
initialLimit: 10,
|
||||
maxLimit: 10,
|
||||
maxQueueCount: 4,
|
||||
maxQueueCost: 40,
|
||||
defaultMaxWaitMs: 5_000,
|
||||
});
|
||||
const held = await mustAdmit(c, req({ cost: 10 }));
|
||||
const queued = await c.acquire(req({ cost: 8 }));
|
||||
assert.equal(queued.status, "queued");
|
||||
|
||||
c.updateConfig(
|
||||
baseConfig({
|
||||
minLimit: 5,
|
||||
initialLimit: 5,
|
||||
maxLimit: 5,
|
||||
maxQueueCount: 4,
|
||||
maxQueueCost: 40,
|
||||
defaultMaxWaitMs: 5_000,
|
||||
})
|
||||
);
|
||||
|
||||
if (queued.status === "queued") {
|
||||
await assert.rejects(queued.promise, (err: unknown) => {
|
||||
assert.equal((err as { code?: string }).code, "ADMISSION_OVERSIZED");
|
||||
return true;
|
||||
});
|
||||
}
|
||||
assert.equal(c.snapshot().queuedCount, 0);
|
||||
held.release();
|
||||
});
|
||||
|
||||
it("updateConfig enforce→shadow classifies individually oversized active work as virtual rejected", async () => {
|
||||
const c = controller({
|
||||
mode: "enforce",
|
||||
minLimit: 5,
|
||||
initialLimit: 20,
|
||||
maxLimit: 20,
|
||||
maxQueueCount: 4,
|
||||
maxQueueCost: 40,
|
||||
});
|
||||
const oversized = await mustAdmit(c, req({ cost: 15 }));
|
||||
const fitting = await mustAdmit(c, req({ cost: 5 }));
|
||||
|
||||
c.updateConfig(
|
||||
baseConfig({
|
||||
mode: "shadow",
|
||||
minLimit: 5,
|
||||
initialLimit: 10,
|
||||
maxLimit: 10,
|
||||
maxQueueCount: 4,
|
||||
maxQueueCost: 40,
|
||||
})
|
||||
);
|
||||
|
||||
const snap = c.snapshot();
|
||||
// currentLimit clamps to 10; cost 15 is individually oversized → virtual rejected, not queued.
|
||||
assert.equal(snap.currentLimit, 10);
|
||||
assert.equal(snap.virtualActiveCost, 5);
|
||||
assert.equal(snap.virtualActiveCount, 1);
|
||||
assert.equal(snap.virtualQueuedCost, 0);
|
||||
assert.equal(snap.virtualQueuedCount, 0);
|
||||
// Real active leases remain until release.
|
||||
assert.equal(snap.activeCost, 20);
|
||||
assert.equal(snap.activeCount, 2);
|
||||
|
||||
oversized.release();
|
||||
fitting.release();
|
||||
});
|
||||
|
||||
it("updateConfig rebuilds shadow virtual dispositions under new limits and queue bounds", async () => {
|
||||
const c = controller({
|
||||
mode: "shadow",
|
||||
minLimit: 5,
|
||||
initialLimit: 20,
|
||||
maxLimit: 20,
|
||||
maxQueueCount: 2,
|
||||
maxQueueCost: 12,
|
||||
});
|
||||
const first = await c.acquire(req({ cost: 8 }));
|
||||
const second = await c.acquire(req({ cost: 8 }));
|
||||
const third = await c.acquire(req({ cost: 8 }));
|
||||
assert.equal(first.status, "admitted");
|
||||
assert.equal(second.status, "admitted");
|
||||
assert.equal(third.status, "admitted");
|
||||
|
||||
c.updateConfig(
|
||||
baseConfig({
|
||||
mode: "shadow",
|
||||
minLimit: 5,
|
||||
initialLimit: 10,
|
||||
maxLimit: 10,
|
||||
maxQueueCount: 1,
|
||||
maxQueueCost: 8,
|
||||
})
|
||||
);
|
||||
|
||||
const snap = c.snapshot();
|
||||
// One active (8), one queued (8), one rejected (queue full under new bounds).
|
||||
assert.equal(snap.virtualActiveCost, 8);
|
||||
assert.equal(snap.virtualActiveCount, 1);
|
||||
assert.equal(snap.virtualQueuedCost, 8);
|
||||
assert.equal(snap.virtualQueuedCount, 1);
|
||||
assert.equal(snap.activeCount, 3);
|
||||
|
||||
if (first.status === "admitted") first.lease.release();
|
||||
if (second.status === "admitted") second.lease.release();
|
||||
if (third.status === "admitted") third.lease.release();
|
||||
});
|
||||
});
|
||||
|
||||
describe("deterministic overload harness", () => {
|
||||
async function runEqualServiceWindows(offeredPerWindow: number) {
|
||||
const clock = new FakeClock();
|
||||
const c = new AdaptiveAdmissionController(
|
||||
baseConfig({
|
||||
mode: "enforce",
|
||||
initialLimit: 20,
|
||||
minLimit: 20,
|
||||
maxLimit: 20,
|
||||
maxQueueCount: 1,
|
||||
maxQueueCost: 1,
|
||||
defaultMaxWaitMs: 20,
|
||||
}),
|
||||
{ now: clock.now, setTimer: clock.setTimer, clearTimer: clock.clearTimer }
|
||||
);
|
||||
let completed = 0;
|
||||
let fastRejected = 0;
|
||||
let active: AdmissionLease[] = [];
|
||||
|
||||
for (let window = 0; window < 20; window++) {
|
||||
for (const lease of active) {
|
||||
lease.release("success", { latencyMs: 10 });
|
||||
completed += 1;
|
||||
}
|
||||
active = [];
|
||||
for (let i = 0; i < offeredPerWindow; i++) {
|
||||
const result = await c.acquire(req({ cost: 5, tenantKey: `tenant-${i % 3}` }));
|
||||
if (result.status === "admitted") active.push(result.lease);
|
||||
else if (result.status === "rejected") fastRejected += 1;
|
||||
else assert.fail("cost-5 excess must reject immediately when queue cost cap is 1");
|
||||
}
|
||||
clock.advance(10);
|
||||
const snapshot = c.snapshot();
|
||||
assert.ok(snapshot.activeCost <= 20);
|
||||
assert.ok(snapshot.activeCount <= 4);
|
||||
assert.equal(snapshot.queuedCost, 0);
|
||||
assert.equal(snapshot.queuedCount, 0);
|
||||
}
|
||||
for (const lease of active) {
|
||||
lease.release("success", { latencyMs: 10 });
|
||||
completed += 1;
|
||||
}
|
||||
c.shutdown();
|
||||
assert.equal(clock.pendingTimerCount, 0);
|
||||
return { completed, fastRejected };
|
||||
}
|
||||
|
||||
it("raises goodput to capacity then plateaus at 2× and 5× offered load", async () => {
|
||||
// Capacity is 4 admits/window (limit 20, cost 5). Offered loads: 0.5×, 1×, 2×, 5×.
|
||||
const low = await runEqualServiceWindows(2);
|
||||
const atCapacity = await runEqualServiceWindows(4);
|
||||
const doubleOver = await runEqualServiceWindows(8);
|
||||
const fiveOver = await runEqualServiceWindows(20);
|
||||
|
||||
assert.equal(low.completed, 40);
|
||||
assert.equal(atCapacity.completed, 80);
|
||||
assert.ok(atCapacity.completed >= low.completed * 1.9, "goodput must rise toward capacity");
|
||||
assert.equal(
|
||||
doubleOver.completed,
|
||||
atCapacity.completed,
|
||||
"2× offered load must plateau at capacity"
|
||||
);
|
||||
assert.equal(
|
||||
fiveOver.completed,
|
||||
atCapacity.completed,
|
||||
"5× offered load must plateau at capacity"
|
||||
);
|
||||
assert.equal(low.fastRejected, 0);
|
||||
assert.equal(atCapacity.fastRejected, 0);
|
||||
assert.equal(
|
||||
doubleOver.fastRejected,
|
||||
4 * 20,
|
||||
"2× excess rejects immediately with bounded queue"
|
||||
);
|
||||
assert.equal(
|
||||
fiveOver.fastRejected,
|
||||
16 * 20,
|
||||
"5× excess rejects immediately with bounded queue"
|
||||
);
|
||||
});
|
||||
});
|
||||
255
tests/unit/adaptive-admission-features.test.ts
Normal file
255
tests/unit/adaptive-admission-features.test.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { estimateAdmissionCost } from "../../open-sse/services/admission/cost.ts";
|
||||
import {
|
||||
ADMISSION_TOOL_SCAN_BUDGET,
|
||||
extractAdmissionCostFeatures,
|
||||
} from "../../open-sse/services/admission/requestFeatures.ts";
|
||||
|
||||
describe("bounded request feature extraction", () => {
|
||||
it("does not call JSON.stringify or toJSON", () => {
|
||||
let stringifyCalls = 0;
|
||||
const original = JSON.stringify;
|
||||
JSON.stringify = ((...args: Parameters<typeof JSON.stringify>) => {
|
||||
stringifyCalls += 1;
|
||||
return original.apply(JSON, args as [unknown]);
|
||||
}) as typeof JSON.stringify;
|
||||
try {
|
||||
const body = {
|
||||
toJSON() {
|
||||
throw new Error("toJSON must not be invoked");
|
||||
},
|
||||
messages: [{ role: "user", content: "hello world" }],
|
||||
tools: [{ type: "function", function: { name: "x" } }],
|
||||
n: 3,
|
||||
stream: true,
|
||||
};
|
||||
const features = extractAdmissionCostFeatures(body);
|
||||
assert.ok((features.bodyBytes ?? 0) > 0);
|
||||
assert.ok((features.messageCount ?? 0) >= 1);
|
||||
assert.ok((features.toolCount ?? 0) >= 1);
|
||||
assert.equal(features.requestedFanout, 3);
|
||||
assert.equal(features.streaming, true);
|
||||
assert.ok((features.estimatedInputTokens ?? 0) > 0);
|
||||
assert.equal(stringifyCalls, 0);
|
||||
} finally {
|
||||
JSON.stringify = original;
|
||||
}
|
||||
});
|
||||
|
||||
it("extracts production-realistic Chat, Responses, Gemini, and Antigravity shapes", () => {
|
||||
// OpenAI Chat Completions — stream omitted defaults false (higher non-stream class).
|
||||
const chat = extractAdmissionCostFeatures({
|
||||
model: "gpt-4o",
|
||||
messages: [
|
||||
{ role: "system", content: "You are helpful." },
|
||||
{ role: "user", content: "Summarize the logs" },
|
||||
],
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
function: { name: "lookup", parameters: { type: "object" } },
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: { name: "write", parameters: { type: "object" } },
|
||||
},
|
||||
],
|
||||
n: 2,
|
||||
});
|
||||
assert.equal(chat.messageCount, 2);
|
||||
assert.equal(chat.toolCount, 2);
|
||||
assert.equal(chat.requestedFanout, 2);
|
||||
assert.equal(chat.streaming, false);
|
||||
|
||||
// OpenAI Responses API — string input counts as one item; array counts length.
|
||||
const responsesString = extractAdmissionCostFeatures({
|
||||
model: "gpt-4.1",
|
||||
input: "What is the capital of France?",
|
||||
tools: [{ type: "web_search_preview" }],
|
||||
stream: true,
|
||||
});
|
||||
assert.equal(responsesString.messageCount, 1);
|
||||
assert.equal(responsesString.toolCount, 1);
|
||||
assert.equal(responsesString.streaming, true);
|
||||
|
||||
const responsesArray = extractAdmissionCostFeatures({
|
||||
model: "gpt-4.1",
|
||||
input: [
|
||||
{ role: "user", content: [{ type: "input_text", text: "q1" }] },
|
||||
{ role: "user", content: [{ type: "input_text", text: "q2" }] },
|
||||
],
|
||||
stream: false,
|
||||
n: 3,
|
||||
});
|
||||
assert.equal(responsesArray.messageCount, 2);
|
||||
assert.equal(responsesArray.requestedFanout, 3);
|
||||
assert.equal(responsesArray.streaming, false);
|
||||
|
||||
// Empty string input is not a content item.
|
||||
const emptyInput = extractAdmissionCostFeatures({ input: "" });
|
||||
assert.equal(emptyInput.messageCount, 0);
|
||||
|
||||
// Gemini generateContent — nested generationConfig.candidateCount + functionDeclarations.
|
||||
const gemini = extractAdmissionCostFeatures({
|
||||
contents: [
|
||||
{ role: "user", parts: [{ text: "hello" }] },
|
||||
{ role: "model", parts: [{ text: "world" }] },
|
||||
],
|
||||
tools: [
|
||||
{
|
||||
functionDeclarations: [
|
||||
{ name: "get_weather", parameters: { type: "OBJECT" } },
|
||||
{ name: "get_time", parameters: { type: "OBJECT" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
generationConfig: { candidateCount: 4, temperature: 0.2 },
|
||||
});
|
||||
assert.equal(gemini.messageCount, 2);
|
||||
assert.equal(gemini.toolCount, 2);
|
||||
assert.equal(gemini.requestedFanout, 4);
|
||||
assert.equal(gemini.streaming, false);
|
||||
|
||||
// Antigravity-style wrapper under `request`.
|
||||
const antigravity = extractAdmissionCostFeatures({
|
||||
request: {
|
||||
contents: [{ role: "user", parts: [{ text: "hi" }] }],
|
||||
tools: [
|
||||
{
|
||||
functionDeclarations: [{ name: "a" }, { name: "b" }, { name: "c" }],
|
||||
},
|
||||
],
|
||||
generationConfig: { candidateCount: 5 },
|
||||
stream: true,
|
||||
},
|
||||
});
|
||||
assert.equal(antigravity.messageCount, 1);
|
||||
assert.equal(antigravity.toolCount, 3);
|
||||
assert.equal(antigravity.requestedFanout, 5);
|
||||
assert.equal(antigravity.streaming, true);
|
||||
|
||||
// Authoritative extraction context wins over body stream inference.
|
||||
const overridden = extractAdmissionCostFeatures(
|
||||
{ messages: [{ role: "user", content: "x" }], stream: false },
|
||||
{ streaming: true }
|
||||
);
|
||||
assert.equal(overridden.streaming, true);
|
||||
|
||||
const overriddenOff = extractAdmissionCostFeatures(
|
||||
{ messages: [{ role: "user", content: "x" }], stream: true },
|
||||
{ streaming: false }
|
||||
);
|
||||
assert.equal(overriddenOff.streaming, false);
|
||||
});
|
||||
|
||||
it("bounds tool scans and never touches entries beyond the budget (conservative count)", () => {
|
||||
// Huge leading string makes estimateSizeFast byte-exit before walking tools,
|
||||
// so only countTools can touch the tools proxy — proving its scan bound alone.
|
||||
const sizePad = "x".repeat(300_000);
|
||||
|
||||
let accesses = 0;
|
||||
const tools = new Proxy([] as unknown[], {
|
||||
get(target, prop, receiver) {
|
||||
if (prop === "length") return ADMISSION_TOOL_SCAN_BUDGET + 10_000;
|
||||
if (typeof prop === "string" && /^[0-9]+$/.test(prop)) {
|
||||
const index = Number(prop);
|
||||
accesses += 1;
|
||||
if (index >= ADMISSION_TOOL_SCAN_BUDGET) {
|
||||
throw new Error(`tool entry ${index} must not be touched`);
|
||||
}
|
||||
return { type: "function", function: { name: `t${index}` } };
|
||||
}
|
||||
return Reflect.get(target, prop, receiver);
|
||||
},
|
||||
});
|
||||
|
||||
const features = extractAdmissionCostFeatures({ pad: sizePad, tools });
|
||||
// Any uninspected tail saturates the feature so heavier unseen entries cannot undercharge.
|
||||
assert.equal(features.toolCount, Number.MAX_SAFE_INTEGER);
|
||||
assert.equal(accesses, 0, "known oversized source should saturate before indexed access");
|
||||
|
||||
// functionDeclarations length is O(1); truncated tail still cannot undercharge.
|
||||
let declAccesses = 0;
|
||||
const geminiTools = new Proxy([] as unknown[], {
|
||||
get(target, prop, receiver) {
|
||||
if (prop === "length") return ADMISSION_TOOL_SCAN_BUDGET + 50;
|
||||
if (typeof prop === "string" && /^[0-9]+$/.test(prop)) {
|
||||
const index = Number(prop);
|
||||
declAccesses += 1;
|
||||
if (index >= ADMISSION_TOOL_SCAN_BUDGET) {
|
||||
throw new Error(`gemini tool entry ${index} must not be touched`);
|
||||
}
|
||||
return {
|
||||
functionDeclarations: new Proxy([] as unknown[], {
|
||||
get(t, p, r) {
|
||||
if (p === "length") return 3;
|
||||
if (typeof p === "string" && /^[0-9]+$/.test(p)) {
|
||||
throw new Error("functionDeclarations elements need not be scanned");
|
||||
}
|
||||
return Reflect.get(t, p, r);
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
return Reflect.get(target, prop, receiver);
|
||||
},
|
||||
});
|
||||
|
||||
const geminiFeatures = extractAdmissionCostFeatures({ pad: sizePad, tools: geminiTools });
|
||||
assert.equal(geminiFeatures.toolCount, Number.MAX_SAFE_INTEGER);
|
||||
assert.equal(declAccesses, 0);
|
||||
|
||||
let aliasTouches = 0;
|
||||
const sixtyFour = (label: string) =>
|
||||
new Proxy([] as unknown[], {
|
||||
get(target, prop, receiver) {
|
||||
if (prop === "length") return ADMISSION_TOOL_SCAN_BUDGET;
|
||||
if (typeof prop === "string" && /^[0-9]+$/.test(prop)) {
|
||||
aliasTouches += 1;
|
||||
return { name: `${label}-${prop}` };
|
||||
}
|
||||
return Reflect.get(target, prop, receiver);
|
||||
},
|
||||
});
|
||||
const aliases = extractAdmissionCostFeatures({
|
||||
pad: sizePad,
|
||||
tools: sixtyFour("tool"),
|
||||
functions: sixtyFour("function"),
|
||||
});
|
||||
assert.equal(aliases.toolCount, Number.MAX_SAFE_INTEGER);
|
||||
assert.ok(aliasTouches <= ADMISSION_TOOL_SCAN_BUDGET);
|
||||
|
||||
let wrappedTouches = 0;
|
||||
const wrappedTail = new Proxy([] as unknown[], {
|
||||
get(target, prop, receiver) {
|
||||
if (prop === "length") return ADMISSION_TOOL_SCAN_BUDGET + 1;
|
||||
if (typeof prop === "string" && /^[0-9]+$/.test(prop)) {
|
||||
wrappedTouches += 1;
|
||||
return { functionDeclarations: new Array(1_000).fill(null) };
|
||||
}
|
||||
return Reflect.get(target, prop, receiver);
|
||||
},
|
||||
});
|
||||
const layered = extractAdmissionCostFeatures({
|
||||
pad: sizePad,
|
||||
tools: [{ type: "function" }],
|
||||
request: { tools: wrappedTail },
|
||||
});
|
||||
assert.equal(layered.toolCount, Number.MAX_SAFE_INTEGER);
|
||||
assert.equal(estimateAdmissionCost(layered), 1_000);
|
||||
assert.equal(wrappedTouches, 0);
|
||||
});
|
||||
|
||||
it("nested fanout under request wrapper is visible and stream defaults false", () => {
|
||||
const features = extractAdmissionCostFeatures({
|
||||
request: {
|
||||
messages: [{ role: "user", content: "x" }],
|
||||
n: 7,
|
||||
},
|
||||
});
|
||||
assert.equal(features.requestedFanout, 7);
|
||||
assert.equal(features.streaming, false);
|
||||
assert.equal(features.messageCount, 1);
|
||||
});
|
||||
});
|
||||
450
tests/unit/adaptive-admission-lifecycle.test.ts
Normal file
450
tests/unit/adaptive-admission-lifecycle.test.ts
Normal file
@@ -0,0 +1,450 @@
|
||||
import { describe, it, beforeEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
createAdaptiveAdmissionRuntime,
|
||||
DEFAULT_ADAPTIVE_ADMISSION_CONFIG,
|
||||
type AdaptiveAdmissionRuntime,
|
||||
} from "../../open-sse/services/admission/runtime.ts";
|
||||
import {
|
||||
type AdaptiveAdmissionConfig,
|
||||
type AdmissionLease,
|
||||
type AdmissionReleaseMeta,
|
||||
type AdmissionReleaseOutcome,
|
||||
} from "../../open-sse/services/admission/types.ts";
|
||||
import type {
|
||||
ResourcePressureGuardResult,
|
||||
ResourcePressureObservation,
|
||||
} from "../../open-sse/utils/resourcePressure.ts";
|
||||
|
||||
/** Purpose-built lease spy: counts every release() while exposing released after first call. */
|
||||
function createSpyLease(id = "spy-lease", cost = 1) {
|
||||
const calls: Array<{ outcome?: AdmissionReleaseOutcome; meta?: AdmissionReleaseMeta }> = [];
|
||||
let released = false;
|
||||
const lease: AdmissionLease = {
|
||||
id,
|
||||
cost,
|
||||
get released() {
|
||||
return released;
|
||||
},
|
||||
release(outcome?: AdmissionReleaseOutcome, meta?: AdmissionReleaseMeta) {
|
||||
calls.push({ outcome, meta });
|
||||
released = true;
|
||||
},
|
||||
};
|
||||
return {
|
||||
lease,
|
||||
calls,
|
||||
get releaseCount() {
|
||||
return calls.length;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
class FakeClock {
|
||||
nowMs = 0;
|
||||
private nextId = 1;
|
||||
private timers = new Map<number, { due: number; fn: () => void }>();
|
||||
|
||||
now = () => this.nowMs;
|
||||
|
||||
setTimer = (fn: () => void, delayMs: number): number => {
|
||||
const id = this.nextId++;
|
||||
this.timers.set(id, { due: this.nowMs + Math.max(0, delayMs), fn });
|
||||
return id;
|
||||
};
|
||||
|
||||
clearTimer = (id: number): void => {
|
||||
this.timers.delete(id);
|
||||
};
|
||||
|
||||
get pendingTimerCount(): number {
|
||||
return this.timers.size;
|
||||
}
|
||||
|
||||
advance(ms: number): void {
|
||||
const target = this.nowMs + ms;
|
||||
while (true) {
|
||||
let nextId: number | undefined;
|
||||
let nextDue = Number.POSITIVE_INFINITY;
|
||||
for (const [id, t] of this.timers) {
|
||||
if (t.due <= target && t.due < nextDue) {
|
||||
nextDue = t.due;
|
||||
nextId = id;
|
||||
}
|
||||
}
|
||||
if (nextId === undefined) {
|
||||
this.nowMs = target;
|
||||
return;
|
||||
}
|
||||
const timer = this.timers.get(nextId)!;
|
||||
this.timers.delete(nextId);
|
||||
this.nowMs = timer.due;
|
||||
timer.fn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function emptyObservation(
|
||||
overrides: Partial<ResourcePressureObservation["state"]> = {}
|
||||
): ResourcePressureObservation {
|
||||
return {
|
||||
signals: null,
|
||||
state: {
|
||||
severity: "normal",
|
||||
reason: "none",
|
||||
elevatedStreak: 0,
|
||||
recoveryStreak: 0,
|
||||
lastTransitionAtMs: 0,
|
||||
observedAtMs: 0,
|
||||
...overrides,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeRuntime(
|
||||
clock: FakeClock,
|
||||
overrides: {
|
||||
config?: AdaptiveAdmissionConfig;
|
||||
check?: () => ResourcePressureGuardResult | null;
|
||||
observe?: () => ResourcePressureObservation;
|
||||
warn?: (message: string) => void;
|
||||
} = {}
|
||||
): AdaptiveAdmissionRuntime {
|
||||
return createAdaptiveAdmissionRuntime({
|
||||
config: overrides.config ?? { ...DEFAULT_ADAPTIVE_ADMISSION_CONFIG },
|
||||
clock: {
|
||||
now: clock.now,
|
||||
setTimer: clock.setTimer,
|
||||
clearTimer: clock.clearTimer,
|
||||
},
|
||||
checkResourcePressure: overrides.check ?? (() => null),
|
||||
getResourcePressureObservation: overrides.observe ?? (() => emptyObservation()),
|
||||
warn: overrides.warn,
|
||||
});
|
||||
}
|
||||
|
||||
describe("response lifecycle helpers", () => {
|
||||
let clock: FakeClock;
|
||||
beforeEach(() => {
|
||||
clock = new FakeClock();
|
||||
});
|
||||
|
||||
function attachJson(
|
||||
runtime: AdaptiveAdmissionRuntime,
|
||||
spy: ReturnType<typeof createSpyLease>,
|
||||
status: number,
|
||||
options: { signal?: AbortSignal; admittedAtMs?: number } = {}
|
||||
) {
|
||||
const admittedAtMs = options.admittedAtMs ?? clock.nowMs;
|
||||
return runtime.attachResponseLifecycle(
|
||||
new Response(JSON.stringify({ ok: status < 400 }), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
spy.lease,
|
||||
{ admittedAtMs, signal: options.signal, nowMs: clock.now }
|
||||
);
|
||||
}
|
||||
|
||||
it("classifies non-SSE HTTP outcomes with cancellation winning", async () => {
|
||||
const runtime = makeRuntime(clock);
|
||||
const cases: Array<{
|
||||
status: number;
|
||||
expected: AdmissionReleaseOutcome;
|
||||
signal?: AbortSignal;
|
||||
label: string;
|
||||
}> = [
|
||||
{ status: 200, expected: "success", label: "2xx" },
|
||||
{ status: 302, expected: "success", label: "3xx" },
|
||||
{ status: 400, expected: "local_reject", label: "ordinary 4xx" },
|
||||
{ status: 429, expected: "local_reject", label: "429" },
|
||||
{ status: 408, expected: "timeout", label: "408" },
|
||||
{ status: 499, expected: "cancelled", label: "499" },
|
||||
{ status: 504, expected: "timeout", label: "504" },
|
||||
{ status: 502, expected: "upstream_error", label: "5xx" },
|
||||
{ status: 500, expected: "upstream_error", label: "500" },
|
||||
];
|
||||
|
||||
for (const c of cases) {
|
||||
const spy = createSpyLease(`json-${c.label}`);
|
||||
clock.nowMs = 100;
|
||||
attachJson(runtime, spy, c.status, { admittedAtMs: 40 });
|
||||
assert.equal(spy.releaseCount, 1, c.label);
|
||||
assert.equal(spy.calls[0]!.outcome, c.expected, c.label);
|
||||
assert.equal(spy.calls[0]!.meta?.latencyMs, 60, c.label);
|
||||
assert.equal(spy.lease.released, true, c.label);
|
||||
}
|
||||
|
||||
// Already-aborted signal wins over 2xx.
|
||||
const ac = new AbortController();
|
||||
ac.abort();
|
||||
const abortedSpy = createSpyLease("aborted-2xx");
|
||||
clock.nowMs = 200;
|
||||
attachJson(runtime, abortedSpy, 200, { signal: ac.signal, admittedAtMs: 150 });
|
||||
assert.equal(abortedSpy.releaseCount, 1);
|
||||
assert.equal(abortedSpy.calls[0]!.outcome, "cancelled");
|
||||
assert.equal(abortedSpy.calls[0]!.meta?.latencyMs, 50);
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
it("classifies SSE completion outcomes using the request signal", async () => {
|
||||
const runtime = makeRuntime(clock);
|
||||
let spySuffix = 0;
|
||||
|
||||
async function drainSse(
|
||||
status: number,
|
||||
signal?: AbortSignal,
|
||||
expectImmediateRelease = false
|
||||
): Promise<ReturnType<typeof createSpyLease>> {
|
||||
const spy = createSpyLease(`sse-${status}-${spySuffix++}`);
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode("data: done\n\n"));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
clock.nowMs = 300;
|
||||
const wrapped = runtime.attachResponseLifecycle(
|
||||
new Response(body, {
|
||||
status,
|
||||
statusText: "OK",
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
}),
|
||||
spy.lease,
|
||||
{ admittedAtMs: 250, signal, nowMs: clock.now }
|
||||
);
|
||||
if (expectImmediateRelease) {
|
||||
assert.equal(spy.releaseCount, 1);
|
||||
return spy;
|
||||
}
|
||||
assert.equal(spy.releaseCount, 0);
|
||||
await wrapped.text();
|
||||
return spy;
|
||||
}
|
||||
|
||||
const ok = await drainSse(200);
|
||||
assert.equal(ok.releaseCount, 1);
|
||||
assert.equal(ok.calls[0]!.outcome, "success");
|
||||
assert.equal(ok.calls[0]!.meta?.latencyMs, 50);
|
||||
|
||||
const redirect = await drainSse(302);
|
||||
assert.equal(redirect.calls[0]!.outcome, "success");
|
||||
|
||||
const ordinary4xx = await drainSse(404);
|
||||
assert.equal(ordinary4xx.calls[0]!.outcome, "local_reject");
|
||||
|
||||
const tooMany = await drainSse(429);
|
||||
assert.equal(tooMany.calls[0]!.outcome, "local_reject");
|
||||
|
||||
const requestTimeout = await drainSse(408);
|
||||
assert.equal(requestTimeout.calls[0]!.outcome, "timeout");
|
||||
|
||||
const clientGone = await drainSse(499);
|
||||
assert.equal(clientGone.calls[0]!.outcome, "cancelled");
|
||||
|
||||
const gatewayTimeout = await drainSse(504);
|
||||
assert.equal(gatewayTimeout.calls[0]!.outcome, "timeout");
|
||||
|
||||
const upstream = await drainSse(503);
|
||||
assert.equal(upstream.calls[0]!.outcome, "upstream_error");
|
||||
|
||||
// Already-aborted signal settles immediately as cancelled (wins over 2xx).
|
||||
const ac = new AbortController();
|
||||
ac.abort();
|
||||
const abortedOk = await drainSse(200, ac.signal, true);
|
||||
assert.equal(abortedOk.releaseCount, 1);
|
||||
assert.equal(abortedOk.calls[0]!.outcome, "cancelled");
|
||||
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
it("requires explicit non-success outcomes for handler failures", async () => {
|
||||
const runtime = makeRuntime(clock);
|
||||
const outcomes: Array<Exclude<AdmissionReleaseOutcome, "success">> = [
|
||||
"local_reject",
|
||||
"upstream_error",
|
||||
"timeout",
|
||||
"cancelled",
|
||||
];
|
||||
for (const outcome of outcomes) {
|
||||
const spy = createSpyLease(`handler-${outcome}`);
|
||||
clock.nowMs = 500;
|
||||
runtime.releaseHandlerFailure(spy.lease, outcome, {
|
||||
admittedAtMs: 400,
|
||||
nowMs: clock.now,
|
||||
});
|
||||
assert.equal(spy.releaseCount, 1, outcome);
|
||||
assert.equal(spy.calls[0]!.outcome, outcome);
|
||||
assert.equal(spy.calls[0]!.meta?.latencyMs, 100);
|
||||
// Exactly-once: second call must not re-release.
|
||||
runtime.releaseHandlerFailure(spy.lease, outcome, {
|
||||
admittedAtMs: 400,
|
||||
nowMs: clock.now,
|
||||
});
|
||||
assert.equal(spy.releaseCount, 1, `${outcome} second`);
|
||||
}
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
it("releases JSON/non-SSE responses immediately once", async () => {
|
||||
const runtime = makeRuntime(clock);
|
||||
const spy = createSpyLease("json-once");
|
||||
clock.nowMs = 80;
|
||||
const wrapped = attachJson(runtime, spy, 200, { admittedAtMs: 20 });
|
||||
assert.equal(spy.releaseCount, 1);
|
||||
assert.equal(spy.calls[0]!.outcome, "success");
|
||||
assert.equal(spy.calls[0]!.meta?.latencyMs, 60);
|
||||
assert.equal(await wrapped.text(), JSON.stringify({ ok: true }));
|
||||
runtime.attachResponseLifecycle(
|
||||
new Response("{}", { status: 200, headers: { "Content-Type": "application/json" } }),
|
||||
spy.lease,
|
||||
{ admittedAtMs: 20, nowMs: clock.now }
|
||||
);
|
||||
assert.equal(spy.releaseCount, 1);
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
it("keeps SSE lease until stream drain and releases exactly once", async () => {
|
||||
const runtime = makeRuntime(clock);
|
||||
const spy = createSpyLease("sse-drain");
|
||||
let pullCount = 0;
|
||||
const chunks = [
|
||||
new TextEncoder().encode("data: 1\n\n"),
|
||||
new TextEncoder().encode("data: 2\n\n"),
|
||||
];
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
pull(controller) {
|
||||
if (pullCount < chunks.length) {
|
||||
controller.enqueue(chunks[pullCount++]);
|
||||
return;
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
clock.nowMs = 120;
|
||||
const wrapped = runtime.attachResponseLifecycle(
|
||||
new Response(body, {
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
}),
|
||||
spy.lease,
|
||||
{ admittedAtMs: 100, nowMs: clock.now }
|
||||
);
|
||||
assert.equal(spy.releaseCount, 0);
|
||||
assert.equal(wrapped.status, 200);
|
||||
assert.equal(wrapped.statusText, "OK");
|
||||
assert.equal(wrapped.headers.get("Content-Type"), "text/event-stream");
|
||||
const text = await wrapped.text();
|
||||
assert.match(text, /data: 1/);
|
||||
assert.match(text, /data: 2/);
|
||||
assert.equal(spy.releaseCount, 1);
|
||||
assert.equal(spy.calls[0]!.outcome, "success");
|
||||
assert.equal(spy.calls[0]!.meta?.latencyMs, 20);
|
||||
// Drain again must not re-release (stream already consumed).
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
it("releases once on stream error", async () => {
|
||||
const runtime = makeRuntime(clock);
|
||||
const spy = createSpyLease("sse-error");
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
pull(controller) {
|
||||
controller.error(new Error("upstream boom"));
|
||||
},
|
||||
});
|
||||
clock.nowMs = 90;
|
||||
const wrapped = runtime.attachResponseLifecycle(
|
||||
new Response(body, { status: 200, headers: { "Content-Type": "text/event-stream" } }),
|
||||
spy.lease,
|
||||
{ admittedAtMs: 70, nowMs: clock.now }
|
||||
);
|
||||
await assert.rejects(async () => {
|
||||
await wrapped.text();
|
||||
});
|
||||
assert.equal(spy.releaseCount, 1);
|
||||
assert.equal(spy.calls[0]!.outcome, "upstream_error");
|
||||
assert.equal(spy.calls[0]!.meta?.latencyMs, 20);
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
it("releases once on consumer cancel without buffering", async () => {
|
||||
const runtime = makeRuntime(clock);
|
||||
const spy = createSpyLease("sse-cancel");
|
||||
let cancelCount = 0;
|
||||
let pulled = 0;
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
pull(controller) {
|
||||
pulled += 1;
|
||||
controller.enqueue(new TextEncoder().encode(`data: ${pulled}\n\n`));
|
||||
},
|
||||
cancel() {
|
||||
cancelCount += 1;
|
||||
},
|
||||
});
|
||||
clock.nowMs = 60;
|
||||
const wrapped = runtime.attachResponseLifecycle(
|
||||
new Response(body, { status: 200, headers: { "Content-Type": "text/event-stream" } }),
|
||||
spy.lease,
|
||||
{ admittedAtMs: 10, nowMs: clock.now }
|
||||
);
|
||||
const reader = wrapped.body!.getReader();
|
||||
await reader.read();
|
||||
assert.equal(spy.releaseCount, 0);
|
||||
const pulledAfterFirst = pulled;
|
||||
await reader.cancel("client gone");
|
||||
assert.equal(cancelCount, 1);
|
||||
assert.equal(spy.releaseCount, 1);
|
||||
assert.equal(spy.calls[0]!.outcome, "cancelled");
|
||||
assert.equal(spy.calls[0]!.meta?.latencyMs, 50);
|
||||
// Laziness: no full buffering of the infinite producer.
|
||||
assert.ok(pulledAfterFirst <= 2);
|
||||
assert.ok(pulled < 20);
|
||||
// Second cancel is a no-op for both reader cancel and lease release.
|
||||
await reader.cancel("again");
|
||||
assert.equal(cancelCount, 1);
|
||||
assert.equal(spy.releaseCount, 1);
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
it("request abort cancels the reader and releases once under races", async () => {
|
||||
const runtime = makeRuntime(clock);
|
||||
const spy = createSpyLease("sse-abort-race");
|
||||
const ac = new AbortController();
|
||||
let cancelCount = 0;
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
async pull(controller) {
|
||||
controller.enqueue(new TextEncoder().encode("data: ping\n\n"));
|
||||
await new Promise<void>(() => {
|
||||
/* hang until cancel */
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
cancelCount += 1;
|
||||
},
|
||||
});
|
||||
clock.nowMs = 40;
|
||||
const wrapped = runtime.attachResponseLifecycle(
|
||||
new Response(body, { status: 200, headers: { "Content-Type": "text/event-stream" } }),
|
||||
spy.lease,
|
||||
{ admittedAtMs: 10, signal: ac.signal, nowMs: clock.now }
|
||||
);
|
||||
const reader = wrapped.body!.getReader();
|
||||
const first = reader.read();
|
||||
ac.abort();
|
||||
// Race: also cancel consumer.
|
||||
void reader.cancel("race");
|
||||
await Promise.race([
|
||||
first.catch(() => undefined),
|
||||
new Promise((resolve) => setImmediate(resolve)),
|
||||
]);
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
assert.equal(spy.releaseCount, 1);
|
||||
assert.equal(spy.calls[0]!.outcome, "cancelled");
|
||||
assert.equal(typeof spy.calls[0]!.meta?.latencyMs, "number");
|
||||
assert.ok((spy.calls[0]!.meta?.latencyMs ?? -1) >= 0);
|
||||
assert.equal(cancelCount, 1);
|
||||
runtime.dispose();
|
||||
});
|
||||
});
|
||||
67
tests/unit/adaptive-admission-queue.test.ts
Normal file
67
tests/unit/adaptive-admission-queue.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { FairCostQueue, type QueueEntry } from "../../open-sse/services/admission/queue.ts";
|
||||
|
||||
describe("FairCostQueue removeById cursor preservation", () => {
|
||||
function qEntry(id: string, tenantKey: string, cost = 1): QueueEntry<{ id: string }> {
|
||||
return {
|
||||
id,
|
||||
tenantKey,
|
||||
cost,
|
||||
enqueuedAtMs: 0,
|
||||
deadlineMs: Number.MAX_SAFE_INTEGER,
|
||||
payload: { id },
|
||||
};
|
||||
}
|
||||
|
||||
it("preserves the logical successor when removing a bucket before the cursor", () => {
|
||||
const q = new FairCostQueue<{ id: string }>(10, 100);
|
||||
assert.equal(q.enqueue(qEntry("a1", "a")), true);
|
||||
assert.equal(q.enqueue(qEntry("a2", "a")), true);
|
||||
assert.equal(q.enqueue(qEntry("b1", "b")), true);
|
||||
assert.equal(q.enqueue(qEntry("c1", "c")), true);
|
||||
|
||||
// Dequeue a1 leaves cursor at b while tenant a still has a2.
|
||||
assert.equal(q.dequeue()?.id, "a1");
|
||||
assert.equal(q.removeById("a2")?.id, "a2");
|
||||
// Successor of the pre-removal cursor must remain b, not skip to c.
|
||||
assert.equal(q.dequeue()?.id, "b1");
|
||||
assert.equal(q.dequeue()?.id, "c1");
|
||||
assert.equal(q.size, 0);
|
||||
});
|
||||
|
||||
it("preserves the logical successor when removing the bucket at the cursor", () => {
|
||||
const q = new FairCostQueue<{ id: string }>(10, 100);
|
||||
assert.equal(q.enqueue(qEntry("a1", "a")), true);
|
||||
assert.equal(q.enqueue(qEntry("b1", "b")), true);
|
||||
assert.equal(q.enqueue(qEntry("c1", "c")), true);
|
||||
|
||||
assert.equal(q.dequeue()?.id, "a1"); // cursor now at b
|
||||
assert.equal(q.removeById("b1")?.id, "b1");
|
||||
assert.equal(q.dequeue()?.id, "c1");
|
||||
assert.equal(q.size, 0);
|
||||
});
|
||||
|
||||
it("preserves the cursor when removing a bucket after the cursor", () => {
|
||||
const q = new FairCostQueue<{ id: string }>(10, 100);
|
||||
assert.equal(q.enqueue(qEntry("a1", "a")), true);
|
||||
assert.equal(q.enqueue(qEntry("b1", "b")), true);
|
||||
assert.equal(q.enqueue(qEntry("c1", "c")), true);
|
||||
|
||||
assert.equal(q.dequeue()?.id, "a1"); // cursor now at b
|
||||
assert.equal(q.removeById("c1")?.id, "c1");
|
||||
assert.equal(q.dequeue()?.id, "b1");
|
||||
assert.equal(q.size, 0);
|
||||
});
|
||||
|
||||
it("resets the cursor when the final bucket is removed", () => {
|
||||
const q = new FairCostQueue<{ id: string }>(10, 100);
|
||||
assert.equal(q.enqueue(qEntry("a1", "a")), true);
|
||||
assert.equal(q.enqueue(qEntry("b1", "b")), true);
|
||||
assert.equal(q.dequeue()?.id, "a1"); // cursor at b
|
||||
assert.equal(q.removeById("b1")?.id, "b1");
|
||||
assert.equal(q.size, 0);
|
||||
assert.equal(q.enqueue(qEntry("d1", "d")), true);
|
||||
assert.equal(q.dequeue()?.id, "d1");
|
||||
});
|
||||
});
|
||||
454
tests/unit/adaptive-admission-route-matrix.test.ts
Normal file
454
tests/unit/adaptive-admission-route-matrix.test.ts
Normal file
@@ -0,0 +1,454 @@
|
||||
/**
|
||||
* Behavioral matrix: adaptive-admission enforce rejection across the 10 real
|
||||
* shared LLM POST route modules. Uses a test-owned POST table (not production
|
||||
* registries/globs). Asserts standardized 503 contract, zero provider fetch,
|
||||
* provider-health isolation, and runtime reject accounting.
|
||||
*
|
||||
* DB isolation: only Node/assert + harness are static imports; createChatPipelineHarness
|
||||
* must run before any dynamic runtime/resource/DB/route import so DATA_DIR is set first.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createChatPipelineHarness } from "../integration/_chatPipelineHarness.ts";
|
||||
|
||||
const harness = await createChatPipelineHarness("adaptive-admission-route-matrix");
|
||||
assert.ok(
|
||||
harness.TEST_DATA_DIR.includes("adaptive-admission-route-matrix") ||
|
||||
harness.TEST_DATA_DIR.includes("omniroute-"),
|
||||
"task-private harness DATA_DIR must be set before DB imports"
|
||||
);
|
||||
console.log(`[adaptive-admission-route-matrix] DATA_DIR=${harness.TEST_DATA_DIR}`);
|
||||
|
||||
const { BaseExecutor, resetStorage, seedConnection, cleanup } = harness;
|
||||
|
||||
const {
|
||||
getAdaptiveAdmissionRuntime,
|
||||
reloadAdaptiveAdmissionRuntime,
|
||||
resetAdaptiveAdmissionRuntimeForTests,
|
||||
} = await import("../../open-sse/services/admission/runtime.ts");
|
||||
const { reloadResourcePressureRuntime } = await import("../../open-sse/utils/resourcePressure.ts");
|
||||
const { getProviderConnectionById } = await import("../../src/lib/db/providers.ts");
|
||||
const { getCircuitBreaker, resetAllCircuitBreakers, STATE } =
|
||||
await import("../../src/shared/utils/circuitBreaker.ts");
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const relayProxies = await import("../../src/lib/db/relayProxies.ts");
|
||||
|
||||
const chatCompletionsRoute = await import("../../src/app/api/v1/chat/completions/route.ts");
|
||||
const messagesRoute = await import("../../src/app/api/v1/messages/route.ts");
|
||||
const responsesRoute = await import("../../src/app/api/v1/responses/route.ts");
|
||||
const responsesCatchAllRoute = await import("../../src/app/api/v1/responses/[...path]/route.ts");
|
||||
const completionsRoute = await import("../../src/app/api/v1/completions/route.ts");
|
||||
const ollamaRoute = await import("../../src/app/api/v1/api/chat/route.ts");
|
||||
const antigravityRoute = await import("../../src/app/api/v1/antigravity/route.ts");
|
||||
const providerPinnedRoute =
|
||||
await import("../../src/app/api/v1/providers/[provider]/chat/completions/route.ts");
|
||||
const relayRoute = await import("../../src/app/api/v1/relay/chat/completions/route.ts");
|
||||
const geminiRoute = await import("../../src/app/api/v1beta/models/[...path]/route.ts");
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
const MiB = 1024 ** 2;
|
||||
const MODEL = "openai/gpt-4o-mini";
|
||||
const ADMISSION_MESSAGE = "Request too large for current capacity";
|
||||
|
||||
type RouteCase = {
|
||||
name: string;
|
||||
invoke: (request: Request) => Promise<Response>;
|
||||
buildRequest: () => Request;
|
||||
};
|
||||
|
||||
function padContent(label: string, targetBytes = 2048): string {
|
||||
const base = `${label}-admission-matrix-`;
|
||||
return base + "y".repeat(Math.max(0, targetBytes - base.length));
|
||||
}
|
||||
|
||||
function jsonRequest(
|
||||
url: string,
|
||||
body: unknown,
|
||||
headers: Record<string, string> = {},
|
||||
signal?: AbortSignal
|
||||
): Request {
|
||||
return new Request(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
...headers,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal,
|
||||
});
|
||||
}
|
||||
|
||||
function chatBody(label: string) {
|
||||
return {
|
||||
model: MODEL,
|
||||
stream: false,
|
||||
messages: [{ role: "user", content: padContent(label) }],
|
||||
};
|
||||
}
|
||||
|
||||
function messagesBody(label: string) {
|
||||
return {
|
||||
model: MODEL,
|
||||
max_tokens: 64,
|
||||
stream: false,
|
||||
messages: [{ role: "user", content: padContent(label) }],
|
||||
};
|
||||
}
|
||||
|
||||
function responsesBody(label: string) {
|
||||
return {
|
||||
model: MODEL,
|
||||
stream: false,
|
||||
input: [{ role: "user", content: padContent(label) }],
|
||||
};
|
||||
}
|
||||
|
||||
function completionsBody(label: string) {
|
||||
return {
|
||||
model: MODEL,
|
||||
stream: false,
|
||||
prompt: padContent(label, 3072),
|
||||
};
|
||||
}
|
||||
|
||||
function antigravityBody(label: string) {
|
||||
return {
|
||||
model: MODEL,
|
||||
project: "admission-matrix-project",
|
||||
request: {
|
||||
contents: [{ role: "user", parts: [{ text: padContent(label) }] }],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function geminiBody(label: string) {
|
||||
return {
|
||||
contents: [{ role: "user", parts: [{ text: padContent(label) }] }],
|
||||
};
|
||||
}
|
||||
|
||||
function insertRelayToken(rawToken: string) {
|
||||
const db = core.getDbInstance();
|
||||
const id = "rl_admission_matrix";
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const tokenHash = createHash("sha256").update(rawToken).digest("hex");
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO relay_tokens (id, name, token_hash, token_prefix, description, combo_id, allowed_models,
|
||||
max_tokens_per_request, max_requests_per_minute, max_requests_per_day, max_cost_per_day,
|
||||
enabled, created_at, updated_at, expires_at, metadata)
|
||||
VALUES (?, ?, ?, ?, '', NULL, '["*"]', 128000, 1000, 100000, 0, 1, ?, ?, NULL, '{}')
|
||||
`
|
||||
).run(id, "admission-matrix-relay", tokenHash, "rl_matrix", now, now);
|
||||
const token = relayProxies.getRelayToken(id);
|
||||
if (!token) throw new Error("failed to insert matrix relay token");
|
||||
return { token, rawToken };
|
||||
}
|
||||
|
||||
function reloadNormalResourcePressure() {
|
||||
reloadResourcePressureRuntime({
|
||||
heapThresholdMb: 10_000,
|
||||
immediateHeapUsedMb: () => 1,
|
||||
sample: async () => ({
|
||||
observedAtMs: Date.now(),
|
||||
v8: { heapUsedBytes: MiB, heapLimitBytes: 10_000 * MiB },
|
||||
process: {
|
||||
rssBytes: MiB,
|
||||
externalBytes: 0,
|
||||
arrayBuffersBytes: 0,
|
||||
availableBytes: null,
|
||||
constrainedBytes: null,
|
||||
},
|
||||
cgroup: { currentBytes: null, maxBytes: null, highBytes: null, events: null },
|
||||
psi: null,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function reloadEnforceOversized() {
|
||||
reloadAdaptiveAdmissionRuntime({
|
||||
config: {
|
||||
mode: "enforce",
|
||||
minLimit: 1,
|
||||
initialLimit: 1,
|
||||
maxLimit: 1,
|
||||
maxQueueCount: 1,
|
||||
maxQueueCost: 1,
|
||||
defaultMaxWaitMs: 50,
|
||||
windowMs: 50,
|
||||
cost: {
|
||||
maxRequestCost: 100,
|
||||
baseCost: 1,
|
||||
bodyBytesPerUnit: 1,
|
||||
tokensPerUnit: 1,
|
||||
messagesPerUnit: 1,
|
||||
toolsPerUnit: 1,
|
||||
fanoutPerUnit: 1,
|
||||
streamingClassCost: 1,
|
||||
nonStreamingClassCost: 1,
|
||||
},
|
||||
},
|
||||
checkResourcePressure: () => null,
|
||||
});
|
||||
}
|
||||
|
||||
function connectionFailureState(connection: Record<string, unknown> | null) {
|
||||
assert.ok(connection);
|
||||
return {
|
||||
isActive: connection.isActive,
|
||||
testStatus: connection.testStatus,
|
||||
rateLimitedUntil: connection.rateLimitedUntil ?? null,
|
||||
backoffLevel: connection.backoffLevel ?? null,
|
||||
lastError: connection.lastError ?? null,
|
||||
lastErrorAt: connection.lastErrorAt ?? null,
|
||||
lastErrorType: connection.lastErrorType ?? null,
|
||||
lastErrorSource: connection.lastErrorSource ?? null,
|
||||
errorCode: connection.errorCode ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function breakerSnapshot(breaker: ReturnType<typeof getCircuitBreaker>) {
|
||||
const status = breaker.getStatus();
|
||||
return {
|
||||
state: status.state,
|
||||
failureCount: status.failureCount,
|
||||
successCount: breaker.successCount,
|
||||
};
|
||||
}
|
||||
|
||||
async function assertAdmissionOversized(response: Response, fetchCalls: number) {
|
||||
assert.equal(response.status, 503);
|
||||
assert.equal(fetchCalls, 0);
|
||||
const contentType = String(response.headers.get("content-type") || "");
|
||||
assert.match(contentType, /application\/json/i);
|
||||
const payload = (await response.json()) as {
|
||||
error?: { code?: string; type?: string; message?: string };
|
||||
};
|
||||
assert.equal(payload.error?.type, "server_error");
|
||||
assert.equal(payload.error?.code, "admission_oversized");
|
||||
assert.equal(payload.error?.message, ADMISSION_MESSAGE);
|
||||
}
|
||||
|
||||
// Test-owned table of the exact 10 canonical shared LLM POST handlers.
|
||||
const ROUTE_CASES: RouteCase[] = [
|
||||
{
|
||||
name: "chat.completions",
|
||||
invoke: (request) => chatCompletionsRoute.POST(request),
|
||||
buildRequest: () =>
|
||||
jsonRequest("http://localhost/v1/chat/completions", chatBody("chat-completions")),
|
||||
},
|
||||
{
|
||||
name: "messages",
|
||||
invoke: (request) => messagesRoute.POST(request, {}),
|
||||
buildRequest: () => jsonRequest("http://localhost/v1/messages", messagesBody("messages")),
|
||||
},
|
||||
{
|
||||
name: "responses",
|
||||
invoke: (request) => responsesRoute.POST(request, {}),
|
||||
buildRequest: () =>
|
||||
jsonRequest("http://localhost/v1/responses", responsesBody("responses"), {
|
||||
Accept: "application/json",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "responses.catch-all",
|
||||
invoke: (request) => responsesCatchAllRoute.POST(request),
|
||||
buildRequest: () =>
|
||||
jsonRequest(
|
||||
"http://localhost/v1/responses/input_items",
|
||||
responsesBody("responses-catch-all"),
|
||||
{ Accept: "application/json" }
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "completions.legacy",
|
||||
invoke: (request) => completionsRoute.POST(request),
|
||||
buildRequest: () =>
|
||||
jsonRequest("http://localhost/v1/completions", completionsBody("legacy-completions")),
|
||||
},
|
||||
{
|
||||
name: "ollama.api.chat",
|
||||
invoke: (request) => ollamaRoute.POST(request),
|
||||
buildRequest: () => jsonRequest("http://localhost/api/chat", chatBody("ollama")),
|
||||
},
|
||||
{
|
||||
name: "antigravity",
|
||||
invoke: (request) => antigravityRoute.POST(request),
|
||||
buildRequest: () =>
|
||||
jsonRequest("http://localhost/v1/antigravity", antigravityBody("antigravity")),
|
||||
},
|
||||
{
|
||||
name: "providers.pinned",
|
||||
invoke: (request) =>
|
||||
providerPinnedRoute.POST(request, { params: Promise.resolve({ provider: "openai" }) }),
|
||||
buildRequest: () =>
|
||||
jsonRequest("http://localhost/v1/providers/openai/chat/completions", {
|
||||
model: "gpt-4o-mini",
|
||||
stream: false,
|
||||
messages: [{ role: "user", content: padContent("provider-pinned") }],
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "relay.chat.completions",
|
||||
invoke: (request) => relayRoute.POST(request),
|
||||
buildRequest: () => {
|
||||
throw new Error("relay buildRequest is set per-test after token insert");
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gemini.v1beta.generateContent",
|
||||
invoke: (request) =>
|
||||
geminiRoute.POST(request, {
|
||||
params: Promise.resolve({ path: ["openai", "gpt-4o-mini:generateContent"] }),
|
||||
}),
|
||||
buildRequest: () =>
|
||||
jsonRequest(
|
||||
"http://localhost/v1beta/models/openai/gpt-4o-mini:generateContent",
|
||||
geminiBody("gemini")
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
test.beforeEach(async () => {
|
||||
BaseExecutor.RETRY_CONFIG.delayMs = 0;
|
||||
await resetStorage();
|
||||
resetAllCircuitBreakers();
|
||||
resetAdaptiveAdmissionRuntimeForTests();
|
||||
reloadNormalResourcePressure();
|
||||
reloadEnforceOversized();
|
||||
globalThis.fetch = originalFetch;
|
||||
delete process.env.OMNIROUTE_RELAY_BACKEND;
|
||||
delete process.env.RELAY_ROUTING_BACKEND;
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
globalThis.fetch = originalFetch;
|
||||
resetAdaptiveAdmissionRuntimeForTests();
|
||||
delete process.env.OMNIROUTE_RELAY_BACKEND;
|
||||
delete process.env.RELAY_ROUTING_BACKEND;
|
||||
await resetStorage();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
globalThis.fetch = originalFetch;
|
||||
resetAdaptiveAdmissionRuntimeForTests();
|
||||
await cleanup();
|
||||
});
|
||||
|
||||
test(
|
||||
"adaptive admission enforce rejects all 10 shared LLM POST routes with standardized contract",
|
||||
{ timeout: 30_000 },
|
||||
async () => {
|
||||
const connection = await seedConnection("openai", {
|
||||
name: "admission-matrix-openai",
|
||||
apiKey: "sk-openai-admission-matrix",
|
||||
});
|
||||
const connectionId = String(connection.id);
|
||||
const beforeConnection = connectionFailureState(
|
||||
(await getProviderConnectionById(connectionId)) as Record<string, unknown> | null
|
||||
);
|
||||
const breaker = getCircuitBreaker("openai");
|
||||
const beforeBreaker = breakerSnapshot(breaker);
|
||||
assert.equal(beforeBreaker.state, STATE.CLOSED);
|
||||
|
||||
const rawRelayToken = `relay_matrix_${createHash("sha256").update("admission").digest("hex").slice(0, 24)}`;
|
||||
insertRelayToken(rawRelayToken);
|
||||
process.env.OMNIROUTE_RELAY_BACKEND = "ts";
|
||||
|
||||
const cases: RouteCase[] = ROUTE_CASES.map((routeCase) => {
|
||||
if (routeCase.name !== "relay.chat.completions") return routeCase;
|
||||
return {
|
||||
...routeCase,
|
||||
buildRequest: () =>
|
||||
jsonRequest("http://localhost/api/v1/relay/chat/completions", chatBody("relay"), {
|
||||
Authorization: `Bearer ${rawRelayToken}`,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
assert.equal(cases.length, 10);
|
||||
|
||||
let fetchCalls = 0;
|
||||
globalThis.fetch = async () => {
|
||||
fetchCalls += 1;
|
||||
return new Response("provider must not run under admission reject", { status: 500 });
|
||||
};
|
||||
|
||||
const beforeRuntime = getAdaptiveAdmissionRuntime().snapshot();
|
||||
assert.equal(beforeRuntime.activeCount, 0);
|
||||
assert.equal(beforeRuntime.queuedCount, 0);
|
||||
|
||||
for (const routeCase of cases) {
|
||||
const rejectedBefore = getAdaptiveAdmissionRuntime().snapshot().rejectedCount;
|
||||
const response = await routeCase.invoke(routeCase.buildRequest());
|
||||
await assertAdmissionOversized(response, fetchCalls);
|
||||
|
||||
const afterCase = getAdaptiveAdmissionRuntime().snapshot();
|
||||
assert.equal(
|
||||
afterCase.rejectedCount,
|
||||
rejectedBefore + 1,
|
||||
`${routeCase.name}: rejectedCount must increment once`
|
||||
);
|
||||
assert.equal(afterCase.activeCount, 0, `${routeCase.name}: activeCount must return to 0`);
|
||||
assert.equal(afterCase.queuedCount, 0, `${routeCase.name}: queuedCount must return to 0`);
|
||||
assert.equal(fetchCalls, 0, `${routeCase.name}: fetch must stay 0`);
|
||||
}
|
||||
|
||||
const afterRuntime = getAdaptiveAdmissionRuntime().snapshot();
|
||||
assert.equal(afterRuntime.rejectedCount, beforeRuntime.rejectedCount + cases.length);
|
||||
assert.equal(afterRuntime.activeCount, 0);
|
||||
assert.equal(afterRuntime.queuedCount, 0);
|
||||
assert.equal(fetchCalls, 0);
|
||||
|
||||
assert.deepEqual(
|
||||
connectionFailureState(
|
||||
(await getProviderConnectionById(connectionId)) as Record<string, unknown> | null
|
||||
),
|
||||
beforeConnection
|
||||
);
|
||||
assert.deepEqual(breakerSnapshot(breaker), beforeBreaker);
|
||||
}
|
||||
);
|
||||
|
||||
test(
|
||||
"provider-pinned route propagates request AbortSignal into admission rejection",
|
||||
{ timeout: 5_000 },
|
||||
async () => {
|
||||
await seedConnection("openai", {
|
||||
name: "admission-abort-openai",
|
||||
apiKey: "sk-openai-admission-abort",
|
||||
});
|
||||
reloadEnforceOversized();
|
||||
|
||||
let fetchCalls = 0;
|
||||
globalThis.fetch = async () => {
|
||||
fetchCalls += 1;
|
||||
return new Response("provider must not run", { status: 500 });
|
||||
};
|
||||
|
||||
const ac = new AbortController();
|
||||
ac.abort();
|
||||
const response = await providerPinnedRoute.POST(
|
||||
jsonRequest(
|
||||
"http://localhost/v1/providers/openai/chat/completions",
|
||||
{
|
||||
model: "gpt-4o-mini",
|
||||
stream: false,
|
||||
messages: [{ role: "user", content: padContent("abort-provider") }],
|
||||
},
|
||||
{},
|
||||
ac.signal
|
||||
),
|
||||
{ params: Promise.resolve({ provider: "openai" }) }
|
||||
);
|
||||
|
||||
assert.equal(response.status, 499);
|
||||
assert.equal(fetchCalls, 0);
|
||||
const payload = (await response.json()) as { error?: { code?: string; type?: string } };
|
||||
assert.equal(payload.error?.code, "admission_aborted");
|
||||
assert.equal(payload.error?.type, "client_disconnected");
|
||||
assert.equal(getAdaptiveAdmissionRuntime().snapshot().activeCount, 0);
|
||||
}
|
||||
);
|
||||
857
tests/unit/adaptive-admission-runtime.test.ts
Normal file
857
tests/unit/adaptive-admission-runtime.test.ts
Normal file
@@ -0,0 +1,857 @@
|
||||
import { describe, it, beforeEach, afterEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
createAdaptiveAdmissionRuntime,
|
||||
getAdaptiveAdmissionRuntime,
|
||||
reloadAdaptiveAdmissionRuntime,
|
||||
resetAdaptiveAdmissionRuntimeForTests,
|
||||
resolveAdaptiveAdmissionConfigFromEnv,
|
||||
DEFAULT_ADAPTIVE_ADMISSION_CONFIG,
|
||||
type AdaptiveAdmissionRuntime,
|
||||
} from "../../open-sse/services/admission/runtime.ts";
|
||||
import {
|
||||
MAX_ADMISSION_COST_OR_LIMIT,
|
||||
MAX_ADMISSION_WINDOW_MS,
|
||||
type AdaptiveAdmissionConfig,
|
||||
} from "../../open-sse/services/admission/types.ts";
|
||||
import type {
|
||||
ResourcePressureGuardResult,
|
||||
ResourcePressureObservation,
|
||||
} from "../../open-sse/utils/resourcePressure.ts";
|
||||
import { buildErrorBody } from "../../open-sse/utils/error.ts";
|
||||
|
||||
class FakeClock {
|
||||
nowMs = 0;
|
||||
private nextId = 1;
|
||||
private timers = new Map<number, { due: number; fn: () => void }>();
|
||||
|
||||
now = () => this.nowMs;
|
||||
|
||||
setTimer = (fn: () => void, delayMs: number): number => {
|
||||
const id = this.nextId++;
|
||||
this.timers.set(id, { due: this.nowMs + Math.max(0, delayMs), fn });
|
||||
return id;
|
||||
};
|
||||
|
||||
clearTimer = (id: number): void => {
|
||||
this.timers.delete(id);
|
||||
};
|
||||
|
||||
get pendingTimerCount(): number {
|
||||
return this.timers.size;
|
||||
}
|
||||
|
||||
advance(ms: number): void {
|
||||
const target = this.nowMs + ms;
|
||||
while (true) {
|
||||
let nextId: number | undefined;
|
||||
let nextDue = Number.POSITIVE_INFINITY;
|
||||
for (const [id, t] of this.timers) {
|
||||
if (t.due <= target && t.due < nextDue) {
|
||||
nextDue = t.due;
|
||||
nextId = id;
|
||||
}
|
||||
}
|
||||
if (nextId === undefined) {
|
||||
this.nowMs = target;
|
||||
return;
|
||||
}
|
||||
const timer = this.timers.get(nextId)!;
|
||||
this.timers.delete(nextId);
|
||||
this.nowMs = timer.due;
|
||||
timer.fn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function enforceConfig(overrides: Partial<AdaptiveAdmissionConfig> = {}): AdaptiveAdmissionConfig {
|
||||
return {
|
||||
mode: "enforce",
|
||||
minLimit: 4,
|
||||
maxLimit: 20,
|
||||
initialLimit: 8,
|
||||
maxQueueCount: 2,
|
||||
maxQueueCost: 16,
|
||||
defaultMaxWaitMs: 100,
|
||||
windowMs: 50,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function emptyObservation(
|
||||
overrides: Partial<ResourcePressureObservation["state"]> = {}
|
||||
): ResourcePressureObservation {
|
||||
return {
|
||||
signals: null,
|
||||
state: {
|
||||
severity: "normal",
|
||||
reason: "none",
|
||||
elevatedStreak: 0,
|
||||
recoveryStreak: 0,
|
||||
lastTransitionAtMs: 0,
|
||||
observedAtMs: 0,
|
||||
...overrides,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function criticalGuard(reason = "v8_heap_absolute"): ResourcePressureGuardResult {
|
||||
const message = "Service temporarily unavailable due to resource pressure. Retry shortly.";
|
||||
return {
|
||||
success: false,
|
||||
status: 503,
|
||||
error: message,
|
||||
response: new Response(
|
||||
JSON.stringify(
|
||||
buildErrorBody(503, message, undefined, {
|
||||
type: "server_error",
|
||||
code: "resource_pressure",
|
||||
})
|
||||
),
|
||||
{
|
||||
status: 503,
|
||||
headers: { "Content-Type": "application/json", "Retry-After": "5" },
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function makeRuntime(
|
||||
clock: FakeClock,
|
||||
overrides: {
|
||||
config?: AdaptiveAdmissionConfig;
|
||||
check?: () => ResourcePressureGuardResult | null;
|
||||
observe?: () => ResourcePressureObservation;
|
||||
warn?: (message: string) => void;
|
||||
} = {}
|
||||
): AdaptiveAdmissionRuntime {
|
||||
return createAdaptiveAdmissionRuntime({
|
||||
config: overrides.config ?? { ...DEFAULT_ADAPTIVE_ADMISSION_CONFIG },
|
||||
clock: {
|
||||
now: clock.now,
|
||||
setTimer: clock.setTimer,
|
||||
clearTimer: clock.clearTimer,
|
||||
},
|
||||
checkResourcePressure: overrides.check ?? (() => null),
|
||||
getResourcePressureObservation: overrides.observe ?? (() => emptyObservation()),
|
||||
warn: overrides.warn,
|
||||
});
|
||||
}
|
||||
|
||||
async function parseJson(response: Response): Promise<Record<string, unknown>> {
|
||||
return JSON.parse(await response.text()) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
describe("adaptive admission runtime env + defaults", () => {
|
||||
it("defaults to complete shadow config", () => {
|
||||
assert.equal(DEFAULT_ADAPTIVE_ADMISSION_CONFIG.mode, "shadow");
|
||||
assert.equal(DEFAULT_ADAPTIVE_ADMISSION_CONFIG.minLimit, 8);
|
||||
assert.equal(DEFAULT_ADAPTIVE_ADMISSION_CONFIG.initialLimit, 64);
|
||||
assert.equal(DEFAULT_ADAPTIVE_ADMISSION_CONFIG.maxLimit, 1000);
|
||||
assert.equal(DEFAULT_ADAPTIVE_ADMISSION_CONFIG.maxQueueCount, 128);
|
||||
assert.equal(DEFAULT_ADAPTIVE_ADMISSION_CONFIG.maxQueueCost, 2000);
|
||||
assert.equal(DEFAULT_ADAPTIVE_ADMISSION_CONFIG.defaultMaxWaitMs, 5000);
|
||||
assert.equal(DEFAULT_ADAPTIVE_ADMISSION_CONFIG.windowMs, 1000);
|
||||
});
|
||||
|
||||
it("strictly resolves supported env names and rejects invalid values", () => {
|
||||
const cfg = resolveAdaptiveAdmissionConfigFromEnv({
|
||||
ADAPTIVE_ADMISSION_MODE: "enforce",
|
||||
ADAPTIVE_ADMISSION_MIN_LIMIT: "10",
|
||||
ADAPTIVE_ADMISSION_INITIAL_LIMIT: "20",
|
||||
ADAPTIVE_ADMISSION_MAX_LIMIT: "30",
|
||||
ADAPTIVE_ADMISSION_MAX_QUEUE_COUNT: "40",
|
||||
ADAPTIVE_ADMISSION_MAX_QUEUE_COST: "50",
|
||||
ADAPTIVE_ADMISSION_MAX_WAIT_MS: "600",
|
||||
ADAPTIVE_ADMISSION_WINDOW_MS: "700",
|
||||
});
|
||||
assert.deepEqual(
|
||||
{
|
||||
mode: cfg.mode,
|
||||
minLimit: cfg.minLimit,
|
||||
initialLimit: cfg.initialLimit,
|
||||
maxLimit: cfg.maxLimit,
|
||||
maxQueueCount: cfg.maxQueueCount,
|
||||
maxQueueCost: cfg.maxQueueCost,
|
||||
defaultMaxWaitMs: cfg.defaultMaxWaitMs,
|
||||
windowMs: cfg.windowMs,
|
||||
},
|
||||
{
|
||||
mode: "enforce",
|
||||
minLimit: 10,
|
||||
initialLimit: 20,
|
||||
maxLimit: 30,
|
||||
maxQueueCount: 40,
|
||||
maxQueueCost: 50,
|
||||
defaultMaxWaitMs: 600,
|
||||
windowMs: 700,
|
||||
}
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => resolveAdaptiveAdmissionConfigFromEnv({ ADAPTIVE_ADMISSION_MODE: "strict" }),
|
||||
/ADAPTIVE_ADMISSION_MODE/
|
||||
);
|
||||
assert.throws(
|
||||
() => resolveAdaptiveAdmissionConfigFromEnv({ ADAPTIVE_ADMISSION_MIN_LIMIT: "0" }),
|
||||
/ADAPTIVE_ADMISSION_MIN_LIMIT/
|
||||
);
|
||||
assert.throws(
|
||||
() => resolveAdaptiveAdmissionConfigFromEnv({ ADAPTIVE_ADMISSION_MAX_LIMIT: "1.5" }),
|
||||
/ADAPTIVE_ADMISSION_MAX_LIMIT/
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts exact documented maxima and rejects max+1 plus cross-field invalidity", () => {
|
||||
const maxCost = String(MAX_ADMISSION_COST_OR_LIMIT);
|
||||
const maxWindow = String(MAX_ADMISSION_WINDOW_MS);
|
||||
const maxQueue = String(Number.MAX_SAFE_INTEGER);
|
||||
|
||||
const atMaxima = resolveAdaptiveAdmissionConfigFromEnv({
|
||||
ADAPTIVE_ADMISSION_MODE: "shadow",
|
||||
ADAPTIVE_ADMISSION_MIN_LIMIT: "1",
|
||||
ADAPTIVE_ADMISSION_INITIAL_LIMIT: maxCost,
|
||||
ADAPTIVE_ADMISSION_MAX_LIMIT: maxCost,
|
||||
ADAPTIVE_ADMISSION_MAX_QUEUE_COUNT: maxQueue,
|
||||
ADAPTIVE_ADMISSION_MAX_QUEUE_COST: maxCost,
|
||||
ADAPTIVE_ADMISSION_MAX_WAIT_MS: maxWindow,
|
||||
ADAPTIVE_ADMISSION_WINDOW_MS: maxWindow,
|
||||
});
|
||||
assert.equal(atMaxima.maxLimit, MAX_ADMISSION_COST_OR_LIMIT);
|
||||
assert.equal(atMaxima.maxQueueCount, Number.MAX_SAFE_INTEGER);
|
||||
assert.equal(atMaxima.windowMs, MAX_ADMISSION_WINDOW_MS);
|
||||
assert.equal(atMaxima.defaultMaxWaitMs, MAX_ADMISSION_WINDOW_MS);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveAdaptiveAdmissionConfigFromEnv({
|
||||
ADAPTIVE_ADMISSION_MAX_LIMIT: String(MAX_ADMISSION_COST_OR_LIMIT + 1),
|
||||
}),
|
||||
/maxLimit|must be <=/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveAdaptiveAdmissionConfigFromEnv({
|
||||
ADAPTIVE_ADMISSION_MAX_QUEUE_COST: String(MAX_ADMISSION_COST_OR_LIMIT + 1),
|
||||
}),
|
||||
/maxQueueCost|must be <=/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveAdaptiveAdmissionConfigFromEnv({
|
||||
ADAPTIVE_ADMISSION_WINDOW_MS: String(MAX_ADMISSION_WINDOW_MS + 1),
|
||||
}),
|
||||
/windowMs|must be <=/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveAdaptiveAdmissionConfigFromEnv({
|
||||
ADAPTIVE_ADMISSION_MAX_WAIT_MS: String(MAX_ADMISSION_WINDOW_MS + 1),
|
||||
}),
|
||||
/defaultMaxWaitMs|must be <=/
|
||||
);
|
||||
// Queue count uses full safe-integer range; beyond that fails lexical/safe-integer parsing.
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveAdaptiveAdmissionConfigFromEnv({
|
||||
ADAPTIVE_ADMISSION_MAX_QUEUE_COUNT: "9007199254740992",
|
||||
}),
|
||||
/ADAPTIVE_ADMISSION_MAX_QUEUE_COUNT|safe integer/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveAdaptiveAdmissionConfigFromEnv({
|
||||
ADAPTIVE_ADMISSION_MIN_LIMIT: "20",
|
||||
ADAPTIVE_ADMISSION_MAX_LIMIT: "10",
|
||||
}),
|
||||
/minLimit must be <= maxLimit/
|
||||
);
|
||||
});
|
||||
|
||||
it("default process runtime falls back to shadow on invalid env without crashing", () => {
|
||||
resetAdaptiveAdmissionRuntimeForTests();
|
||||
const warnings: string[] = [];
|
||||
const previous = process.env.ADAPTIVE_ADMISSION_MODE;
|
||||
process.env.ADAPTIVE_ADMISSION_MODE = "not-a-mode";
|
||||
try {
|
||||
const runtime = reloadAdaptiveAdmissionRuntime({
|
||||
warn: (message) => warnings.push(message),
|
||||
checkResourcePressure: () => null,
|
||||
getResourcePressureObservation: () => emptyObservation(),
|
||||
});
|
||||
const snap = runtime.snapshot();
|
||||
assert.equal(snap.mode, "shadow");
|
||||
assert.equal(snap.minLimit, 8);
|
||||
assert.equal(snap.initialLimit ?? snap.currentLimit >= 8, true);
|
||||
assert.equal(warnings.length, 1);
|
||||
assert.match(
|
||||
warnings[0]!,
|
||||
/invalid environment configuration; using default shadow admission settings/
|
||||
);
|
||||
assert.ok(!warnings.join("\n").includes("not-a-mode"));
|
||||
assert.ok(!warnings.join("\n").toLowerCase().includes("secret"));
|
||||
runtime.dispose();
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.ADAPTIVE_ADMISSION_MODE;
|
||||
else process.env.ADAPTIVE_ADMISSION_MODE = previous;
|
||||
resetAdaptiveAdmissionRuntimeForTests();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("adaptive admission runtime modes", () => {
|
||||
let clock: FakeClock;
|
||||
beforeEach(() => {
|
||||
clock = new FakeClock();
|
||||
});
|
||||
afterEach(() => {
|
||||
resetAdaptiveAdmissionRuntimeForTests();
|
||||
});
|
||||
|
||||
it("default shadow always admits with a real lease and shadowDecision", async () => {
|
||||
const runtime = makeRuntime(clock);
|
||||
const result = await runtime.acquire({
|
||||
tenantKey: "tenant-secret-1",
|
||||
body: { messages: [{ role: "user", content: "hi" }], stream: true },
|
||||
});
|
||||
assert.equal(result.status, "admitted");
|
||||
if (result.status !== "admitted") throw new Error("expected admitted");
|
||||
assert.equal(result.mode, "shadow");
|
||||
assert.ok(result.lease);
|
||||
assert.equal(typeof result.lease.release, "function");
|
||||
assert.equal(result.lease.released, false);
|
||||
assert.ok(
|
||||
result.shadowDecision === "would-admit" ||
|
||||
result.shadowDecision === "would-queue" ||
|
||||
result.shadowDecision === "would-reject"
|
||||
);
|
||||
result.lease.release("success");
|
||||
assert.equal(result.lease.released, true);
|
||||
result.lease.release("success");
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
it("explicit off admits without enforcing capacity", async () => {
|
||||
const runtime = makeRuntime(clock, {
|
||||
config: {
|
||||
...DEFAULT_ADAPTIVE_ADMISSION_CONFIG,
|
||||
mode: "off",
|
||||
initialLimit: 1,
|
||||
minLimit: 1,
|
||||
maxLimit: 1,
|
||||
},
|
||||
});
|
||||
const a = await runtime.acquire({ tenantKey: "t1", body: { messages: [] } });
|
||||
const b = await runtime.acquire({ tenantKey: "t2", body: { messages: [] } });
|
||||
assert.equal(a.status, "admitted");
|
||||
assert.equal(b.status, "admitted");
|
||||
if (a.status === "admitted") a.lease.release();
|
||||
if (b.status === "admitted") b.lease.release();
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
it("explicit enforce can reject with sanitized HTTP response", async () => {
|
||||
const runtime = makeRuntime(clock, {
|
||||
config: enforceConfig({
|
||||
initialLimit: 1,
|
||||
minLimit: 1,
|
||||
maxLimit: 1,
|
||||
maxQueueCount: 1,
|
||||
maxQueueCost: 1,
|
||||
defaultMaxWaitMs: 50,
|
||||
cost: { maxRequestCost: 1, baseCost: 1 },
|
||||
}),
|
||||
});
|
||||
const first = await runtime.acquire({
|
||||
tenantKey: "t1",
|
||||
body: { messages: [{ role: "user", content: "a" }], stream: true },
|
||||
});
|
||||
assert.equal(first.status, "admitted");
|
||||
|
||||
const secondPromise = runtime.acquire({
|
||||
tenantKey: "t2",
|
||||
body: { messages: [{ role: "user", content: "b" }], stream: true },
|
||||
maxWaitMs: 50,
|
||||
});
|
||||
// Drive injected deadline timer; no wall-clock sleeps.
|
||||
clock.advance(50);
|
||||
const second = await secondPromise;
|
||||
assert.equal(second.status, "rejected");
|
||||
if (second.status !== "rejected") throw new Error("expected rejected");
|
||||
assert.equal(second.response.status, 503);
|
||||
const body = await parseJson(second.response);
|
||||
assert.equal(typeof body.error.message, "string");
|
||||
assert.match(second.code, /^admission_/);
|
||||
assert.ok(!JSON.stringify(body).includes("t2"));
|
||||
assert.ok(!JSON.stringify(body).includes("tenant"));
|
||||
if (first.status === "admitted") first.lease.release();
|
||||
runtime.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe("runtime streaming cost forwarding", () => {
|
||||
it("acquire lease cost reflects input.streaming via feature extraction", async () => {
|
||||
const clock = new FakeClock();
|
||||
// Sharply distinct streaming class costs; neutralize other feature contributions.
|
||||
const runtime = makeRuntime(clock, {
|
||||
config: {
|
||||
...DEFAULT_ADAPTIVE_ADMISSION_CONFIG,
|
||||
mode: "shadow",
|
||||
cost: {
|
||||
baseCost: 1,
|
||||
bodyBytesPerUnit: 1_000_000,
|
||||
tokensPerUnit: 1_000_000,
|
||||
messagesPerUnit: 1_000_000,
|
||||
toolsPerUnit: 1_000_000,
|
||||
fanoutPerUnit: 1_000_000,
|
||||
streamingClassCost: 1,
|
||||
nonStreamingClassCost: 50,
|
||||
maxRequestCost: 1_000,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Empty body keeps non-class contributions identical; stream omitted defaults false when not forwarded.
|
||||
|
||||
const body = {};
|
||||
const streamed = await runtime.acquire({
|
||||
tenantKey: "stream-on",
|
||||
body,
|
||||
streaming: true,
|
||||
});
|
||||
assert.equal(streamed.status, "admitted");
|
||||
if (streamed.status !== "admitted") throw new Error("expected admitted");
|
||||
const streamCost = streamed.lease.cost;
|
||||
streamed.lease.release("success");
|
||||
|
||||
const nonStreamed = await runtime.acquire({
|
||||
tenantKey: "stream-off",
|
||||
body,
|
||||
streaming: false,
|
||||
});
|
||||
assert.equal(nonStreamed.status, "admitted");
|
||||
if (nonStreamed.status !== "admitted") throw new Error("expected admitted");
|
||||
const nonStreamCost = nonStreamed.lease.cost;
|
||||
nonStreamed.lease.release("success");
|
||||
|
||||
const defaulted = await runtime.acquire({
|
||||
tenantKey: "stream-default",
|
||||
body,
|
||||
});
|
||||
assert.equal(defaulted.status, "admitted");
|
||||
if (defaulted.status !== "admitted") throw new Error("expected admitted");
|
||||
const defaultCost = defaulted.lease.cost;
|
||||
defaulted.lease.release("success");
|
||||
|
||||
runtime.dispose();
|
||||
|
||||
// base(1) + fanout unit(1) + class cost → streaming 3, non-streaming 52
|
||||
assert.equal(streamCost, 3);
|
||||
assert.equal(nonStreamCost, 52);
|
||||
assert.equal(defaultCost, 52);
|
||||
assert.notEqual(
|
||||
streamCost,
|
||||
nonStreamCost,
|
||||
"streaming true/false must produce different acquired lease costs"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rejection mapping", () => {
|
||||
let clock: FakeClock;
|
||||
beforeEach(() => {
|
||||
clock = new FakeClock();
|
||||
});
|
||||
|
||||
it("maps ADMISSION_ABORTED to local 499 without Retry-After", async () => {
|
||||
const runtime = makeRuntime(clock, {
|
||||
config: enforceConfig({
|
||||
initialLimit: 1,
|
||||
minLimit: 1,
|
||||
maxLimit: 1,
|
||||
maxQueueCount: 4,
|
||||
maxQueueCost: 40,
|
||||
defaultMaxWaitMs: 1000,
|
||||
// Force unit cost so one admitted request fills the limit.
|
||||
cost: { maxRequestCost: 1, baseCost: 1 },
|
||||
}),
|
||||
});
|
||||
const holder = await runtime.acquire({
|
||||
tenantKey: "hold",
|
||||
body: { messages: [{ role: "user", content: "hold" }], stream: true },
|
||||
});
|
||||
assert.equal(holder.status, "admitted");
|
||||
|
||||
const ac = new AbortController();
|
||||
const pending = runtime.acquire({
|
||||
tenantKey: "wait",
|
||||
body: { messages: [{ role: "user", content: "wait" }], stream: true },
|
||||
signal: ac.signal,
|
||||
maxWaitMs: 1000,
|
||||
});
|
||||
ac.abort();
|
||||
const rejected = await pending;
|
||||
assert.equal(rejected.status, "rejected");
|
||||
if (rejected.status !== "rejected") throw new Error("expected rejected");
|
||||
assert.equal(rejected.code, "admission_aborted");
|
||||
assert.equal(rejected.response.status, 499);
|
||||
assert.equal(rejected.response.headers.get("Retry-After"), null);
|
||||
const body = await parseJson(rejected.response);
|
||||
assert.equal(body.error.code, "admission_aborted");
|
||||
assert.ok(!JSON.stringify(body).includes("wait"));
|
||||
if (holder.status === "admitted") holder.lease.release();
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
it("maps queue full / deadline / oversized to sanitized 503 codes", async () => {
|
||||
const runtime = makeRuntime(clock, {
|
||||
config: enforceConfig({
|
||||
initialLimit: 1,
|
||||
minLimit: 1,
|
||||
maxLimit: 1,
|
||||
maxQueueCount: 1,
|
||||
maxQueueCost: 1,
|
||||
defaultMaxWaitMs: 20,
|
||||
cost: { maxRequestCost: 1, baseCost: 1, bodyBytesPerUnit: 1_000_000 },
|
||||
}),
|
||||
});
|
||||
const hold = await runtime.acquire({
|
||||
tenantKey: "hold",
|
||||
body: { stream: true },
|
||||
});
|
||||
assert.equal(hold.status, "admitted");
|
||||
|
||||
const deadlinePromise = runtime.acquire({
|
||||
tenantKey: "q1",
|
||||
body: { stream: true },
|
||||
maxWaitMs: 20,
|
||||
});
|
||||
clock.advance(20);
|
||||
const deadlineRejected = await deadlinePromise;
|
||||
assert.equal(deadlineRejected.status, "rejected");
|
||||
if (deadlineRejected.status === "rejected") {
|
||||
assert.equal(deadlineRejected.response.status, 503);
|
||||
assert.equal(deadlineRejected.code, "admission_deadline");
|
||||
const body = await parseJson(deadlineRejected.response);
|
||||
assert.equal(body.error.code, "admission_deadline");
|
||||
assert.equal(deadlineRejected.response.headers.get("Retry-After"), "1");
|
||||
}
|
||||
|
||||
// Fill the single queue slot then force queue_full on the next arrival.
|
||||
const waiterPromise = runtime.acquire({
|
||||
tenantKey: "waiter",
|
||||
body: { stream: true },
|
||||
maxWaitMs: 1_000,
|
||||
});
|
||||
const full = await runtime.acquire({
|
||||
tenantKey: "full",
|
||||
body: { stream: true },
|
||||
});
|
||||
assert.equal(full.status, "rejected");
|
||||
if (full.status === "rejected") {
|
||||
assert.equal(full.code, "admission_queue_full");
|
||||
assert.equal(full.response.status, 503);
|
||||
assert.equal(full.response.headers.get("Retry-After"), "1");
|
||||
const body = await parseJson(full.response);
|
||||
assert.equal(body.error.code, "admission_queue_full");
|
||||
}
|
||||
clock.advance(1_000);
|
||||
await waiterPromise;
|
||||
|
||||
// Oversized: cost features that exceed limit 1 with tiny max.
|
||||
const oversizedRuntime = makeRuntime(clock, {
|
||||
config: enforceConfig({
|
||||
initialLimit: 1,
|
||||
minLimit: 1,
|
||||
maxLimit: 1,
|
||||
maxQueueCount: 1,
|
||||
maxQueueCost: 1,
|
||||
cost: {
|
||||
maxRequestCost: 100,
|
||||
baseCost: 1,
|
||||
bodyBytesPerUnit: 1,
|
||||
tokensPerUnit: 1,
|
||||
messagesPerUnit: 1,
|
||||
toolsPerUnit: 1,
|
||||
fanoutPerUnit: 1,
|
||||
},
|
||||
}),
|
||||
});
|
||||
const huge = await oversizedRuntime.acquire({
|
||||
tenantKey: "huge",
|
||||
body: {
|
||||
messages: Array.from({ length: 50 }, (_, i) => ({
|
||||
role: "user",
|
||||
content: `m${i}-${"x".repeat(32)}`,
|
||||
})),
|
||||
stream: true,
|
||||
},
|
||||
});
|
||||
assert.equal(huge.status, "rejected");
|
||||
if (huge.status === "rejected") {
|
||||
assert.equal(huge.code, "admission_oversized");
|
||||
assert.equal(huge.response.status, 503);
|
||||
const body = await parseJson(huge.response);
|
||||
assert.equal(body.error.code, "admission_oversized");
|
||||
assert.ok(!JSON.stringify(body).toLowerCase().includes("cost"));
|
||||
assert.ok(!JSON.stringify(body).includes("huge"));
|
||||
}
|
||||
if (hold.status === "admitted") hold.lease.release();
|
||||
runtime.dispose();
|
||||
oversizedRuntime.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resource pressure integration", () => {
|
||||
let clock: FakeClock;
|
||||
beforeEach(() => {
|
||||
clock = new FakeClock();
|
||||
});
|
||||
|
||||
it("returns the existing critical guard response without acquiring work", async () => {
|
||||
let acquires = 0;
|
||||
const guard = criticalGuard();
|
||||
const runtime = makeRuntime(clock, {
|
||||
config: enforceConfig({ initialLimit: 10 }),
|
||||
check: () => {
|
||||
acquires += 1;
|
||||
return guard;
|
||||
},
|
||||
});
|
||||
const result = await runtime.acquire({
|
||||
tenantKey: "t-pressure",
|
||||
body: { messages: [{ role: "user", content: "x" }] },
|
||||
});
|
||||
assert.equal(result.status, "rejected");
|
||||
if (result.status !== "rejected") throw new Error("expected rejected");
|
||||
assert.equal(result.response, guard.response);
|
||||
assert.equal(result.code, "resource_pressure");
|
||||
assert.equal(runtime.snapshot().pressureGuardRejectCount, 1);
|
||||
assert.equal(runtime.snapshot().activeCount, 0);
|
||||
assert.equal(acquires, 1);
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
it("feeds fresh critical pressure observation even when the safety guard rejects", async () => {
|
||||
const guard = criticalGuard();
|
||||
let observation = emptyObservation({
|
||||
severity: "critical",
|
||||
reason: "v8_heap_absolute",
|
||||
observedAtMs: 1_000,
|
||||
});
|
||||
const pressures: string[] = [];
|
||||
const runtime = createAdaptiveAdmissionRuntime({
|
||||
config: enforceConfig({
|
||||
initialLimit: 20,
|
||||
minLimit: 4,
|
||||
maxLimit: 20,
|
||||
windowMs: 50,
|
||||
criticalDecreaseFactor: 0.5,
|
||||
}),
|
||||
clock: {
|
||||
now: clock.now,
|
||||
setTimer: clock.setTimer,
|
||||
clearTimer: clock.clearTimer,
|
||||
},
|
||||
checkResourcePressure: () => guard,
|
||||
getResourcePressureObservation: () => observation,
|
||||
onPressureObserved: (pressure) => pressures.push(pressure),
|
||||
});
|
||||
|
||||
const first = await runtime.acquire({
|
||||
tenantKey: "guarded",
|
||||
body: { messages: [{ role: "user", content: "x" }] },
|
||||
});
|
||||
assert.equal(first.status, "rejected");
|
||||
if (first.status !== "rejected") throw new Error("expected rejected");
|
||||
// Exact same guard response identity; zero controller acquisition.
|
||||
assert.equal(first.response, guard.response);
|
||||
assert.equal(first.code, "resource_pressure");
|
||||
assert.equal(runtime.snapshot().activeCount, 0);
|
||||
assert.deepEqual(pressures, ["critical"]);
|
||||
// One critical reduction: floor(20 * 0.5) = 10.
|
||||
assert.equal(runtime.snapshot().currentLimit, 10);
|
||||
|
||||
// Replay same observation: no additional feed or reduction.
|
||||
const second = await runtime.acquire({
|
||||
tenantKey: "guarded-2",
|
||||
body: { messages: [{ role: "user", content: "y" }] },
|
||||
});
|
||||
assert.equal(second.response, guard.response);
|
||||
assert.deepEqual(pressures, ["critical"]);
|
||||
assert.equal(runtime.snapshot().currentLimit, 10);
|
||||
|
||||
// New window resets criticalDecreaseConsumed; fresh observation may reduce again.
|
||||
clock.advance(50);
|
||||
observation = emptyObservation({
|
||||
severity: "critical",
|
||||
reason: "v8_heap_absolute",
|
||||
observedAtMs: 2_000,
|
||||
});
|
||||
const third = await runtime.acquire({
|
||||
tenantKey: "guarded-3",
|
||||
body: { messages: [{ role: "user", content: "z" }] },
|
||||
});
|
||||
assert.equal(third.response, guard.response);
|
||||
assert.deepEqual(pressures, ["critical", "critical"]);
|
||||
assert.equal(runtime.snapshot().currentLimit, 5);
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
it("dedupes unchanged observations and re-feeds genuinely fresh ones", async () => {
|
||||
let observation = emptyObservation({
|
||||
severity: "high",
|
||||
reason: "psi_some",
|
||||
observedAtMs: 100,
|
||||
});
|
||||
const pressures: string[] = [];
|
||||
const runtime = createAdaptiveAdmissionRuntime({
|
||||
config: { ...DEFAULT_ADAPTIVE_ADMISSION_CONFIG, mode: "shadow" },
|
||||
clock: {
|
||||
now: clock.now,
|
||||
setTimer: clock.setTimer,
|
||||
clearTimer: clock.clearTimer,
|
||||
},
|
||||
checkResourcePressure: () => null,
|
||||
getResourcePressureObservation: () => observation,
|
||||
onPressureObserved: (pressure) => pressures.push(pressure),
|
||||
});
|
||||
|
||||
await runtime.acquire({ tenantKey: "a", body: {} });
|
||||
await runtime.acquire({ tenantKey: "b", body: {} });
|
||||
assert.deepEqual(pressures, ["high"]);
|
||||
|
||||
observation = emptyObservation({
|
||||
severity: "high",
|
||||
reason: "psi_some",
|
||||
observedAtMs: 100,
|
||||
});
|
||||
await runtime.acquire({ tenantKey: "c", body: {} });
|
||||
assert.deepEqual(pressures, ["high"]);
|
||||
|
||||
observation = emptyObservation({
|
||||
severity: "critical",
|
||||
reason: "psi_full",
|
||||
observedAtMs: 200,
|
||||
});
|
||||
await runtime.acquire({ tenantKey: "d", body: {} });
|
||||
assert.deepEqual(pressures, ["high", "critical"]);
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
it("fails open when pressure check or observation throws", async () => {
|
||||
const runtime = makeRuntime(clock, {
|
||||
config: { ...DEFAULT_ADAPTIVE_ADMISSION_CONFIG, mode: "shadow" },
|
||||
check: () => {
|
||||
throw new Error("check boom");
|
||||
},
|
||||
observe: () => {
|
||||
throw new Error("observe boom");
|
||||
},
|
||||
});
|
||||
const result = await runtime.acquire({ tenantKey: "t", body: { messages: [] } });
|
||||
assert.equal(result.status, "admitted");
|
||||
if (result.status === "admitted") result.lease.release();
|
||||
runtime.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe("public snapshot privacy", () => {
|
||||
it("exposes only aggregate counters and low-cardinality resource fields", async () => {
|
||||
const clock = new FakeClock();
|
||||
const runtime = makeRuntime(clock, {
|
||||
observe: () =>
|
||||
emptyObservation({
|
||||
severity: "high",
|
||||
reason: "cgroup_ratio",
|
||||
observedAtMs: 42,
|
||||
}),
|
||||
});
|
||||
await runtime.acquire({
|
||||
tenantKey: "tenant-very-secret",
|
||||
body: {
|
||||
messages: [{ role: "user", content: "SECRET_PAYLOAD_XYZ" }],
|
||||
api_key: "sk-live-secret",
|
||||
},
|
||||
});
|
||||
const snap = runtime.snapshot();
|
||||
const text = JSON.stringify(snap);
|
||||
assert.ok(!text.includes("tenant-very-secret"));
|
||||
assert.ok(!text.includes("SECRET_PAYLOAD_XYZ"));
|
||||
assert.ok(!text.includes("sk-live-secret"));
|
||||
assert.ok(!text.includes("lease-"));
|
||||
assert.equal(typeof snap.mode, "string");
|
||||
assert.equal(typeof snap.currentLimit, "number");
|
||||
assert.equal(typeof snap.activeCount, "number");
|
||||
assert.equal(snap.resourceSeverity, "high");
|
||||
assert.equal(snap.resourceReason, "cgroup_ratio");
|
||||
assert.equal(snap.resourceObservedAtMs, 42);
|
||||
assert.equal(typeof snap.pressureGuardRejectCount, "number");
|
||||
const snapRecord = snap as unknown as Record<string, unknown>;
|
||||
assert.equal(snapRecord.tenants, undefined);
|
||||
assert.equal(snapRecord.queue, undefined);
|
||||
assert.equal(snapRecord.features, undefined);
|
||||
runtime.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe("process runtime reload isolation", () => {
|
||||
afterEach(() => {
|
||||
resetAdaptiveAdmissionRuntimeForTests();
|
||||
});
|
||||
|
||||
it("reload disposes previous queued work/timers and replaces the process runtime", async () => {
|
||||
resetAdaptiveAdmissionRuntimeForTests();
|
||||
const clock = new FakeClock();
|
||||
const first = reloadAdaptiveAdmissionRuntime({
|
||||
config: enforceConfig({
|
||||
initialLimit: 1,
|
||||
minLimit: 1,
|
||||
maxLimit: 1,
|
||||
maxQueueCount: 4,
|
||||
maxQueueCost: 40,
|
||||
defaultMaxWaitMs: 5_000,
|
||||
windowMs: 1_000,
|
||||
cost: { maxRequestCost: 1, baseCost: 1 },
|
||||
}),
|
||||
clock: {
|
||||
now: clock.now,
|
||||
setTimer: clock.setTimer,
|
||||
clearTimer: clock.clearTimer,
|
||||
},
|
||||
checkResourcePressure: () => null,
|
||||
getResourcePressureObservation: () => emptyObservation(),
|
||||
});
|
||||
|
||||
const hold = await first.acquire({
|
||||
tenantKey: "hold",
|
||||
body: { messages: [{ role: "user", content: "h" }], stream: true },
|
||||
});
|
||||
assert.equal(hold.status, "admitted");
|
||||
assert.ok(clock.pendingTimerCount >= 1);
|
||||
|
||||
const waiting = first.acquire({
|
||||
tenantKey: "waiter",
|
||||
body: { messages: [{ role: "user", content: "w" }], stream: true },
|
||||
maxWaitMs: 5_000,
|
||||
});
|
||||
|
||||
const second = reloadAdaptiveAdmissionRuntime({
|
||||
config: { ...DEFAULT_ADAPTIVE_ADMISSION_CONFIG, mode: "shadow" },
|
||||
checkResourcePressure: () => null,
|
||||
getResourcePressureObservation: () => emptyObservation(),
|
||||
});
|
||||
assert.notEqual(second, first);
|
||||
assert.equal(getAdaptiveAdmissionRuntime(), second);
|
||||
|
||||
const rejected = await waiting;
|
||||
assert.equal(rejected.status, "rejected");
|
||||
if (rejected.status === "rejected") {
|
||||
assert.equal(rejected.code, "admission_shutdown");
|
||||
}
|
||||
// Previous timers should be cleared by dispose/shutdown.
|
||||
assert.equal(clock.pendingTimerCount, 0);
|
||||
second.dispose();
|
||||
resetAdaptiveAdmissionRuntimeForTests();
|
||||
});
|
||||
});
|
||||
120
tests/unit/authz/probe-9033-repro.test.ts
Normal file
120
tests/unit/authz/probe-9033-repro.test.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
// Repro test for #9033 — IP blacklist does not block on direct connections
|
||||
// and does not propagate without restart.
|
||||
// D1: blacklisted IP on a DIRECT connection (trusted peer stamp, no XFF) is NOT blocked
|
||||
// D2: persisted config written after first load is never re-read by the loaded instance
|
||||
// Bonus: ipFilterModeSchema rejects "whitelist-priority" that the UI offers and checkIP implements
|
||||
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 { NextRequest } from "next/server";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-9033-repro-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.JWT_SECRET = "test-secret-9033";
|
||||
|
||||
const core = await import("../../../src/lib/db/core.ts");
|
||||
const ipFilter = await import("../../../open-sse/services/ipFilter.ts");
|
||||
const pipeline = await import("../../../src/server/authz/pipeline.ts");
|
||||
|
||||
const ORIGINAL_STAMP_TOKEN = process.env.OMNIROUTE_PEER_STAMP_TOKEN;
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
if (ORIGINAL_STAMP_TOKEN === undefined) delete process.env.OMNIROUTE_PEER_STAMP_TOKEN;
|
||||
else process.env.OMNIROUTE_PEER_STAMP_TOKEN = ORIGINAL_STAMP_TOKEN;
|
||||
});
|
||||
|
||||
test.beforeEach(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
ipFilter.resetIPFilter();
|
||||
delete process.env.OMNIROUTE_PEER_STAMP_TOKEN;
|
||||
});
|
||||
|
||||
const BLOCKED = "203.0.113.99";
|
||||
|
||||
function makeRequest(extraHeaders: Record<string, string> = {}) {
|
||||
return new NextRequest("http://localhost/v1/models", {
|
||||
headers: { ...extraHeaders },
|
||||
});
|
||||
}
|
||||
|
||||
test("D1: blacklisted IP is blocked on a DIRECT connection (trusted peer stamp, no XFF)", async () => {
|
||||
process.env.OMNIROUTE_PEER_STAMP_TOKEN = "stamp-tok";
|
||||
ipFilter.configureIPFilter({ enabled: true, mode: "blacklist" });
|
||||
ipFilter.addToBlacklist(BLOCKED);
|
||||
|
||||
// Simulate a direct connection: the peer stamp says the client is BLOCKED,
|
||||
// and there is no x-forwarded-for header (direct connection, not via proxy).
|
||||
const res = await pipeline.runAuthzPipeline(
|
||||
makeRequest({ "x-omniroute-peer-ip": "stamp-tok|203.0.113.99" }),
|
||||
{ enforce: true }
|
||||
);
|
||||
|
||||
assert.equal(res.status, 403, `direct blacklisted IP must be blocked, got status=${res.status}`);
|
||||
});
|
||||
|
||||
test("D2: persisted config written after first load is honored WITHOUT restart", async () => {
|
||||
// Simulate: the settings route (separate module instance) writes config to DB.
|
||||
// The ipFilter module instance (already loaded) must re-read it.
|
||||
// First, load the module once (simulates initial load from a previous request).
|
||||
ipFilter.resetIPFilter();
|
||||
ipFilter.configureIPFilter({ enabled: true, mode: "blacklist" });
|
||||
ipFilter.addToBlacklist(BLOCKED);
|
||||
assert.equal(ipFilter.checkIP(BLOCKED).allowed, false, "blacklist must be active after config");
|
||||
|
||||
// Now simulate a "settings route" write: write directly to the DB key_value table
|
||||
// with a DIFFERENT config (e.g. empty blacklist, effectively "allow all").
|
||||
const db = core.getDbInstance();
|
||||
db.prepare("INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run(
|
||||
"ipFilter",
|
||||
"config",
|
||||
JSON.stringify({ enabled: true, mode: "blacklist", blacklist: [], whitelist: [] })
|
||||
);
|
||||
|
||||
// Without a restart, the ipFilter instance must re-read from DB on next checkIP call.
|
||||
// The BLOCKED IP should NOT be blocked anymore because the DB config has empty blacklist.
|
||||
const result = ipFilter.checkIP(BLOCKED);
|
||||
assert.equal(
|
||||
result.allowed,
|
||||
true,
|
||||
`stale-config enforcer must re-read DB, got: ${JSON.stringify(result)}`
|
||||
);
|
||||
});
|
||||
|
||||
test("D3: behind reverse proxy (peer stamp=loopback + via-proxy marker + XFF=blacklisted IP) still blocks", async () => {
|
||||
process.env.OMNIROUTE_PEER_STAMP_TOKEN = "stamp-tok";
|
||||
ipFilter.configureIPFilter({ enabled: true, mode: "blacklist" });
|
||||
ipFilter.addToBlacklist(BLOCKED);
|
||||
|
||||
// Behind a reverse proxy: the peer IP is the proxy hop (127.0.0.1),
|
||||
// the via-proxy marker is set, and the real client IP is in x-forwarded-for.
|
||||
const res = await pipeline.runAuthzPipeline(
|
||||
makeRequest({
|
||||
"x-omniroute-peer-ip": "stamp-tok|127.0.0.1",
|
||||
"x-omniroute-via-proxy": "stamp-tok|1",
|
||||
"x-forwarded-for": BLOCKED,
|
||||
}),
|
||||
{ enforce: true }
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
res.status,
|
||||
403,
|
||||
`behind-proxy blacklisted IP must be blocked, got status=${res.status}`
|
||||
);
|
||||
});
|
||||
|
||||
test("Bonus: ipFilterModeSchema accepts whitelist-priority", async () => {
|
||||
const { ipFilterModeSchema } = await import("../../../src/shared/validation/schemas/misc.ts");
|
||||
const result = ipFilterModeSchema.safeParse("whitelist-priority");
|
||||
assert.equal(
|
||||
result.success,
|
||||
true,
|
||||
`ipFilterModeSchema must accept "whitelist-priority", got: ${JSON.stringify(result)}`
|
||||
);
|
||||
});
|
||||
@@ -404,7 +404,7 @@ test("resolveProjectRoot walks up from start dir to nearest package.json or .git
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-root-"));
|
||||
const subDir = path.join(tempRoot, "sub", "deep");
|
||||
fs.mkdirSync(subDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(tempRoot, "package.json"), "{}");
|
||||
fs.writeFileSync(path.join(tempRoot, "package.json"), JSON.stringify({ name: "omniroute" }));
|
||||
|
||||
try {
|
||||
// Walking up from a deep subdir that does not have markers must find the real root.
|
||||
|
||||
443
tests/unit/chat-adaptive-admission-binding.test.ts
Normal file
443
tests/unit/chat-adaptive-admission-binding.test.ts
Normal file
@@ -0,0 +1,443 @@
|
||||
/**
|
||||
* Shared handleChat ↔ adaptive admission binding tests.
|
||||
* Proves policy-seam acquire, lazy client-raw, early pre-acquire returns,
|
||||
* enforce rejection before provider work, and default shadow admission.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createChatPipelineHarness } from "../integration/_chatPipelineHarness.ts";
|
||||
|
||||
const harness = await createChatPipelineHarness("chat-adaptive-admission-binding");
|
||||
const { BaseExecutor, buildRequest, handleChat, resetStorage, seedConnection } = harness;
|
||||
const {
|
||||
getAdaptiveAdmissionRuntime,
|
||||
reloadAdaptiveAdmissionRuntime,
|
||||
resetAdaptiveAdmissionRuntimeForTests,
|
||||
} = await import("../../open-sse/services/admission/runtime.ts");
|
||||
const { buildClientRawRequest } = await import("../../src/sse/handlers/chat/clientRawRequest.ts");
|
||||
const { getProviderConnectionById } = await import("../../src/lib/db/providers.ts");
|
||||
const { reloadResourcePressureRuntime } = await import("../../open-sse/utils/resourcePressure.ts");
|
||||
const { getCircuitBreaker, resetAllCircuitBreakers, STATE } =
|
||||
await import("../../src/shared/utils/circuitBreaker.ts");
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
const MiB = 1024 ** 2;
|
||||
|
||||
function reloadNormalResourcePressure() {
|
||||
reloadResourcePressureRuntime({
|
||||
heapThresholdMb: 10_000,
|
||||
immediateHeapUsedMb: () => 1,
|
||||
sample: async () => ({
|
||||
observedAtMs: Date.now(),
|
||||
v8: { heapUsedBytes: MiB, heapLimitBytes: 10_000 * MiB },
|
||||
process: {
|
||||
rssBytes: MiB,
|
||||
externalBytes: 0,
|
||||
arrayBuffersBytes: 0,
|
||||
availableBytes: null,
|
||||
constrainedBytes: null,
|
||||
},
|
||||
cgroup: { currentBytes: null, maxBytes: null, highBytes: null, events: null },
|
||||
psi: null,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function reloadCriticalResourcePressure() {
|
||||
reloadResourcePressureRuntime({
|
||||
heapThresholdMb: 100,
|
||||
immediateHeapUsedMb: () => 500,
|
||||
sample: async () => {
|
||||
throw new Error("critical request path must not await the async sampler");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function connectionFailureState(connection: Record<string, unknown> | null) {
|
||||
assert.ok(connection);
|
||||
return {
|
||||
isActive: connection.isActive,
|
||||
testStatus: connection.testStatus,
|
||||
rateLimitedUntil: connection.rateLimitedUntil ?? null,
|
||||
backoffLevel: connection.backoffLevel ?? null,
|
||||
lastError: connection.lastError ?? null,
|
||||
lastErrorAt: connection.lastErrorAt ?? null,
|
||||
lastErrorType: connection.lastErrorType ?? null,
|
||||
lastErrorSource: connection.lastErrorSource ?? null,
|
||||
errorCode: connection.errorCode ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
BaseExecutor.RETRY_CONFIG.delayMs = 0;
|
||||
await resetStorage();
|
||||
resetAdaptiveAdmissionRuntimeForTests();
|
||||
reloadNormalResourcePressure();
|
||||
// Default process runtime is shadow; leave it unless a test reloads enforce.
|
||||
reloadAdaptiveAdmissionRuntime({
|
||||
config: {
|
||||
mode: "shadow",
|
||||
minLimit: 8,
|
||||
initialLimit: 64,
|
||||
maxLimit: 1000,
|
||||
maxQueueCount: 128,
|
||||
maxQueueCost: 2000,
|
||||
defaultMaxWaitMs: 5_000,
|
||||
windowMs: 1_000,
|
||||
},
|
||||
checkResourcePressure: () => null,
|
||||
});
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
globalThis.fetch = originalFetch;
|
||||
resetAdaptiveAdmissionRuntimeForTests();
|
||||
await resetStorage();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
globalThis.fetch = originalFetch;
|
||||
resetAdaptiveAdmissionRuntimeForTests();
|
||||
await harness.cleanup();
|
||||
});
|
||||
|
||||
test("invalid body early-return creates no admission lease activity", async () => {
|
||||
const before = getAdaptiveAdmissionRuntime().snapshot();
|
||||
const response = await handleChat(
|
||||
new Request("http://localhost/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "{not-json",
|
||||
})
|
||||
);
|
||||
assert.equal(response.status, 400);
|
||||
const after = getAdaptiveAdmissionRuntime().snapshot();
|
||||
assert.equal(after.admittedCount, before.admittedCount);
|
||||
assert.equal(after.activeCount, 0);
|
||||
assert.equal(after.rejectedCount, before.rejectedCount);
|
||||
});
|
||||
|
||||
test("schema-invalid request never acquires an admission lease", async () => {
|
||||
const before = getAdaptiveAdmissionRuntime().snapshot();
|
||||
const response = await handleChat(
|
||||
buildRequest({
|
||||
body: {
|
||||
model: "openai/gpt-4o-mini",
|
||||
messages: "not-an-array",
|
||||
},
|
||||
})
|
||||
);
|
||||
assert.equal(response.status, 400);
|
||||
const after = getAdaptiveAdmissionRuntime().snapshot();
|
||||
assert.equal(after.admittedCount, before.admittedCount);
|
||||
assert.equal(after.activeCount, 0);
|
||||
});
|
||||
|
||||
test("default shadow admits and releases active lease on JSON result", async () => {
|
||||
await seedConnection("openai", { apiKey: "sk-openai-shadow-admit" });
|
||||
|
||||
let fetchCalls = 0;
|
||||
globalThis.fetch = async () => {
|
||||
fetchCalls += 1;
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: "chatcmpl-test",
|
||||
object: "chat.completion",
|
||||
choices: [
|
||||
{ index: 0, message: { role: "assistant", content: "ok" }, finish_reason: "stop" },
|
||||
],
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
};
|
||||
|
||||
const before = getAdaptiveAdmissionRuntime().snapshot();
|
||||
const response = await handleChat(
|
||||
buildRequest({
|
||||
body: {
|
||||
model: "openai/gpt-4o-mini",
|
||||
stream: false,
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
},
|
||||
})
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(fetchCalls, 1);
|
||||
const after = getAdaptiveAdmissionRuntime().snapshot();
|
||||
assert.equal(after.activeCount, 0);
|
||||
assert.equal(after.admittedCount, before.admittedCount + 1);
|
||||
});
|
||||
|
||||
test("shared SSE response holds the lease until consumer cancellation", async () => {
|
||||
await seedConnection("openai", { apiKey: "sk-openai-stream-admit" });
|
||||
|
||||
let fetchCalls = 0;
|
||||
globalThis.fetch = async () => {
|
||||
fetchCalls += 1;
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
`data: ${JSON.stringify({
|
||||
id: "chatcmpl-stream",
|
||||
object: "chat.completion.chunk",
|
||||
choices: [{ index: 0, delta: { content: "hi" }, finish_reason: null }],
|
||||
})}\n\n`
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
return new Response(stream, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
};
|
||||
|
||||
const before = getAdaptiveAdmissionRuntime().snapshot();
|
||||
const response = await handleChat(
|
||||
buildRequest({
|
||||
body: {
|
||||
model: "openai/gpt-4o-mini",
|
||||
stream: true,
|
||||
messages: [{ role: "user", content: "stream" }],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(fetchCalls, 1);
|
||||
assert.equal(getAdaptiveAdmissionRuntime().snapshot().activeCount, 1);
|
||||
assert.equal(getAdaptiveAdmissionRuntime().snapshot().admittedCount, before.admittedCount + 1);
|
||||
|
||||
await response.body!.cancel();
|
||||
assert.equal(getAdaptiveAdmissionRuntime().snapshot().activeCount, 0);
|
||||
});
|
||||
|
||||
function reloadEnforceOversized() {
|
||||
// cost >> limit forces immediate ADMISSION_OVERSIZED (not clamped-to-limit admit).
|
||||
reloadAdaptiveAdmissionRuntime({
|
||||
config: {
|
||||
mode: "enforce",
|
||||
minLimit: 1,
|
||||
initialLimit: 1,
|
||||
maxLimit: 1,
|
||||
maxQueueCount: 1,
|
||||
maxQueueCost: 1,
|
||||
defaultMaxWaitMs: 50,
|
||||
windowMs: 50,
|
||||
cost: {
|
||||
maxRequestCost: 100,
|
||||
baseCost: 1,
|
||||
bodyBytesPerUnit: 1,
|
||||
tokensPerUnit: 1,
|
||||
messagesPerUnit: 1,
|
||||
toolsPerUnit: 1,
|
||||
fanoutPerUnit: 1,
|
||||
streamingClassCost: 1,
|
||||
nonStreamingClassCost: 1,
|
||||
},
|
||||
},
|
||||
checkResourcePressure: () => null,
|
||||
});
|
||||
}
|
||||
|
||||
function oversizedBody(prefix: string) {
|
||||
return {
|
||||
model: "openai/gpt-4o-mini",
|
||||
stream: false,
|
||||
messages: Array.from({ length: 20 }, (_, i) => ({
|
||||
role: "user",
|
||||
content: `${prefix}-${i}-${"x".repeat(64)}`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
test("enforce oversized/queue rejection returns standardized 503 before provider fetch", async () => {
|
||||
await seedConnection("openai", { apiKey: "sk-openai-enforce-reject" });
|
||||
reloadEnforceOversized();
|
||||
|
||||
let fetchCalls = 0;
|
||||
globalThis.fetch = async () => {
|
||||
fetchCalls += 1;
|
||||
return new Response("should-not-run", { status: 200 });
|
||||
};
|
||||
|
||||
const response = await handleChat(buildRequest({ body: oversizedBody("message") }));
|
||||
|
||||
assert.equal(response.status, 503);
|
||||
const payload = await response.json();
|
||||
assert.match(String(payload.error?.code || ""), /^admission_/);
|
||||
assert.equal(fetchCalls, 0);
|
||||
assert.equal(getAdaptiveAdmissionRuntime().snapshot().activeCount, 0);
|
||||
});
|
||||
|
||||
test("lazy client-raw factory is not invoked on admission rejection", async () => {
|
||||
reloadEnforceOversized();
|
||||
|
||||
let factoryCalls = 0;
|
||||
const body = oversizedBody("lazy");
|
||||
const request = buildRequest({ body });
|
||||
|
||||
const response = await handleChat(request, () => {
|
||||
factoryCalls += 1;
|
||||
return buildClientRawRequest(request, body);
|
||||
});
|
||||
|
||||
assert.equal(response.status, 503);
|
||||
assert.equal(factoryCalls, 0);
|
||||
});
|
||||
|
||||
test("lazy client-raw factory is invoked exactly once after admission", async () => {
|
||||
await seedConnection("openai", { apiKey: "sk-openai-lazy-raw" });
|
||||
reloadAdaptiveAdmissionRuntime({
|
||||
config: {
|
||||
mode: "shadow",
|
||||
minLimit: 8,
|
||||
initialLimit: 64,
|
||||
maxLimit: 1000,
|
||||
maxQueueCount: 128,
|
||||
maxQueueCost: 2000,
|
||||
defaultMaxWaitMs: 5_000,
|
||||
windowMs: 1_000,
|
||||
},
|
||||
checkResourcePressure: () => null,
|
||||
});
|
||||
|
||||
let factoryCalls = 0;
|
||||
globalThis.fetch = async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
id: "chatcmpl-lazy",
|
||||
object: "chat.completion",
|
||||
choices: [
|
||||
{ index: 0, message: { role: "assistant", content: "ok" }, finish_reason: "stop" },
|
||||
],
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
|
||||
const body = {
|
||||
model: "openai/gpt-4o-mini",
|
||||
stream: false,
|
||||
messages: [{ role: "user", content: "lazy once" }],
|
||||
};
|
||||
const request = buildRequest({ body });
|
||||
await handleChat(request, () => {
|
||||
factoryCalls += 1;
|
||||
return buildClientRawRequest(request, body);
|
||||
});
|
||||
|
||||
assert.equal(factoryCalls, 1);
|
||||
assert.equal(getAdaptiveAdmissionRuntime().snapshot().activeCount, 0);
|
||||
});
|
||||
|
||||
test(
|
||||
"execution-time resource pressure bypasses provider/account accounting",
|
||||
{ timeout: 2_000 },
|
||||
async () => {
|
||||
const connection = await seedConnection("openai", {
|
||||
name: "pressure-isolation",
|
||||
apiKey: "sk-openai-pressure-isolation",
|
||||
});
|
||||
const connectionId = String(connection.id);
|
||||
const beforeConnection = connectionFailureState(
|
||||
(await getProviderConnectionById(connectionId)) as Record<string, unknown> | null
|
||||
);
|
||||
const breaker = getCircuitBreaker("openai");
|
||||
const beforeBreaker = breaker.getStatus();
|
||||
const beforeSuccessCount = breaker.successCount;
|
||||
|
||||
reloadCriticalResourcePressure();
|
||||
let fetchCalls = 0;
|
||||
globalThis.fetch = async () => {
|
||||
fetchCalls += 1;
|
||||
return new Response("provider must not run", { status: 500 });
|
||||
};
|
||||
|
||||
const response = await handleChat(
|
||||
buildRequest({
|
||||
body: {
|
||||
model: "openai/gpt-4o-mini",
|
||||
messages: [{ role: "user", content: "shed locally" }],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
assert.equal(response.status, 503);
|
||||
assert.equal(response.headers.get("Retry-After"), "5");
|
||||
const payload = await response.json();
|
||||
assert.equal(payload.error.code, "resource_pressure");
|
||||
assert.equal(fetchCalls, 0);
|
||||
assert.deepEqual(
|
||||
connectionFailureState(
|
||||
(await getProviderConnectionById(connectionId)) as Record<string, unknown> | null
|
||||
),
|
||||
beforeConnection
|
||||
);
|
||||
const afterBreaker = breaker.getStatus();
|
||||
assert.equal(afterBreaker.state, beforeBreaker.state);
|
||||
assert.equal(afterBreaker.failureCount, beforeBreaker.failureCount);
|
||||
assert.equal(breaker.successCount, beforeSuccessCount);
|
||||
}
|
||||
);
|
||||
|
||||
test("resource pressure takes precedence over an open provider breaker", async () => {
|
||||
const breaker = getCircuitBreaker("openai");
|
||||
for (let i = 0; i < 20 && breaker.getStatus().state !== STATE.OPEN; i += 1) {
|
||||
breaker._onFailure();
|
||||
}
|
||||
const before = breaker.getStatus();
|
||||
const beforeSuccessCount = breaker.successCount;
|
||||
assert.equal(before.state, STATE.OPEN);
|
||||
|
||||
reloadCriticalResourcePressure();
|
||||
let fetchCalls = 0;
|
||||
globalThis.fetch = async () => {
|
||||
fetchCalls += 1;
|
||||
return new Response("provider must not run", { status: 500 });
|
||||
};
|
||||
|
||||
const response = await handleChat(
|
||||
buildRequest({
|
||||
body: {
|
||||
model: "openai/gpt-4o-mini",
|
||||
messages: [{ role: "user", content: "pressure before breaker" }],
|
||||
},
|
||||
})
|
||||
);
|
||||
assert.equal(response.status, 503);
|
||||
assert.equal((await response.json()).error.code, "resource_pressure");
|
||||
assert.equal(fetchCalls, 0);
|
||||
|
||||
const after = breaker.getStatus();
|
||||
assert.equal(after.state, STATE.OPEN);
|
||||
assert.equal(after.failureCount, before.failureCount);
|
||||
assert.equal(breaker.successCount, beforeSuccessCount);
|
||||
});
|
||||
|
||||
test("local admission rejection does not mutate a supplied provider breaker", async () => {
|
||||
resetAllCircuitBreakers();
|
||||
const breaker = getCircuitBreaker("openai");
|
||||
const before = breaker.getStatus();
|
||||
const beforeSuccessCount = breaker.successCount;
|
||||
assert.equal(before.state, STATE.CLOSED);
|
||||
assert.equal(before.failureCount, 0);
|
||||
|
||||
reloadEnforceOversized();
|
||||
|
||||
let fetchCalls = 0;
|
||||
globalThis.fetch = async () => {
|
||||
fetchCalls += 1;
|
||||
return new Response("nope", { status: 200 });
|
||||
};
|
||||
|
||||
const response = await handleChat(buildRequest({ body: oversizedBody("breaker") }));
|
||||
assert.equal(response.status, 503);
|
||||
assert.equal(fetchCalls, 0);
|
||||
|
||||
const after = breaker.getStatus();
|
||||
assert.equal(after.state, STATE.CLOSED);
|
||||
assert.equal(after.failureCount, before.failureCount);
|
||||
assert.equal(breaker.successCount, beforeSuccessCount);
|
||||
});
|
||||
483
tests/unit/chat-admission-wrapper.test.ts
Normal file
483
tests/unit/chat-admission-wrapper.test.ts
Normal file
@@ -0,0 +1,483 @@
|
||||
/**
|
||||
* Focused unit tests for the shared handleChat adaptive-admission lifecycle wrapper.
|
||||
* No provider/network work — pure wrapper + context seams.
|
||||
*/
|
||||
import { describe, it, beforeEach, afterEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
ANONYMOUS_ADMISSION_TENANT_KEY,
|
||||
captureDeferredClientRawBody,
|
||||
classifyHandlerFailure,
|
||||
createChatAdmissionContext,
|
||||
resolveAdmissionTenantKey,
|
||||
withChatAdmission,
|
||||
type ChatAdmissionContext,
|
||||
} from "../../src/sse/handlers/chatAdmission.ts";
|
||||
import {
|
||||
createAdaptiveAdmissionRuntime,
|
||||
type AdaptiveAdmissionRuntime,
|
||||
} from "../../open-sse/services/admission/runtime.ts";
|
||||
import type { AdaptiveAdmissionConfig } from "../../open-sse/services/admission/types.ts";
|
||||
|
||||
class FakeClock {
|
||||
nowMs = 0;
|
||||
private nextId = 1;
|
||||
private timers = new Map<number, { due: number; fn: () => void }>();
|
||||
|
||||
now = () => this.nowMs;
|
||||
|
||||
setTimer = (fn: () => void, delayMs: number): number => {
|
||||
const id = this.nextId++;
|
||||
this.timers.set(id, { due: this.nowMs + Math.max(0, delayMs), fn });
|
||||
return id;
|
||||
};
|
||||
|
||||
clearTimer = (id: number): void => {
|
||||
this.timers.delete(id);
|
||||
};
|
||||
|
||||
advance(ms: number): void {
|
||||
const target = this.nowMs + ms;
|
||||
while (true) {
|
||||
let nextId: number | undefined;
|
||||
let nextDue = Number.POSITIVE_INFINITY;
|
||||
for (const [id, t] of this.timers) {
|
||||
if (t.due <= target && t.due < nextDue) {
|
||||
nextDue = t.due;
|
||||
nextId = id;
|
||||
}
|
||||
}
|
||||
if (nextId === undefined) {
|
||||
this.nowMs = target;
|
||||
return;
|
||||
}
|
||||
const timer = this.timers.get(nextId)!;
|
||||
this.timers.delete(nextId);
|
||||
this.nowMs = timer.due;
|
||||
timer.fn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function enforceConfig(overrides: Partial<AdaptiveAdmissionConfig> = {}): AdaptiveAdmissionConfig {
|
||||
return {
|
||||
mode: "enforce",
|
||||
minLimit: 1,
|
||||
maxLimit: 4,
|
||||
initialLimit: 1,
|
||||
maxQueueCount: 1,
|
||||
maxQueueCost: 4,
|
||||
defaultMaxWaitMs: 50,
|
||||
windowMs: 50,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeRuntime(
|
||||
clock: FakeClock,
|
||||
config: AdaptiveAdmissionConfig = enforceConfig()
|
||||
): AdaptiveAdmissionRuntime {
|
||||
return createAdaptiveAdmissionRuntime({
|
||||
config,
|
||||
clock: {
|
||||
now: clock.now,
|
||||
setTimer: clock.setTimer,
|
||||
clearTimer: clock.clearTimer,
|
||||
},
|
||||
checkResourcePressure: () => null,
|
||||
getResourcePressureObservation: () => ({
|
||||
signals: null,
|
||||
state: {
|
||||
severity: "normal",
|
||||
reason: "none",
|
||||
elevatedStreak: 0,
|
||||
recoveryStreak: 0,
|
||||
lastTransitionAtMs: 0,
|
||||
observedAtMs: 0,
|
||||
},
|
||||
}),
|
||||
nowMs: clock.now,
|
||||
});
|
||||
}
|
||||
|
||||
describe("resolveAdmissionTenantKey", () => {
|
||||
it("uses only opaque api key id; never falls through to empty/raw", () => {
|
||||
assert.equal(resolveAdmissionTenantKey("key-abc"), "key-abc");
|
||||
assert.equal(resolveAdmissionTenantKey(""), ANONYMOUS_ADMISSION_TENANT_KEY);
|
||||
assert.equal(resolveAdmissionTenantKey(null), ANONYMOUS_ADMISSION_TENANT_KEY);
|
||||
assert.equal(resolveAdmissionTenantKey(undefined), ANONYMOUS_ADMISSION_TENANT_KEY);
|
||||
});
|
||||
});
|
||||
|
||||
describe("captureDeferredClientRawBody", () => {
|
||||
it("captures only fixed mutable fields and restores client-visible values after admission", () => {
|
||||
let enumerations = 0;
|
||||
const target: Record<string, unknown> = {
|
||||
model: "no-think/openai/model",
|
||||
reasoning: { effort: "high" },
|
||||
untouched: "value",
|
||||
};
|
||||
const body = new Proxy(target, {
|
||||
ownKeys() {
|
||||
enumerations += 1;
|
||||
return Reflect.ownKeys(target);
|
||||
},
|
||||
});
|
||||
|
||||
const deferred = captureDeferredClientRawBody(body);
|
||||
assert.equal(enumerations, 0, "pre-admission capture must not enumerate the body");
|
||||
|
||||
body.model = "openai/model";
|
||||
body.reasoning_effort = "none";
|
||||
delete body.reasoning;
|
||||
|
||||
const captured = deferred.withClientBody((clientBody) => ({
|
||||
model: (clientBody as Record<string, unknown>).model,
|
||||
reasoning: (clientBody as Record<string, unknown>).reasoning,
|
||||
hasEffort: Object.hasOwn(clientBody as object, "reasoning_effort"),
|
||||
}));
|
||||
|
||||
assert.deepEqual(captured, {
|
||||
model: "no-think/openai/model",
|
||||
reasoning: { effort: "high" },
|
||||
hasEffort: false,
|
||||
});
|
||||
assert.equal(body.model, "openai/model", "working body must be restored after snapshot build");
|
||||
assert.equal(body.reasoning_effort, "none");
|
||||
assert.equal(Object.hasOwn(body, "reasoning"), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("classifyHandlerFailure", () => {
|
||||
it("classifies abort / timeout / 4xx / else correctly", () => {
|
||||
const aborted = new AbortController();
|
||||
aborted.abort();
|
||||
assert.equal(classifyHandlerFailure(new Error("x"), aborted.signal), "cancelled");
|
||||
|
||||
const abortErr = new Error("aborted");
|
||||
abortErr.name = "AbortError";
|
||||
assert.equal(classifyHandlerFailure(abortErr), "cancelled");
|
||||
|
||||
const timeoutErr = new Error("timed out");
|
||||
timeoutErr.name = "TimeoutError";
|
||||
assert.equal(classifyHandlerFailure(timeoutErr), "timeout");
|
||||
|
||||
assert.equal(classifyHandlerFailure(Object.assign(new Error("t"), { status: 504 })), "timeout");
|
||||
assert.equal(
|
||||
classifyHandlerFailure(Object.assign(new Error("bad"), { status: 400 })),
|
||||
"local_reject"
|
||||
);
|
||||
assert.equal(classifyHandlerFailure(new Error("upstream boom")), "upstream_error");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createChatAdmissionContext", () => {
|
||||
let clock: FakeClock;
|
||||
let runtime: AdaptiveAdmissionRuntime;
|
||||
|
||||
beforeEach(() => {
|
||||
clock = new FakeClock();
|
||||
runtime = makeRuntime(clock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
it("does not acquire when never called", async () => {
|
||||
const ctx = createChatAdmissionContext(() => runtime);
|
||||
assert.equal(ctx.getAdmittedState(), null);
|
||||
assert.equal(runtime.snapshot().activeCount, 0);
|
||||
assert.equal(runtime.snapshot().admittedCount, 0);
|
||||
});
|
||||
|
||||
it("acquires once and rejects a second acquire without re-entering runtime", async () => {
|
||||
// Capacity must clear default feature cost; this case only locks once-semantics.
|
||||
runtime.dispose();
|
||||
runtime = makeRuntime(
|
||||
clock,
|
||||
enforceConfig({
|
||||
initialLimit: 64,
|
||||
minLimit: 8,
|
||||
maxLimit: 100,
|
||||
maxQueueCount: 8,
|
||||
maxQueueCost: 200,
|
||||
})
|
||||
);
|
||||
const ctx = createChatAdmissionContext(() => runtime);
|
||||
const first = await ctx.acquire(
|
||||
"tenant-a",
|
||||
{ signal: undefined },
|
||||
{
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
stream: false,
|
||||
}
|
||||
);
|
||||
assert.equal(first, null);
|
||||
assert.ok(ctx.getAdmittedState());
|
||||
assert.equal(runtime.snapshot().activeCount, 1);
|
||||
|
||||
const second = await ctx.acquire("tenant-b", {}, { messages: [] });
|
||||
assert.equal(second, null);
|
||||
assert.equal(runtime.snapshot().activeCount, 1);
|
||||
assert.equal(runtime.snapshot().admittedCount, 1);
|
||||
|
||||
ctx.getAdmittedState()!.admitted.lease.release("success");
|
||||
});
|
||||
|
||||
it("returns standardized 503 rejection without holding a lease", async () => {
|
||||
const tiny = makeRuntime(
|
||||
clock,
|
||||
enforceConfig({
|
||||
initialLimit: 1,
|
||||
minLimit: 1,
|
||||
maxLimit: 1,
|
||||
maxQueueCount: 1,
|
||||
maxQueueCost: 1,
|
||||
cost: { maxRequestCost: 1, baseCost: 1 },
|
||||
})
|
||||
);
|
||||
const holdCtx = createChatAdmissionContext(() => tiny);
|
||||
assert.equal(await holdCtx.acquire("hold", {}, { messages: [] }), null);
|
||||
|
||||
const rejectCtx = createChatAdmissionContext(() => tiny);
|
||||
const rejectPromise = rejectCtx.acquire(
|
||||
"waiter",
|
||||
{},
|
||||
{ messages: [{ role: "user", content: "x" }] }
|
||||
);
|
||||
clock.advance(50);
|
||||
const rejection = await rejectPromise;
|
||||
assert.ok(rejection);
|
||||
assert.equal(rejection!.status, 503);
|
||||
const body = await rejection!.json();
|
||||
assert.match(String(body.error?.code || ""), /^admission_/);
|
||||
assert.equal(rejectCtx.getAdmittedState(), null);
|
||||
|
||||
holdCtx.getAdmittedState()!.admitted.lease.release("success");
|
||||
tiny.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe("withChatAdmission lifecycle", () => {
|
||||
let clock: FakeClock;
|
||||
let runtime: AdaptiveAdmissionRuntime;
|
||||
|
||||
beforeEach(() => {
|
||||
clock = new FakeClock();
|
||||
runtime = makeRuntime(clock, enforceConfig({ mode: "shadow", initialLimit: 8, maxLimit: 20 }));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
function wrap(
|
||||
impl: (
|
||||
request: unknown,
|
||||
clientRaw: unknown,
|
||||
body: unknown,
|
||||
correlationId: string | undefined,
|
||||
ctx: ChatAdmissionContext
|
||||
) => Promise<Response>
|
||||
) {
|
||||
return withChatAdmission(impl as never, { getRuntime: () => runtime });
|
||||
}
|
||||
|
||||
it("early return before acquire creates no lease / runtime activity", async () => {
|
||||
const handle = wrap(async () => new Response(JSON.stringify({ ok: true }), { status: 400 }));
|
||||
const res = await handle({ signal: undefined }, null, null);
|
||||
assert.equal(res.status, 400);
|
||||
assert.equal(runtime.snapshot().activeCount, 0);
|
||||
assert.equal(runtime.snapshot().admittedCount, 0);
|
||||
});
|
||||
|
||||
it("JSON success releases active lease before return", async () => {
|
||||
const handle = wrap(async (_req, _raw, _body, _id, ctx) => {
|
||||
const rejection = await ctx.acquire("k1", {}, { messages: [], stream: false });
|
||||
assert.equal(rejection, null);
|
||||
assert.equal(runtime.snapshot().activeCount, 1);
|
||||
return new Response(JSON.stringify({ choices: [] }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
});
|
||||
const res = await handle({}, null, null);
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(runtime.snapshot().activeCount, 0);
|
||||
assert.equal(runtime.snapshot().admittedCount, 1);
|
||||
});
|
||||
|
||||
it("SSE keeps lease through open stream and releases once on cancel", async () => {
|
||||
const handle = wrap(async (_req, _raw, _body, _id, ctx) => {
|
||||
const rejection = await ctx.acquire("k-sse", {}, { messages: [], stream: true });
|
||||
assert.equal(rejection, null);
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode("data: hi\n\n"));
|
||||
},
|
||||
});
|
||||
return new Response(stream, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
});
|
||||
const res = await handle({}, null, null);
|
||||
assert.equal(runtime.snapshot().activeCount, 1);
|
||||
await res.body!.cancel();
|
||||
assert.equal(runtime.snapshot().activeCount, 0);
|
||||
});
|
||||
|
||||
it("handler throw after acquisition releases once as upstream_error and rethrows", async () => {
|
||||
const outcomes: string[] = [];
|
||||
const release = runtime.releaseHandlerFailure;
|
||||
runtime.releaseHandlerFailure = (lease, outcome, options) => {
|
||||
outcomes.push(outcome);
|
||||
release.call(runtime, lease, outcome, options);
|
||||
};
|
||||
const handle = wrap(async (_req, _raw, _body, _id, ctx) => {
|
||||
assert.equal(await ctx.acquire("k-err", {}, { messages: [] }), null);
|
||||
throw new Error("provider exploded");
|
||||
});
|
||||
await assert.rejects(() => handle({}, null, null), /provider exploded/);
|
||||
assert.deepEqual(outcomes, ["upstream_error"]);
|
||||
assert.equal(runtime.snapshot().activeCount, 0);
|
||||
});
|
||||
|
||||
it("attach failure releases exactly once before rethrow", async () => {
|
||||
const outcomes: string[] = [];
|
||||
const release = runtime.releaseHandlerFailure;
|
||||
runtime.releaseHandlerFailure = (lease, outcome, options) => {
|
||||
outcomes.push(outcome);
|
||||
release.call(runtime, lease, outcome, options);
|
||||
};
|
||||
runtime.attachResponseLifecycle = () => {
|
||||
throw new Error("attach failed");
|
||||
};
|
||||
|
||||
const handle = wrap(async (_req, _raw, _body, _id, ctx) => {
|
||||
assert.equal(await ctx.acquire("k-attach", {}, { messages: [] }), null);
|
||||
return new Response("ok");
|
||||
});
|
||||
|
||||
await assert.rejects(() => handle({}, null, null), /attach failed/);
|
||||
assert.deepEqual(outcomes, ["upstream_error"]);
|
||||
assert.equal(runtime.snapshot().activeCount, 0);
|
||||
});
|
||||
|
||||
it("timeout-classified throw releases as timeout", async () => {
|
||||
const handle = wrap(async (_req, _raw, _body, _id, ctx) => {
|
||||
assert.equal(await ctx.acquire("k-to", {}, { messages: [] }), null);
|
||||
throw Object.assign(new Error("gateway timeout"), { status: 504 });
|
||||
});
|
||||
await assert.rejects(() => handle({}, null, null), /gateway timeout/);
|
||||
assert.equal(runtime.snapshot().activeCount, 0);
|
||||
});
|
||||
|
||||
it("queue deadline rejection never invokes inner work after rejection", async () => {
|
||||
const tiny = makeRuntime(
|
||||
clock,
|
||||
enforceConfig({
|
||||
initialLimit: 1,
|
||||
minLimit: 1,
|
||||
maxLimit: 1,
|
||||
maxQueueCount: 1,
|
||||
maxQueueCost: 1,
|
||||
defaultMaxWaitMs: 40,
|
||||
cost: { maxRequestCost: 1, baseCost: 1 },
|
||||
})
|
||||
);
|
||||
|
||||
const holdHandle = withChatAdmission(
|
||||
async (_req, _raw, _body, _id, ctx) => {
|
||||
assert.equal(await ctx.acquire("hold", {}, { messages: [] }), null);
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode("data: hold\n\n"));
|
||||
},
|
||||
});
|
||||
return new Response(stream, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
},
|
||||
{ getRuntime: () => tiny }
|
||||
);
|
||||
const holdRes = await holdHandle({}, null, null);
|
||||
assert.equal(tiny.snapshot().activeCount, 1);
|
||||
|
||||
let innerCalls = 0;
|
||||
const rejectHandle = withChatAdmission(
|
||||
async (_req, _raw, _body, _id, ctx) => {
|
||||
const rejection = await ctx.acquire("waiter", {}, { messages: [] });
|
||||
if (rejection) return rejection;
|
||||
innerCalls += 1;
|
||||
return new Response("inner", { status: 200 });
|
||||
},
|
||||
{ getRuntime: () => tiny }
|
||||
);
|
||||
|
||||
const pending = rejectHandle({}, null, null);
|
||||
clock.advance(40);
|
||||
const rejected = await pending;
|
||||
assert.equal(rejected.status, 503);
|
||||
assert.equal(innerCalls, 0);
|
||||
|
||||
await holdRes.body!.cancel();
|
||||
tiny.dispose();
|
||||
});
|
||||
|
||||
it("queued request abort settles without inner provider work", async () => {
|
||||
const tiny = makeRuntime(
|
||||
clock,
|
||||
enforceConfig({
|
||||
initialLimit: 1,
|
||||
minLimit: 1,
|
||||
maxLimit: 1,
|
||||
maxQueueCount: 4,
|
||||
maxQueueCost: 16,
|
||||
defaultMaxWaitMs: 5_000,
|
||||
cost: { maxRequestCost: 1, baseCost: 1 },
|
||||
})
|
||||
);
|
||||
|
||||
const holdHandle = withChatAdmission(
|
||||
async (_req, _raw, _body, _id, ctx) => {
|
||||
assert.equal(await ctx.acquire("hold", {}, { messages: [] }), null);
|
||||
return new Response(
|
||||
new ReadableStream({
|
||||
start(c) {
|
||||
c.enqueue(new TextEncoder().encode("data: h\n\n"));
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "text/event-stream" } }
|
||||
);
|
||||
},
|
||||
{ getRuntime: () => tiny }
|
||||
);
|
||||
const holdRes = await holdHandle({}, null, null);
|
||||
|
||||
let innerCalls = 0;
|
||||
const ac = new AbortController();
|
||||
const waitHandle = withChatAdmission(
|
||||
async (req, _raw, _body, _id, ctx) => {
|
||||
const rejection = await ctx.acquire("waiter", req, { messages: [] });
|
||||
if (rejection) return rejection;
|
||||
innerCalls += 1;
|
||||
return new Response("inner", { status: 200 });
|
||||
},
|
||||
{ getRuntime: () => tiny }
|
||||
);
|
||||
|
||||
const pending = waitHandle({ signal: ac.signal }, null, null);
|
||||
// Allow queue promise to arm, then abort without wall-clock sleep.
|
||||
await Promise.resolve();
|
||||
ac.abort();
|
||||
const rejected = await pending;
|
||||
assert.ok(rejected.status === 499 || rejected.status === 503);
|
||||
assert.equal(innerCalls, 0);
|
||||
|
||||
await holdRes.body!.cancel();
|
||||
tiny.dispose();
|
||||
});
|
||||
});
|
||||
@@ -326,7 +326,7 @@ test("resolveComboTargetTimeoutMs falls back to the saner combo default when uns
|
||||
|
||||
// #7360 / #7301: any strategy with comboCooldownWait enabled waits out cooldowns for up
|
||||
// to comboCooldownWait.budgetMs, so the per-target timeout floor must cover that budget
|
||||
// (DEFAULT_COMBO_TARGET_TIMEOUT_MS alone would cut a long wait short into a 524).
|
||||
// (DEFAULT_COMBO_TARGET_TIMEOUT_MS alone would cut a long wait short into a 504 combo_target_timeout).
|
||||
test("isComboCooldownWaitEligible engages for every strategy when the feature is enabled", () => {
|
||||
for (const strategy of ALL_COMBO_STRATEGIES) {
|
||||
assert.equal(isComboCooldownWaitEligible(strategy, { enabled: true }), true);
|
||||
@@ -359,7 +359,12 @@ test("resolveComboTargetTimeoutMsForCombo raises the floor to cover the cooldown
|
||||
|
||||
// Explicit per-combo targetTimeoutMs still wins over the derived floor.
|
||||
assert.equal(
|
||||
resolveComboTargetTimeoutMsForCombo({ targetTimeoutMs: 45000 }, 600000, "auto", comboCooldownWait),
|
||||
resolveComboTargetTimeoutMsForCombo(
|
||||
{ targetTimeoutMs: 45000 },
|
||||
600000,
|
||||
"auto",
|
||||
comboCooldownWait
|
||||
),
|
||||
45000
|
||||
);
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { buildTargetTimeoutRunner } from "../../open-sse/services/combo/targetTimeoutRunner.ts";
|
||||
import type { ComboLogger, SingleModelTarget } from "../../open-sse/services/combo/types.ts";
|
||||
|
||||
const noopLog = { warn() {}, info() {}, error() {}, debug() {} } as any;
|
||||
const noopLog: ComboLogger = { warn() {}, info() {}, error() {}, debug() {} };
|
||||
|
||||
test("timeout<=0: passthrough direto (sem timer)", async () => {
|
||||
let called = false;
|
||||
@@ -31,21 +32,28 @@ test("timeout<=0: erro do upstream vira errorResponse 502", async () => {
|
||||
assert.equal(res.status, 502);
|
||||
});
|
||||
|
||||
test("excede o limite: aborta e retorna 524 timed out", async () => {
|
||||
test("excede o limite: aborta e retorna 504 combo_target_timeout", async () => {
|
||||
let aborted = false;
|
||||
const runner = buildTargetTimeoutRunner({
|
||||
handleSingleModel: (_b, _m, target) =>
|
||||
new Promise<Response>((resolve) => {
|
||||
// resolve só se abortado (simula um upstream que respeita o signal)
|
||||
const sig = (target as any)?.modelAbortSignal as AbortSignal | undefined;
|
||||
sig?.addEventListener("abort", () => resolve(new Response(null, { status: 599 })));
|
||||
const sig = target?.modelAbortSignal ?? undefined;
|
||||
sig?.addEventListener("abort", () => {
|
||||
aborted = true;
|
||||
resolve(new Response(null, { status: 599 }));
|
||||
});
|
||||
}),
|
||||
comboTargetTimeoutMs: 20,
|
||||
log: noopLog,
|
||||
});
|
||||
const res = await runner({}, "slow-model");
|
||||
assert.equal(res.status, 524);
|
||||
assert.equal(res.status, 504);
|
||||
assert.equal(aborted, true, "per-target timeout must abort the in-flight target");
|
||||
const body = await res.json();
|
||||
assert.match(JSON.stringify(body), /timed out/i);
|
||||
assert.equal(body?.error?.code, "combo_target_timeout");
|
||||
assert.equal(body?.error?.type, "combo_target_timeout");
|
||||
});
|
||||
|
||||
test("sucesso rápido vence a corrida do timeout", async () => {
|
||||
@@ -66,13 +74,14 @@ test("hedge do parent já abortado propaga o abort ao filho", async () => {
|
||||
const runner = buildTargetTimeoutRunner({
|
||||
handleSingleModel: (_b, _m, target) =>
|
||||
new Promise<Response>((resolve) => {
|
||||
const sig = (target as any)?.modelAbortSignal as AbortSignal | undefined;
|
||||
const sig = target?.modelAbortSignal ?? undefined;
|
||||
if (sig?.aborted) sawAbort = true;
|
||||
resolve(new Response("ok"));
|
||||
}),
|
||||
comboTargetTimeoutMs: 1000,
|
||||
log: noopLog,
|
||||
});
|
||||
await runner({}, "m", { modelAbortSignal: parent.signal } as any);
|
||||
const parentTarget: SingleModelTarget = { modelAbortSignal: parent.signal };
|
||||
await runner({}, "m", parentTarget);
|
||||
assert.equal(sawAbort, true);
|
||||
});
|
||||
|
||||
@@ -383,6 +383,46 @@ test("gemini 524 DOES exhaust connection (cloudflare timeout)", () => {
|
||||
assert.equal(s.exhaustedConnections.has("gemini:gemini-key-abc"), true);
|
||||
});
|
||||
|
||||
test("generic upstream 504 without combo_target_timeout still exhausts the connection", () => {
|
||||
const s = sets();
|
||||
applyComboTargetExhaustion(target(), {
|
||||
...baseOpts,
|
||||
result: { status: 504, headers: null },
|
||||
fallbackResult: {},
|
||||
errorText: "Gateway Timeout",
|
||||
structuredError: { code: "gateway_timeout", type: "server_error" },
|
||||
sets: s,
|
||||
});
|
||||
assert.ok(
|
||||
s.exhaustedConnections.has("test-dedup-provider:conn-1"),
|
||||
"genuine upstream 504 must retain connection-level exhaustion"
|
||||
);
|
||||
assert.equal(s.exhaustedProviders.size, 0);
|
||||
});
|
||||
|
||||
test("OmniRoute combo_target_timeout 504 does NOT exhaust connection or provider", () => {
|
||||
const s = sets();
|
||||
const exhausted = applyComboTargetExhaustion(target(), {
|
||||
...baseOpts,
|
||||
result: { status: 504, headers: null },
|
||||
fallbackResult: {},
|
||||
errorText: "Model slow-model timed out",
|
||||
structuredError: { code: "combo_target_timeout", type: "combo_target_timeout" },
|
||||
sets: s,
|
||||
});
|
||||
assert.equal(exhausted, false);
|
||||
assert.equal(
|
||||
s.exhaustedConnections.size,
|
||||
0,
|
||||
"local per-target timeout must not poison exhaustedConnections"
|
||||
);
|
||||
assert.equal(
|
||||
s.exhaustedProviders.size,
|
||||
0,
|
||||
"local per-target timeout must not poison exhaustedProviders"
|
||||
);
|
||||
});
|
||||
|
||||
// #8133/#8137: auth-level failures (401/403) mean THAT connection's credentials are bad.
|
||||
// When the target carries a connectionId, only that connection is marked exhausted — sibling
|
||||
// connections on the same provider must stay eligible (#8137: whole-provider exhaustion wrongly
|
||||
|
||||
292
tests/unit/combo/combo-target-timeout-standards.test.ts
Normal file
292
tests/unit/combo/combo-target-timeout-standards.test.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
/**
|
||||
* Behavioral evidence for Combo per-target timeout standards:
|
||||
* - local timer returns typed 504 `combo_target_timeout` and fails over
|
||||
* - that local timer must NOT record a provider circuit-breaker failure
|
||||
* - a genuine upstream 504 still records breaker failure / connection exhaustion
|
||||
*
|
||||
* Decision seam for the breaker is the same composition handleComboChat uses:
|
||||
* isComboRequestScopedFailure → shouldRecordProviderBreakerFailure(requestScopedFailure)
|
||||
* Exhaustion uses applyComboTargetExhaustion with the same structuredError path.
|
||||
* Orchestration uses public handleComboChat + injected handleSingleModel (not private mocks).
|
||||
*/
|
||||
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-target-timeout-std-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "combo-target-timeout-std-secret";
|
||||
|
||||
const { handleComboChat } = await import("../../../open-sse/services/combo.ts");
|
||||
const { isComboRequestScopedFailure, shouldRecordProviderBreakerFailure } =
|
||||
await import("../../../open-sse/services/combo/comboPredicates.ts");
|
||||
const { applyComboTargetExhaustion } =
|
||||
await import("../../../open-sse/services/combo/targetExhaustion.ts");
|
||||
const { getProviderBreakerState } = await import("../../../open-sse/services/accountFallback.ts");
|
||||
const { resetAllCircuitBreakers } = await import("../../../src/shared/utils/circuitBreaker.ts");
|
||||
|
||||
const noop = () => {};
|
||||
const log = { info: noop, warn: noop, debug: noop, error: noop };
|
||||
|
||||
type Body = Record<string, unknown>;
|
||||
|
||||
function okResponse(content: string) {
|
||||
return new Response(JSON.stringify({ choices: [{ message: { role: "assistant", content } }] }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
function upstreamGatewayTimeoutResponse() {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: "Gateway Timeout",
|
||||
type: "server_error",
|
||||
code: "gateway_timeout",
|
||||
},
|
||||
}),
|
||||
{ status: 504, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
/** Compose the exact breaker decision seam used by handleComboChat's failure branch. */
|
||||
function decideProviderBreakerRecord(args: {
|
||||
status: number;
|
||||
errorText: string;
|
||||
structuredError?: { code?: string; type?: string };
|
||||
sameProviderNext?: boolean;
|
||||
}) {
|
||||
const requestScopedFailure = isComboRequestScopedFailure(
|
||||
args.status,
|
||||
args.errorText,
|
||||
args.structuredError
|
||||
);
|
||||
return {
|
||||
requestScopedFailure,
|
||||
shouldRecord: shouldRecordProviderBreakerFailure({
|
||||
isStreamReadinessFailure: false,
|
||||
status: args.status,
|
||||
sameProviderNext: args.sameProviderNext === true,
|
||||
requestScopedFailure,
|
||||
error: args.errorText,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function resolvedTarget(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
kind: "model" as const,
|
||||
modelStr: "openai/gpt-4o-mini",
|
||||
provider: "openai",
|
||||
providerId: null,
|
||||
connectionId: "conn-1",
|
||||
executionKey: "k",
|
||||
stepId: "s",
|
||||
weight: 1,
|
||||
label: null,
|
||||
...overrides,
|
||||
} as Parameters<typeof applyComboTargetExhaustion>[0];
|
||||
}
|
||||
|
||||
test.beforeEach(() => {
|
||||
resetAllCircuitBreakers();
|
||||
});
|
||||
|
||||
// ── Decision seam: breaker + request-scoped classification ──────────────────
|
||||
|
||||
test("decision seam: typed combo_target_timeout 504 is request-scoped and does not record breaker failure", () => {
|
||||
const decision = decideProviderBreakerRecord({
|
||||
status: 504,
|
||||
errorText: "Model openai/slow timed out",
|
||||
structuredError: { code: "combo_target_timeout", type: "combo_target_timeout" },
|
||||
sameProviderNext: false,
|
||||
});
|
||||
assert.equal(decision.requestScopedFailure, true);
|
||||
assert.equal(
|
||||
decision.shouldRecord,
|
||||
false,
|
||||
"local per-target timer must not trip the provider circuit breaker"
|
||||
);
|
||||
});
|
||||
|
||||
test("decision seam: generic upstream 504 is NOT request-scoped and still records breaker failure", () => {
|
||||
const decision = decideProviderBreakerRecord({
|
||||
status: 504,
|
||||
errorText: "Gateway Timeout",
|
||||
structuredError: { code: "gateway_timeout", type: "server_error" },
|
||||
sameProviderNext: false,
|
||||
});
|
||||
assert.equal(decision.requestScopedFailure, false);
|
||||
assert.equal(
|
||||
decision.shouldRecord,
|
||||
true,
|
||||
"genuine upstream 504 must retain connection-level breaker recording"
|
||||
);
|
||||
});
|
||||
|
||||
test("decision seam: genuine Cloudflare 524 is not request-scoped (exhaustion, not breaker status set)", () => {
|
||||
// Breaker status set is 408/500/502/503/504 (not 524). 524 remains a connection-
|
||||
// exhaustion signal only — it does not go through request-scoped classification.
|
||||
const decision = decideProviderBreakerRecord({
|
||||
status: 524,
|
||||
errorText: "A Timeout Occurred",
|
||||
structuredError: undefined,
|
||||
sameProviderNext: false,
|
||||
});
|
||||
assert.equal(decision.requestScopedFailure, false);
|
||||
assert.equal(
|
||||
decision.shouldRecord,
|
||||
false,
|
||||
"524 is outside PROVIDER_BREAKER_FAILURE_STATUSES (exhaustion-only signal)"
|
||||
);
|
||||
});
|
||||
|
||||
test("exhaustion: typed combo_target_timeout 504 does not poison connection; generic 504 does", () => {
|
||||
const base = {
|
||||
fallbackResult: {},
|
||||
isTokenLimitBreach: false,
|
||||
allAccountsRateLimited: false,
|
||||
log,
|
||||
tag: "COMBO",
|
||||
exhaustedLogLevel: "info" as const,
|
||||
};
|
||||
|
||||
const localSets = {
|
||||
exhaustedProviders: new Set<string>(),
|
||||
exhaustedConnections: new Set<string>(),
|
||||
transientRateLimitedProviders: new Set<string>(),
|
||||
};
|
||||
applyComboTargetExhaustion(resolvedTarget(), {
|
||||
...base,
|
||||
result: { status: 504, headers: null },
|
||||
errorText: "Model openai/slow timed out",
|
||||
rawModel: "gpt-4o-mini",
|
||||
structuredError: { code: "combo_target_timeout", type: "combo_target_timeout" },
|
||||
sets: localSets,
|
||||
});
|
||||
assert.equal(localSets.exhaustedConnections.size, 0);
|
||||
assert.equal(localSets.exhaustedProviders.size, 0);
|
||||
|
||||
const upstreamSets = {
|
||||
exhaustedProviders: new Set<string>(),
|
||||
exhaustedConnections: new Set<string>(),
|
||||
transientRateLimitedProviders: new Set<string>(),
|
||||
};
|
||||
applyComboTargetExhaustion(resolvedTarget(), {
|
||||
...base,
|
||||
result: { status: 504, headers: null },
|
||||
errorText: "Gateway Timeout",
|
||||
rawModel: "gpt-4o-mini",
|
||||
structuredError: { code: "gateway_timeout", type: "server_error" },
|
||||
sets: upstreamSets,
|
||||
});
|
||||
assert.ok(upstreamSets.exhaustedConnections.has("openai:conn-1"));
|
||||
});
|
||||
|
||||
// ── Orchestration: public handleComboChat ───────────────────────────────────
|
||||
|
||||
test("handleComboChat: local per-target timeout aborts first target, fails over, succeeds, no breaker record", async () => {
|
||||
const calls: string[] = [];
|
||||
let firstAborted = false;
|
||||
|
||||
const res = await handleComboChat({
|
||||
body: { messages: [{ role: "user", content: "ping" }] },
|
||||
combo: {
|
||||
name: "timeout-failover-std",
|
||||
strategy: "priority",
|
||||
models: ["openai/slow-model", "claude/backup-model"],
|
||||
config: {
|
||||
maxRetries: 0,
|
||||
retryDelayMs: 0,
|
||||
fallbackDelayMs: 0,
|
||||
targetTimeoutMs: 40,
|
||||
},
|
||||
},
|
||||
handleSingleModel: async (_b: Body, modelStr: string, target) => {
|
||||
calls.push(modelStr);
|
||||
if (modelStr === "openai/slow-model") {
|
||||
return await new Promise<Response>((resolve) => {
|
||||
const sig = target?.modelAbortSignal;
|
||||
const onAbort = () => {
|
||||
firstAborted = true;
|
||||
// Loser branch; timeoutPromise already supplies the typed 504.
|
||||
resolve(new Response(null, { status: 599 }));
|
||||
};
|
||||
if (sig?.aborted) {
|
||||
onAbort();
|
||||
return;
|
||||
}
|
||||
sig?.addEventListener("abort", onAbort, { once: true });
|
||||
});
|
||||
}
|
||||
return okResponse("recovered-after-local-timeout");
|
||||
},
|
||||
isModelAvailable: async () => true,
|
||||
log,
|
||||
settings: null,
|
||||
allCombos: null,
|
||||
});
|
||||
|
||||
assert.equal(res.status, 200, "combo must succeed on the second target after local timeout");
|
||||
assert.deepEqual(calls, ["openai/slow-model", "claude/backup-model"]);
|
||||
assert.equal(firstAborted, true, "first target must be aborted by the per-target timer");
|
||||
|
||||
const body = (await res.json()) as {
|
||||
choices: Array<{ message: { content: string } }>;
|
||||
};
|
||||
assert.equal(body.choices[0].message.content, "recovered-after-local-timeout");
|
||||
|
||||
const breaker = getProviderBreakerState("openai");
|
||||
assert.equal(
|
||||
breaker?.failureCount ?? 0,
|
||||
0,
|
||||
"local combo_target_timeout must not record a provider circuit-breaker failure"
|
||||
);
|
||||
});
|
||||
|
||||
test("handleComboChat: generic upstream 504 fails over but still records provider breaker failure", async () => {
|
||||
const calls: string[] = [];
|
||||
|
||||
const res = await handleComboChat({
|
||||
body: { messages: [{ role: "user", content: "ping" }] },
|
||||
combo: {
|
||||
name: "upstream-504-failover-std",
|
||||
strategy: "priority",
|
||||
models: ["openai/primary", "claude/backup"],
|
||||
config: {
|
||||
maxRetries: 0,
|
||||
retryDelayMs: 0,
|
||||
fallbackDelayMs: 0,
|
||||
// Keep timeout high so this path is pure upstream 504, not the local timer.
|
||||
targetTimeoutMs: 60_000,
|
||||
},
|
||||
},
|
||||
handleSingleModel: async (_b: Body, modelStr: string) => {
|
||||
calls.push(modelStr);
|
||||
if (modelStr === "openai/primary") {
|
||||
return upstreamGatewayTimeoutResponse();
|
||||
}
|
||||
return okResponse("recovered-after-upstream-504");
|
||||
},
|
||||
isModelAvailable: async () => true,
|
||||
log,
|
||||
settings: null,
|
||||
allCombos: null,
|
||||
});
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
assert.deepEqual(calls, ["openai/primary", "claude/backup"]);
|
||||
const body = (await res.json()) as {
|
||||
choices: Array<{ message: { content: string } }>;
|
||||
};
|
||||
assert.equal(body.choices[0].message.content, "recovered-after-upstream-504");
|
||||
|
||||
const breaker = getProviderBreakerState("openai");
|
||||
assert.ok(
|
||||
(breaker?.failureCount ?? 0) >= 1,
|
||||
"genuine upstream 504 must record at least one provider breaker failure"
|
||||
);
|
||||
});
|
||||
193
tests/unit/deepseek-thinking-efforts.test.ts
Normal file
193
tests/unit/deepseek-thinking-efforts.test.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
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-deepseek-efforts-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "deepseek-efforts-test-secret";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const modelsDb = await import("../../src/lib/db/models.ts");
|
||||
const { getModelInfo } = await import("../../src/sse/services/model.ts");
|
||||
const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts");
|
||||
const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
const { sanitizeReasoningEffortForProvider } = await import("../../open-sse/executors/base.ts");
|
||||
|
||||
test.beforeEach(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
v1ModelsCatalog.__resetCatalogBuilderRunsForTest();
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("DeepSeek registry declares the documented per-model thinking efforts", () => {
|
||||
const models = new Map((REGISTRY.deepseek?.models || []).map((model) => [model.id, model]));
|
||||
|
||||
assert.deepEqual(models.get("deepseek-v4-flash")?.supportedThinkingEfforts, [
|
||||
"none",
|
||||
"low",
|
||||
"high",
|
||||
"max",
|
||||
]);
|
||||
assert.deepEqual(models.get("deepseek-v4-pro")?.supportedThinkingEfforts, [
|
||||
"none",
|
||||
"high",
|
||||
"max",
|
||||
]);
|
||||
});
|
||||
|
||||
test("DeepSeek catalog exposes only the declared effort aliases", async () => {
|
||||
await providersDb.createProviderConnection({
|
||||
provider: "deepseek",
|
||||
authType: "apikey",
|
||||
name: "deepseek-efforts",
|
||||
apiKey: "deepseek-test-key",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
});
|
||||
|
||||
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
|
||||
new Request("http://localhost/api/v1/models")
|
||||
);
|
||||
const body = (await response.json()) as { data: Array<{ id: string }> };
|
||||
const ids = new Set(body.data.map((model) => model.id));
|
||||
|
||||
assert.ok([...ids].some((id) => id.endsWith("deepseek-v4-flash-none")));
|
||||
assert.ok([...ids].some((id) => id.endsWith("deepseek-v4-flash-low")));
|
||||
assert.ok([...ids].some((id) => id.endsWith("deepseek-v4-flash-high")));
|
||||
assert.ok([...ids].some((id) => id.endsWith("deepseek-v4-flash-max")));
|
||||
assert.ok([...ids].some((id) => id.endsWith("deepseek-v4-pro-none")));
|
||||
assert.ok([...ids].some((id) => id.endsWith("deepseek-v4-pro-high")));
|
||||
assert.ok([...ids].some((id) => id.endsWith("deepseek-v4-pro-max")));
|
||||
assert.equal(
|
||||
[...ids].some((id) => id.endsWith("deepseek-v4-pro-low")),
|
||||
false,
|
||||
"Pro does not advertise low"
|
||||
);
|
||||
});
|
||||
|
||||
test("hardcoded DeepSeek effort suffixes resolve through the static registry", async () => {
|
||||
const flashLow = await getModelInfo("ds/deepseek-v4-flash-low");
|
||||
assert.equal(flashLow.provider, "deepseek");
|
||||
assert.equal(flashLow.model, "deepseek-v4-flash");
|
||||
assert.equal(flashLow.resolvedThinkingEffort, "low");
|
||||
|
||||
const flashNone = await getModelInfo("deepseek/deepseek-v4-flash-none");
|
||||
assert.equal(flashNone.model, "deepseek-v4-flash");
|
||||
assert.equal(flashNone.resolvedThinkingEffort, "none");
|
||||
|
||||
const unsupportedProLow = await getModelInfo("ds/deepseek-v4-pro-low");
|
||||
assert.equal(unsupportedProLow.model, "deepseek-v4-pro-low");
|
||||
assert.equal(unsupportedProLow.resolvedThinkingEffort, undefined);
|
||||
});
|
||||
|
||||
test("native DeepSeek preserves Flash low while clamping unsupported Pro low", () => {
|
||||
const flash = sanitizeReasoningEffortForProvider(
|
||||
{ model: "deepseek-v4-flash", reasoning_effort: "low" },
|
||||
"deepseek",
|
||||
"deepseek-v4-flash"
|
||||
) as Record<string, unknown>;
|
||||
assert.equal(flash.reasoning_effort, "low");
|
||||
|
||||
const pro = sanitizeReasoningEffortForProvider(
|
||||
{ model: "deepseek-v4-pro", reasoning_effort: "low" },
|
||||
"deepseek",
|
||||
"deepseek-v4-pro"
|
||||
) as Record<string, unknown>;
|
||||
assert.equal(pro.reasoning_effort, "high");
|
||||
});
|
||||
|
||||
|
||||
test("non-DeepSeek static reasoning models do not advertise unresolvable effort aliases", async () => {
|
||||
// cheaperinference declares deepseek-v4-flash/pro with supportsReasoning: true
|
||||
// but no supportedThinkingEfforts — the catalog must NOT synthesize
|
||||
// cheaperinference/deepseek-v4-flash-{low,high,...} ids for them (#9485 review #1).
|
||||
await providersDb.createProviderConnection({
|
||||
provider: "cheaperinference",
|
||||
authType: "apikey",
|
||||
name: "cheaperinference-blast-radius",
|
||||
apiKey: "cheaperinference-test-key",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
});
|
||||
|
||||
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
|
||||
new Request("http://localhost/api/v1/models")
|
||||
);
|
||||
const body = (await response.json()) as { data: Array<{ id: string }> };
|
||||
const ids = body.data.map((model) => model.id);
|
||||
|
||||
// Static base models for cheaperinference should still be present
|
||||
assert.ok(
|
||||
ids.some((id) => id.endsWith("cheaperinference/deepseek-v4-flash")),
|
||||
"cheaperinference/deepseek-v4-flash base entry should still be present"
|
||||
);
|
||||
// But NO effort-suffixed aliases should be synthesized
|
||||
assert.equal(
|
||||
ids.some((id) => /cheaperinference\/deepseek-v4-flash-(none|low|medium|high|max|xhigh)$/.test(id)),
|
||||
false,
|
||||
"cheaperinference static reasoning models must not advertise unresolvable effort aliases"
|
||||
);
|
||||
assert.equal(
|
||||
ids.some((id) => /cheaperinference\/deepseek-v4-pro-(none|low|medium|high|max|xhigh)$/.test(id)),
|
||||
false,
|
||||
"cheaperinference static reasoning models must not advertise unresolvable effort aliases"
|
||||
);
|
||||
});
|
||||
|
||||
test("custom model named deepseek-v4-flash-low is not rewritten by registry suffix resolution", async () => {
|
||||
// A custom (DB) model literally named deepseek-v4-flash-low on the deepseek
|
||||
// provider must not be silently rewritten to deepseek-v4-flash + effort low,
|
||||
// which would drop its custom apiFormat/targetFormat metadata (#9485 review #3).
|
||||
await modelsDb.addCustomModel(
|
||||
"deepseek",
|
||||
"deepseek-v4-flash-low",
|
||||
"deepseek-v4-flash-low",
|
||||
"manual",
|
||||
"responses",
|
||||
["chat"],
|
||||
"responses"
|
||||
);
|
||||
|
||||
const info = await getModelInfo("ds/deepseek-v4-flash-low");
|
||||
// The model id should be preserved as the literal custom id, not rewritten
|
||||
assert.equal(info.model, "deepseek-v4-flash-low");
|
||||
// The custom apiFormat must survive (not dropped by registry rewriting)
|
||||
assert.equal(info.apiFormat, "responses");
|
||||
// No resolved effort should be injected — this is a distinct custom model
|
||||
assert.equal(info.resolvedThinkingEffort, undefined);
|
||||
});
|
||||
|
||||
test("none effort resolves and passes through the native DeepSeek sanitizer unchanged", async () => {
|
||||
// The -none suffix resolves to base + effort "none", which reaches the native
|
||||
// DeepSeek endpoint as reasoning_effort: "none" unchanged (#9485 review #8).
|
||||
const flashNone = await getModelInfo("ds/deepseek-v4-flash-none");
|
||||
assert.equal(flashNone.model, "deepseek-v4-flash");
|
||||
assert.equal(flashNone.resolvedThinkingEffort, "none");
|
||||
|
||||
const sanitized = sanitizeReasoningEffortForProvider(
|
||||
{ model: "deepseek-v4-flash", reasoning_effort: "none" },
|
||||
"deepseek",
|
||||
"deepseek-v4-flash"
|
||||
) as Record<string, unknown>;
|
||||
assert.equal(sanitized.reasoning_effort, "none");
|
||||
});
|
||||
|
||||
test("isFlash check is robust to suffixed model ids", () => {
|
||||
// A suffixed id like deepseek-v4-flash-low must still be recognized as Flash
|
||||
// so its low effort is preserved, not clamped to high (#9485 review #5).
|
||||
const sanitizedSuffixed = sanitizeReasoningEffortForProvider(
|
||||
{ model: "deepseek-v4-flash-low", reasoning_effort: "low" },
|
||||
"deepseek",
|
||||
"deepseek-v4-flash-low"
|
||||
) as Record<string, unknown>;
|
||||
assert.equal(sanitizedSuffixed.reasoning_effort, "low");
|
||||
});
|
||||
@@ -1,9 +1,12 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { estimateSizeFast, isSmallEnoughForSemanticCache } = await import(
|
||||
"../../open-sse/utils/estimateSize.ts"
|
||||
);
|
||||
const {
|
||||
estimateSizeFast,
|
||||
isSmallEnoughForSemanticCache,
|
||||
ESTIMATE_SIZE_BYTE_LIMIT,
|
||||
ESTIMATE_SIZE_NODE_BUDGET,
|
||||
} = await import("../../open-sse/utils/estimateSize.ts");
|
||||
|
||||
test("estimateSizeFast returns 0 for null/undefined", () => {
|
||||
assert.equal(estimateSizeFast(null), 0);
|
||||
@@ -65,6 +68,22 @@ test("estimateSizeFast early-exits at 262144 bytes (256KB)", () => {
|
||||
assert.ok(result >= 262144, `Should early-exit, got ${result}`);
|
||||
});
|
||||
|
||||
test("estimateSizeFast checks byte limit after numbers and booleans", () => {
|
||||
const almostForNumber = "x".repeat(ESTIMATE_SIZE_BYTE_LIMIT - 4);
|
||||
const withNumber = estimateSizeFast([almostForNumber, 1]);
|
||||
assert.ok(
|
||||
withNumber > ESTIMATE_SIZE_BYTE_LIMIT,
|
||||
`number contribution must trip byte limit, got ${withNumber}`
|
||||
);
|
||||
// boolean is 4 bytes: start 3 under the limit so adding true exceeds (not merely equals).
|
||||
const almostForBool = "x".repeat(ESTIMATE_SIZE_BYTE_LIMIT - 3);
|
||||
const withBool = estimateSizeFast([almostForBool, true]);
|
||||
assert.ok(
|
||||
withBool > ESTIMATE_SIZE_BYTE_LIMIT,
|
||||
`boolean contribution must trip byte limit, got ${withBool}`
|
||||
);
|
||||
});
|
||||
|
||||
test("estimateSizeFast handles mixed object/array nesting", () => {
|
||||
const data = {
|
||||
choices: [
|
||||
@@ -109,3 +128,70 @@ test("estimateSizeFast handles Map-like objects (no infinite loop on iterables)"
|
||||
const result = estimateSizeFast(map);
|
||||
assert.ok(typeof result === "number");
|
||||
});
|
||||
|
||||
/**
|
||||
* Mutation-sensitive bound: a huge logical length with null/empty-object elements
|
||||
* must not pre-touch every index or allocate all references. Node-budget exhaustion
|
||||
* fails closed above 256 KiB so semantic-cache/admission never treat it as small.
|
||||
*/
|
||||
test("estimateSizeFast node budget fails closed on huge sparse null array without full traversal", () => {
|
||||
let elementAccesses = 0;
|
||||
const sparseNulls = new Proxy([] as unknown[], {
|
||||
get(target, prop, receiver) {
|
||||
if (prop === "length") return 5_000_000;
|
||||
if (prop === Symbol.iterator) {
|
||||
throw new Error("iterator must not be used");
|
||||
}
|
||||
if (typeof prop === "string" && /^[0-9]+$/.test(prop)) {
|
||||
elementAccesses += 1;
|
||||
return null;
|
||||
}
|
||||
return Reflect.get(target, prop, receiver);
|
||||
},
|
||||
});
|
||||
|
||||
const result = estimateSizeFast(sparseNulls);
|
||||
assert.ok(
|
||||
result > ESTIMATE_SIZE_BYTE_LIMIT,
|
||||
`node-budget exhaustion must return >256KiB, got ${result}`
|
||||
);
|
||||
assert.ok(
|
||||
elementAccesses <= ESTIMATE_SIZE_NODE_BUDGET + 8,
|
||||
`must not access far beyond node budget; accesses=${elementAccesses}`
|
||||
);
|
||||
assert.ok(elementAccesses > 100, `expected many bounded visits, got ${elementAccesses}`);
|
||||
assert.equal(isSmallEnoughForSemanticCache(sparseNulls), false);
|
||||
});
|
||||
|
||||
test("estimateSizeFast node budget fails closed on empty-object / getter proxy array", () => {
|
||||
let elementAccesses = 0;
|
||||
let farGetterHits = 0;
|
||||
const emptyObjectArray = new Proxy([] as unknown[], {
|
||||
get(target, prop, receiver) {
|
||||
if (prop === "length") return 2_000_000;
|
||||
if (typeof prop === "string" && /^[0-9]+$/.test(prop)) {
|
||||
const index = Number(prop);
|
||||
elementAccesses += 1;
|
||||
if (index >= ESTIMATE_SIZE_NODE_BUDGET) {
|
||||
farGetterHits += 1;
|
||||
}
|
||||
// Fresh empty object per access — old impl would stack-push every reference.
|
||||
return {};
|
||||
}
|
||||
return Reflect.get(target, prop, receiver);
|
||||
},
|
||||
});
|
||||
|
||||
const result = estimateSizeFast(emptyObjectArray);
|
||||
assert.ok(result > ESTIMATE_SIZE_BYTE_LIMIT, `expected fail-closed, got ${result}`);
|
||||
assert.ok(
|
||||
elementAccesses <= ESTIMATE_SIZE_NODE_BUDGET + 8,
|
||||
`accesses must stay near node budget; got ${elementAccesses}`
|
||||
);
|
||||
assert.equal(
|
||||
farGetterHits,
|
||||
0,
|
||||
`entries beyond the node budget must not be touched; far hits=${farGetterHits}`
|
||||
);
|
||||
assert.equal(isSmallEnoughForSemanticCache(emptyObjectArray), false);
|
||||
});
|
||||
|
||||
246
tests/unit/execute-chat-resource-pressure-breaker.test.ts
Normal file
246
tests/unit/execute-chat-resource-pressure-breaker.test.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* Resource-pressure isolation: executeChatWithBreaker must shed BEFORE the
|
||||
* provider breaker path and must not call handleChatCore on pressure 503.
|
||||
* Direct handleChatCore retains default guard protection.
|
||||
*/
|
||||
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-pressure-breaker-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const { executeChatWithBreaker } = await import("../../src/sse/handlers/chatHelpers.ts");
|
||||
const { getCircuitBreaker, resetAllCircuitBreakers, STATE } =
|
||||
await import("../../src/shared/utils/circuitBreaker.ts");
|
||||
const { reloadResourcePressureRuntime, checkResourcePressureGuard } =
|
||||
await import("../../open-sse/utils/resourcePressure.ts");
|
||||
const { handleChatCore } = await import("../../open-sse/handlers/chatCore.ts");
|
||||
|
||||
const MiB = 1024 ** 2;
|
||||
|
||||
async function resetStorage() {
|
||||
resetAllCircuitBreakers();
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
// Restore a non-shedding resource pressure runtime between tests.
|
||||
reloadResourcePressureRuntime({
|
||||
heapThresholdMb: 10_000,
|
||||
immediateHeapUsedMb: () => 1,
|
||||
sample: async () => ({
|
||||
observedAtMs: Date.now(),
|
||||
v8: { heapUsedBytes: MiB, heapLimitBytes: 10_000 * MiB },
|
||||
process: {
|
||||
rssBytes: MiB,
|
||||
externalBytes: 0,
|
||||
arrayBuffersBytes: 0,
|
||||
availableBytes: null,
|
||||
constrainedBytes: null,
|
||||
},
|
||||
cgroup: { currentBytes: null, maxBytes: null, highBytes: null, events: null },
|
||||
psi: null,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await resetStorage();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
await resetStorage();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("executeChatWithBreaker returns typed pressure 503 before normal, bypass, and shadow breaker paths", async () => {
|
||||
reloadResourcePressureRuntime({
|
||||
heapThresholdMb: 100,
|
||||
immediateHeapUsedMb: () => 999,
|
||||
sample: async () => {
|
||||
throw new Error("sampler must not run on request path");
|
||||
},
|
||||
});
|
||||
|
||||
// Sanity: process singleton sheds.
|
||||
const direct = checkResourcePressureGuard();
|
||||
assert.ok(direct);
|
||||
assert.equal(direct!.status, 503);
|
||||
|
||||
const breaker = getCircuitBreaker("openai-pressure-iso");
|
||||
const before = breaker.getStatus();
|
||||
const beforeSuccessCount = breaker.successCount;
|
||||
assert.equal(before.state, STATE.CLOSED);
|
||||
assert.equal(before.failureCount, 0);
|
||||
|
||||
// If handleChatCore were entered it would attempt real provider work / DB.
|
||||
// Use credentials that would fail loudly if chatCore ran deep.
|
||||
const credentials = {
|
||||
connectionId: "conn_pressure_iso",
|
||||
apiKey: "sk-pressure-iso",
|
||||
providerSpecificData: {},
|
||||
};
|
||||
|
||||
let canExecuteCalls = 0;
|
||||
const originalCanExecute = breaker.canExecute.bind(breaker);
|
||||
breaker.canExecute = () => {
|
||||
canExecuteCalls += 1;
|
||||
return originalCanExecute();
|
||||
};
|
||||
|
||||
const baseExecution = {
|
||||
bypassCircuitBreaker: false,
|
||||
breaker,
|
||||
body: { model: "openai/gpt-4o-mini", messages: [{ role: "user", content: "x" }] },
|
||||
provider: "openai",
|
||||
model: "gpt-4o-mini",
|
||||
refreshedCredentials: credentials,
|
||||
proxyInfo: null,
|
||||
log: console,
|
||||
clientRawRequest: { endpoint: "/v1/chat/completions", headers: {}, body: {} },
|
||||
credentials,
|
||||
apiKeyInfo: null,
|
||||
userAgent: "",
|
||||
comboName: null,
|
||||
comboStrategy: null,
|
||||
isCombo: false,
|
||||
extendedContext: false,
|
||||
comboStepId: null,
|
||||
comboExecutionKey: null,
|
||||
};
|
||||
const run = (
|
||||
overrides: {
|
||||
bypassCircuitBreaker?: boolean;
|
||||
trafficType?: "production" | "shadow";
|
||||
} = {}
|
||||
) => executeChatWithBreaker({ ...baseExecution, ...overrides });
|
||||
|
||||
const executions = await Promise.all([
|
||||
run(),
|
||||
run({ bypassCircuitBreaker: true }),
|
||||
run({ trafficType: "shadow" }),
|
||||
]);
|
||||
const pressureResponses: Response[] = [];
|
||||
for (const execution of executions) {
|
||||
assert.equal(execution.tlsFingerprintUsed, false);
|
||||
if (!("localResourcePressureResult" in execution)) {
|
||||
assert.fail("provider execution result escaped the local pressure guard");
|
||||
}
|
||||
assert.equal(execution.localResourcePressureResult.response.status, 503);
|
||||
pressureResponses.push(execution.localResourcePressureResult.response);
|
||||
}
|
||||
const payload = await pressureResponses[0].json();
|
||||
assert.equal(payload.error.code, "resource_pressure");
|
||||
assert.match(payload.error.message, /resource pressure/i);
|
||||
|
||||
const after = breaker.getStatus();
|
||||
assert.equal(canExecuteCalls, 0);
|
||||
assert.equal(after.state, STATE.CLOSED);
|
||||
assert.equal(after.failureCount, before.failureCount);
|
||||
assert.equal(breaker.successCount, beforeSuccessCount);
|
||||
});
|
||||
|
||||
test("direct handleChatCore default still applies resource pressure guard", async () => {
|
||||
reloadResourcePressureRuntime({
|
||||
heapThresholdMb: 100,
|
||||
immediateHeapUsedMb: () => 500,
|
||||
sample: async () => ({
|
||||
observedAtMs: Date.now(),
|
||||
v8: { heapUsedBytes: 500 * MiB, heapLimitBytes: 1000 * MiB },
|
||||
process: {
|
||||
rssBytes: 200 * MiB,
|
||||
externalBytes: 0,
|
||||
arrayBuffersBytes: 0,
|
||||
availableBytes: null,
|
||||
constrainedBytes: null,
|
||||
},
|
||||
cgroup: { currentBytes: null, maxBytes: null, highBytes: null, events: null },
|
||||
psi: null,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await (
|
||||
handleChatCore as unknown as (opts: Record<string, unknown>) => Promise<{
|
||||
success?: boolean;
|
||||
status?: number;
|
||||
error?: string;
|
||||
response?: Response;
|
||||
}>
|
||||
)({
|
||||
body: { model: "openai/gpt-4o-mini", messages: [{ role: "user", content: "hi" }] },
|
||||
modelInfo: { provider: "openai", model: "gpt-4o-mini" },
|
||||
credentials: { connectionId: "c1", apiKey: "sk-x", providerSpecificData: {} },
|
||||
log: console,
|
||||
clientRawRequest: { endpoint: "/v1/chat/completions", headers: new Map() },
|
||||
});
|
||||
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 503);
|
||||
assert.ok(result.response);
|
||||
const payload = await result.response.json();
|
||||
assert.equal(payload.error.code, "resource_pressure");
|
||||
});
|
||||
|
||||
test("handleChatCore skipResourcePressureGuard bypasses the inside-core fuse", async () => {
|
||||
reloadResourcePressureRuntime({
|
||||
heapThresholdMb: 100,
|
||||
immediateHeapUsedMb: () => 500,
|
||||
sample: async () => ({
|
||||
observedAtMs: Date.now(),
|
||||
v8: { heapUsedBytes: 500 * MiB, heapLimitBytes: 1000 * MiB },
|
||||
process: {
|
||||
rssBytes: 200 * MiB,
|
||||
externalBytes: 0,
|
||||
arrayBuffersBytes: 0,
|
||||
availableBytes: null,
|
||||
constrainedBytes: null,
|
||||
},
|
||||
cgroup: { currentBytes: null, maxBytes: null, highBytes: null, events: null },
|
||||
psi: null,
|
||||
}),
|
||||
});
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
let fetchCalls = 0;
|
||||
globalThis.fetch = async () => {
|
||||
fetchCalls += 1;
|
||||
return new Response(JSON.stringify({ error: { message: "upstream" } }), { status: 502 });
|
||||
};
|
||||
|
||||
try {
|
||||
// With skip=true the pressure fuse is not applied; chatCore proceeds and hits fetch.
|
||||
const result = await (
|
||||
handleChatCore as unknown as (opts: Record<string, unknown>) => Promise<{
|
||||
success?: boolean;
|
||||
status?: number;
|
||||
error?: string;
|
||||
response?: Response;
|
||||
}>
|
||||
)({
|
||||
body: { model: "openai/gpt-4o-mini", messages: [{ role: "user", content: "hi" }] },
|
||||
modelInfo: { provider: "openai", model: "gpt-4o-mini" },
|
||||
credentials: { connectionId: "c1", apiKey: "sk-x", providerSpecificData: {} },
|
||||
log: console,
|
||||
clientRawRequest: { endpoint: "/v1/chat/completions", headers: new Map() },
|
||||
skipResourcePressureGuard: true,
|
||||
});
|
||||
|
||||
assert.ok(fetchCalls > 0, "skip must let chatCore reach provider work");
|
||||
// Must NOT be the resource_pressure 503 from the fuse.
|
||||
if (result?.response) {
|
||||
try {
|
||||
const payload = await result.response.clone().json();
|
||||
assert.notEqual(payload?.error?.code, "resource_pressure");
|
||||
} catch {
|
||||
// non-JSON is fine — means we left the pressure fuse path
|
||||
}
|
||||
} else if (result?.status === 503) {
|
||||
assert.notEqual(result?.error, "resource_pressure");
|
||||
}
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
@@ -47,7 +47,7 @@ describe("GeminiWebExecutor — testConnection", () => {
|
||||
"@omniroute/open-sse/executors/gemini-web.ts"
|
||||
);
|
||||
const executor = new GeminiWebExecutor();
|
||||
assert.equal(typeof (executor as any).testConnection, "function");
|
||||
assert.equal(typeof executor.testConnection, "function");
|
||||
});
|
||||
|
||||
it("returns false for empty credentials", async () => {
|
||||
|
||||
27
tests/unit/management-auth-docs.test.ts
Normal file
27
tests/unit/management-auth-docs.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { describe, it } from "node:test";
|
||||
import { ok } from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
describe("Management auth documentation (#7786)", () => {
|
||||
const docPath = "docs/guides/MANAGEMENT-AUTH.md";
|
||||
const content = readFileSync(docPath, "utf-8");
|
||||
|
||||
it("exists and has content", () => {
|
||||
ok(content.length > 500, "should have substantial content");
|
||||
ok(content.includes("Dashboard JWT session"));
|
||||
ok(content.includes("CLI machine-id token"));
|
||||
ok(content.includes("oma_"));
|
||||
});
|
||||
|
||||
it("documents all four credential families", () => {
|
||||
const families = ["Dashboard JWT", "CLI machine-id", "oma_", "Manage-scope"];
|
||||
for (const f of families) {
|
||||
ok(content.includes(f), `should document ${f}`);
|
||||
}
|
||||
});
|
||||
|
||||
it("mentions relevant auth header examples", () => {
|
||||
ok(content.includes("Authorization"));
|
||||
ok(content.includes("Bearer"));
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
buildHealthPayload,
|
||||
buildSessionsSummary,
|
||||
buildTelemetryPayload,
|
||||
projectAdaptiveAdmissionSummary,
|
||||
} from "../../src/lib/monitoring/observability.ts";
|
||||
|
||||
test("buildSessionsSummary returns sticky counts and ordered top sessions", () => {
|
||||
@@ -162,4 +163,113 @@ test("buildHealthPayload keeps legacy aliases and adds session/quota observabili
|
||||
assert.equal(payload.quotaMonitor.active, 1);
|
||||
assert.equal(payload.quotaMonitor.monitors[0].provider, "codex");
|
||||
assert.equal(payload.setupComplete, true);
|
||||
assert.equal(payload.adaptiveAdmission, null);
|
||||
});
|
||||
|
||||
test("buildHealthPayload projects allowlisted adaptiveAdmission aggregates only", () => {
|
||||
const snapshot = {
|
||||
mode: "enforce",
|
||||
currentLimit: 4,
|
||||
minLimit: 1,
|
||||
maxLimit: 8,
|
||||
activeCost: 2,
|
||||
activeCount: 1,
|
||||
queuedCost: 3,
|
||||
queuedCount: 1,
|
||||
virtualActiveCost: 99,
|
||||
virtualActiveCount: 99,
|
||||
virtualQueuedCost: 99,
|
||||
virtualQueuedCount: 99,
|
||||
admittedCount: 10,
|
||||
rejectedCount: 2,
|
||||
wouldAdmitCount: 7,
|
||||
wouldQueueCount: 1,
|
||||
wouldRejectCount: 3,
|
||||
shortLatencyEwma: 12.5,
|
||||
longLatencyEwma: 40.1,
|
||||
utilization: 0.42,
|
||||
pressure: "high",
|
||||
resourceSeverity: "normal",
|
||||
resourceReason: "none",
|
||||
resourceObservedAtMs: 1_700_000_000_000,
|
||||
pressureGuardRejectCount: 5,
|
||||
shutdown: false,
|
||||
// Malicious / high-card sentinels that must never appear in the public payload.
|
||||
tenantId: "tenant-SECRET-should-not-leak",
|
||||
apiKey: "sk-live-SHOULD-NOT-LEAK",
|
||||
model: "openai/gpt-secret-model",
|
||||
sessionId: "sess-secret",
|
||||
requestId: "req-secret",
|
||||
body: { messages: [{ role: "user", content: "PII-body-secret" }] },
|
||||
queueItems: [{ tenantKey: "t-secret", cost: 9 }],
|
||||
resourcePath: "/sys/fs/cgroup/memory.current",
|
||||
} as unknown as import("../../open-sse/services/admission/runtime.ts").AdaptiveAdmissionPublicSnapshot;
|
||||
|
||||
const payload = buildHealthPayload({
|
||||
appVersion: "9.9.9",
|
||||
settings: { setupComplete: false },
|
||||
connections: [],
|
||||
circuitBreakers: [],
|
||||
rateLimitStatus: {},
|
||||
learnedLimits: {},
|
||||
lockouts: {},
|
||||
localProviders: {},
|
||||
inflightRequests: 0,
|
||||
quotaMonitorSummary: {
|
||||
active: 0,
|
||||
alerting: 0,
|
||||
exhausted: 0,
|
||||
errors: 0,
|
||||
statusCounts: {
|
||||
starting: 0,
|
||||
idle: 0,
|
||||
healthy: 0,
|
||||
warning: 0,
|
||||
exhausted: 0,
|
||||
error: 0,
|
||||
},
|
||||
byProvider: {},
|
||||
},
|
||||
quotaMonitorMonitors: [],
|
||||
activeSessions: [],
|
||||
adaptiveAdmission: snapshot,
|
||||
});
|
||||
|
||||
assert.deepEqual(payload.adaptiveAdmission, {
|
||||
mode: "enforce",
|
||||
currentLimit: 4,
|
||||
minLimit: 1,
|
||||
maxLimit: 8,
|
||||
activeCost: 2,
|
||||
activeCount: 1,
|
||||
queuedCost: 3,
|
||||
queuedCount: 1,
|
||||
admittedCount: 10,
|
||||
rejectedCount: 2,
|
||||
wouldAdmitCount: 7,
|
||||
wouldQueueCount: 1,
|
||||
wouldRejectCount: 3,
|
||||
utilization: 0.42,
|
||||
pressure: "high",
|
||||
resourceSeverity: "normal",
|
||||
resourceReason: "none",
|
||||
resourceObservedAtMs: 1_700_000_000_000,
|
||||
pressureGuardRejectCount: 5,
|
||||
shutdown: false,
|
||||
});
|
||||
|
||||
const json = JSON.stringify(payload);
|
||||
assert.equal(json.includes("tenant-SECRET"), false);
|
||||
assert.equal(json.includes("sk-live-SHOULD-NOT-LEAK"), false);
|
||||
assert.equal(json.includes("gpt-secret-model"), false);
|
||||
assert.equal(json.includes("PII-body-secret"), false);
|
||||
assert.equal(json.includes("t-secret"), false);
|
||||
assert.equal(json.includes("memory.current"), false);
|
||||
assert.equal(json.includes("queueItems"), false);
|
||||
assert.equal(json.includes("virtualActiveCost"), false);
|
||||
assert.equal(json.includes("shortLatencyEwma"), false);
|
||||
|
||||
// Direct projector also null-safe.
|
||||
assert.equal(projectAdaptiveAdmissionSummary(null), null);
|
||||
assert.equal(projectAdaptiveAdmissionSummary(undefined), null);
|
||||
});
|
||||
|
||||
@@ -173,6 +173,50 @@ test("transformToOllama prefers reasoning_content without duplicating aliases",
|
||||
);
|
||||
});
|
||||
|
||||
test("transformToOllama passes through non-ok shared responses without rewriting status or body", async () => {
|
||||
const errorBody = {
|
||||
error: {
|
||||
message: "Request too large for current capacity",
|
||||
type: "server_error",
|
||||
code: "admission_oversized",
|
||||
},
|
||||
};
|
||||
const upstream = new Response(JSON.stringify(errorBody), {
|
||||
status: 503,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Retry-After": "1",
|
||||
},
|
||||
});
|
||||
|
||||
const result = transformToOllama(upstream, "llama3.2");
|
||||
assert.equal(result.status, 503);
|
||||
assert.equal(result.headers.get("Retry-After"), "1");
|
||||
assert.match(String(result.headers.get("Content-Type") || ""), /application\/json/i);
|
||||
|
||||
const payload = await result.json();
|
||||
assert.equal(payload.error?.code, "admission_oversized");
|
||||
assert.equal(payload.error?.type, "server_error");
|
||||
assert.equal(payload.error?.message, "Request too large for current capacity");
|
||||
});
|
||||
|
||||
test("transformToOllama leaves successful non-SSE responses untouched", async () => {
|
||||
const body = { choices: [{ message: { role: "assistant", content: "hello" } }] };
|
||||
const upstream = new Response(JSON.stringify(body), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Sentinel": "preserved",
|
||||
},
|
||||
});
|
||||
|
||||
const result = transformToOllama(upstream, "llama3.2");
|
||||
assert.equal(result, upstream);
|
||||
assert.equal(result.status, 200);
|
||||
assert.equal(result.headers.get("X-Sentinel"), "preserved");
|
||||
assert.deepEqual(await result.json(), body);
|
||||
});
|
||||
|
||||
test("transformToOllama merges multi-chunk numeric tool_call id", async () => {
|
||||
const inputSSE = [
|
||||
`data: ${JSON.stringify({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
import {
|
||||
APP_STAGING_ALLOWED_EXACT_PATHS,
|
||||
@@ -56,6 +57,46 @@ test("findUnexpectedArtifactPaths flags app pack files outside the allowlist", (
|
||||
assert.deepEqual(unexpectedPaths, ["dist/scripts/build/prepublish.mjs", "docs/extra.md"]);
|
||||
});
|
||||
|
||||
test("findUnexpectedArtifactPaths flags node_modules even inside an allowed prefix", () => {
|
||||
// Regression guard: the allowlist grants the whole `@omniroute/opencode-provider/`
|
||||
// prefix, which used to authorize a nested node_modules inside it — 79 MB of
|
||||
// devDependencies (80% of the tarball) whenever the publish ran from a machine
|
||||
// that had installed inside that subpackage. package.json `files[]` excludes it
|
||||
// at the source; this asserts the gate FAILS instead of allowing a regression.
|
||||
const unexpectedPaths = findUnexpectedArtifactPaths(
|
||||
[
|
||||
"@omniroute/opencode-provider/node_modules/tsup/package.json",
|
||||
"@omniroute/opencode-provider/node_modules/esbuild/lib/main.js",
|
||||
"@omniroute/opencode-provider/dist/index.js",
|
||||
"@omniroute/opencode-provider/package.json",
|
||||
],
|
||||
{
|
||||
exactPaths: [],
|
||||
prefixPaths: ["@omniroute/opencode-provider/"],
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepEqual(unexpectedPaths, [
|
||||
"@omniroute/opencode-provider/node_modules/esbuild/lib/main.js",
|
||||
"@omniroute/opencode-provider/node_modules/tsup/package.json",
|
||||
]);
|
||||
});
|
||||
|
||||
test("package.json files[] excludes nested node_modules from the published package", () => {
|
||||
// The gate above is defence-in-depth; this pins the actual fix. Without the
|
||||
// "!**/node_modules/**" negation the tarball was 99.4 MB unpacked (31.3 MB
|
||||
// packed) instead of 20.0 MB (5.3 MB).
|
||||
const files: string[] = JSON.parse(
|
||||
readFileSync(new URL("../../package.json", import.meta.url), "utf8")
|
||||
).files;
|
||||
|
||||
assert.ok(
|
||||
files.includes("!**/node_modules/**"),
|
||||
'package.json "files" must keep the "!**/node_modules/**" negation — without it, ' +
|
||||
"a nested install inside @omniroute/* ships ~79 MB of devDependencies."
|
||||
);
|
||||
});
|
||||
|
||||
test("webdav-handler.mjs is allowed in staging dist/ (server-ws.mjs dependency, missed in 3.8.22 build)", () => {
|
||||
const unexpectedPaths = findUnexpectedArtifactPaths(["webdav-handler.mjs"], {
|
||||
exactPaths: APP_STAGING_ALLOWED_EXACT_PATHS,
|
||||
|
||||
46
tests/unit/repro-8522.test.ts
Normal file
46
tests/unit/repro-8522.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* repro-8522 — quality-gate inherited-drift defect.
|
||||
*
|
||||
* Issue #8522: check:file-size (and the eslint-suppressions count) are ABSOLUTE
|
||||
* ratchets with no base-ref comparison. Once the release base is over a frozen
|
||||
* cap (inherited drift from an already-merged PR), EVERY subsequent PR goes red
|
||||
* on that gate regardless of content — the "innocent PR" cannot pass, so red
|
||||
* stops distinguishing "you broke it" from "you exist".
|
||||
*
|
||||
* This test reproduces the minimal defect: an innocent PR (base and head have
|
||||
* IDENTICAL LOC on the frozen file, PR touched nothing) still produces a
|
||||
* violation, because `evaluateFileSizes` compares head LOC to the frozen number
|
||||
* with no notion of the base.
|
||||
*/
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { evaluateFileSizes } from "../../scripts/check/check-file-size.mjs";
|
||||
|
||||
test("8522: innocent PR (base already over frozen cap) must NOT be a violation", () => {
|
||||
// Scenario: frozen cap for src/foo.ts is 100. Some earlier merged PR grew it
|
||||
// to 110. The base of THIS PR is therefore 110. This PR is innocent — it does
|
||||
// not touch src/foo.ts at all, so head LOC == base LOC == 110.
|
||||
const baseLocByFile = { "src/foo.ts": 110 };
|
||||
const currentLocByFile = { ...baseLocByFile }; // PR changed nothing in foo.ts
|
||||
const frozen = { "src/foo.ts": 100 };
|
||||
const cap = 100;
|
||||
|
||||
// With baseLocByFile, the gate compares against max(frozen, base) = max(100, 110) = 110,
|
||||
// so 110 > 110 is false — innocent PR passes.
|
||||
const { violations } = evaluateFileSizes(currentLocByFile, frozen, cap, baseLocByFile);
|
||||
|
||||
assert.deepEqual(violations, [], "innocent PR flagged for inherited drift");
|
||||
});
|
||||
|
||||
test("8522: PR that DOES grow a frozen file above frozen cap is a violation", () => {
|
||||
// Sanity: the gate must still catch a PR that grows the file above its cap.
|
||||
// Base is at the frozen cap (100), but PR grew it to 112.
|
||||
const baseLocByFile = { "src/foo.ts": 100 };
|
||||
const currentLocByFile = { "src/foo.ts": 112 }; // PR grew it +12
|
||||
const frozen = { "src/foo.ts": 100 };
|
||||
const cap = 100;
|
||||
|
||||
// With baseLocByFile: threshold = max(100, 100) = 100, 112 > 100 → violation
|
||||
const { violations } = evaluateFileSizes(currentLocByFile, frozen, cap, baseLocByFile);
|
||||
assert.equal(violations.length, 1, "own-growth PR must be a violation");
|
||||
});
|
||||
65
tests/unit/repro-8956.test.ts
Normal file
65
tests/unit/repro-8956.test.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
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 { resolveProjectRoot } = await import("../../src/lib/system/autoUpdate.ts");
|
||||
|
||||
test("repro-8956: resolveProjectRoot skips synthetic .build/next/package.json (no name field)", () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "repro-8956-"));
|
||||
try {
|
||||
// Simulate a Next.js standalone build layout inside a real repo:
|
||||
// <tmp>/repo/.git/ (real git marker)
|
||||
// <tmp>/repo/package.json (real repo root, has a "name" field)
|
||||
// <tmp>/repo/.build/next/package.json (synthetic marker, {"type":"commonjs"}, no name)
|
||||
// <tmp>/repo/.build/next/server/chunks/ (where the bundled module lives at runtime)
|
||||
const repoRoot = path.join(tmp, "repo");
|
||||
const buildPkgDir = path.join(repoRoot, ".build", "next");
|
||||
const chunksDir = path.join(buildPkgDir, "server", "chunks");
|
||||
|
||||
fs.mkdirSync(path.join(repoRoot, ".git"), { recursive: true });
|
||||
fs.mkdirSync(chunksDir, { recursive: true });
|
||||
|
||||
// Real root package.json with a name
|
||||
fs.writeFileSync(path.join(repoRoot, "package.json"), JSON.stringify({ name: "omniroute" }));
|
||||
// Synthetic Next.js standalone build marker — no "name" field
|
||||
fs.writeFileSync(path.join(buildPkgDir, "package.json"), JSON.stringify({ type: "commonjs" }));
|
||||
|
||||
// Start from the chunks dir (simulating __dirname at runtime)
|
||||
const root = resolveProjectRoot("/fallback", chunksDir);
|
||||
|
||||
// Must NOT stop at .build/next — must walk up to the repo root that has .git
|
||||
assert.equal(
|
||||
root,
|
||||
repoRoot,
|
||||
`resolveProjectRoot returned ${root}, expected the repo root ${repoRoot} ` +
|
||||
"(it stopped at the synthetic .build/next/package.json marker)"
|
||||
);
|
||||
|
||||
// The resolved root must own .git so source-mode validation passes
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(root, ".git")),
|
||||
`PROJECT_ROOT resolved to ${root}, which lacks .git`
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("repro-8956: resolveProjectRoot still finds package.json with a name field", () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "repro-8956-named-"));
|
||||
try {
|
||||
// A normal repo root: has .git AND a named package.json
|
||||
const repoRoot = path.join(tmp, "my-repo");
|
||||
const subDir = path.join(repoRoot, "some", "deep", "path");
|
||||
fs.mkdirSync(subDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(repoRoot, "package.json"), JSON.stringify({ name: "my-app" }));
|
||||
fs.mkdirSync(path.join(repoRoot, ".git"), { recursive: true });
|
||||
|
||||
const root = resolveProjectRoot("/fallback", subDir);
|
||||
assert.equal(root, repoRoot);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
202
tests/unit/resource-pressure-policy.test.ts
Normal file
202
tests/unit/resource-pressure-policy.test.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
createResourcePressureTracker,
|
||||
resolveResourcePressureThresholds,
|
||||
type PressureReason,
|
||||
type PressureSeverity,
|
||||
type ResourcePressureState,
|
||||
type ResourcePressureThresholds,
|
||||
type ResourceSignals,
|
||||
} from "../../open-sse/utils/resourcePressurePolicy.ts";
|
||||
|
||||
const MiB = 1024 ** 2;
|
||||
|
||||
function baseSignals(overrides: Partial<ResourceSignals> = {}): ResourceSignals {
|
||||
return {
|
||||
observedAtMs: 1_000,
|
||||
v8: { heapUsedBytes: 100 * MiB, heapLimitBytes: 1_000 * MiB },
|
||||
process: {
|
||||
rssBytes: 200 * MiB,
|
||||
externalBytes: 10 * MiB,
|
||||
arrayBuffersBytes: MiB,
|
||||
availableBytes: null,
|
||||
constrainedBytes: null,
|
||||
},
|
||||
cgroup: { currentBytes: null, maxBytes: null, highBytes: null, events: null },
|
||||
psi: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const fastThresholds: Partial<ResourcePressureThresholds> = {
|
||||
highRatio: 0.8,
|
||||
criticalRatio: 0.9,
|
||||
recoveryRatio: 0.7,
|
||||
highPsiAvg10: 20,
|
||||
criticalPsiAvg10: 40,
|
||||
recoveryPsiAvg10: 10,
|
||||
sustainedSamplesHigh: 2,
|
||||
sustainedSamplesCritical: 2,
|
||||
sustainedSamplesRecovery: 2,
|
||||
heapAbsoluteThresholdMb: null,
|
||||
};
|
||||
|
||||
describe("resource pressure threshold validation", () => {
|
||||
it("accepts every valid boundary", () => {
|
||||
const thresholds = resolveResourcePressureThresholds({
|
||||
recoveryRatio: 0,
|
||||
highRatio: 0.5,
|
||||
criticalRatio: 1,
|
||||
recoveryPsiAvg10: 0,
|
||||
highPsiAvg10: 50,
|
||||
criticalPsiAvg10: 100,
|
||||
sustainedSamplesHigh: 1,
|
||||
sustainedSamplesCritical: 1,
|
||||
sustainedSamplesRecovery: 10_000,
|
||||
heapAbsoluteThresholdMb: null,
|
||||
});
|
||||
assert.equal(thresholds.recoveryRatio, 0);
|
||||
assert.equal(thresholds.criticalRatio, 1);
|
||||
assert.equal(thresholds.criticalPsiAvg10, 100);
|
||||
assert.equal(thresholds.sustainedSamplesRecovery, 10_000);
|
||||
assert.equal(thresholds.heapAbsoluteThresholdMb, null);
|
||||
});
|
||||
|
||||
it("throws deterministically for invalid partial overrides", () => {
|
||||
const invalid: Array<Partial<ResourcePressureThresholds>> = [
|
||||
{ recoveryRatio: -0.01 },
|
||||
{ criticalRatio: 1.01 },
|
||||
{ highRatio: Number.NaN },
|
||||
{ recoveryRatio: 0.8, highRatio: 0.8 },
|
||||
{ highRatio: 0.95, criticalRatio: 0.9 },
|
||||
{ recoveryPsiAvg10: -1 },
|
||||
{ criticalPsiAvg10: 101 },
|
||||
{ highPsiAvg10: Number.POSITIVE_INFINITY },
|
||||
{ recoveryPsiAvg10: 20, highPsiAvg10: 20 },
|
||||
{ highPsiAvg10: 50, criticalPsiAvg10: 40 },
|
||||
{ sustainedSamplesHigh: 0 },
|
||||
{ sustainedSamplesCritical: 1.5 },
|
||||
{ sustainedSamplesRecovery: 10_001 },
|
||||
{ heapAbsoluteThresholdMb: 0 },
|
||||
{ heapAbsoluteThresholdMb: Number.POSITIVE_INFINITY },
|
||||
];
|
||||
for (const partial of invalid) {
|
||||
assert.throws(() => resolveResourcePressureThresholds(partial), RangeError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("resource pressure policy", () => {
|
||||
it("does not let high then critical count as two critical samples", () => {
|
||||
const tracker = createResourcePressureTracker(fastThresholds);
|
||||
const high = baseSignals({
|
||||
v8: { heapUsedBytes: 850 * MiB, heapLimitBytes: 1_000 * MiB },
|
||||
});
|
||||
const critical = baseSignals({
|
||||
v8: { heapUsedBytes: 950 * MiB, heapLimitBytes: 1_000 * MiB },
|
||||
});
|
||||
|
||||
assert.equal(tracker.observe(high).severity, "normal");
|
||||
assert.equal(tracker.observe(critical).severity, "normal");
|
||||
assert.equal(tracker.observe(critical).severity, "critical");
|
||||
});
|
||||
|
||||
it("resets pending streak when severity or reason alternates", () => {
|
||||
const tracker = createResourcePressureTracker(fastThresholds);
|
||||
const heapCritical = baseSignals({
|
||||
v8: { heapUsedBytes: 950 * MiB, heapLimitBytes: 1_000 * MiB },
|
||||
});
|
||||
const psiCritical = baseSignals({
|
||||
psi: {
|
||||
someAvg10: 50,
|
||||
someAvg60: null,
|
||||
someAvg300: null,
|
||||
fullAvg10: null,
|
||||
fullAvg60: null,
|
||||
fullAvg300: null,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(tracker.observe(heapCritical).severity, "normal");
|
||||
assert.equal(tracker.observe(psiCritical).severity, "normal");
|
||||
assert.equal(tracker.observe(psiCritical).severity, "critical");
|
||||
assert.equal(tracker.getState().reason, "psi_some");
|
||||
});
|
||||
|
||||
it("baselines cumulative OOM counters and only treats increases as events", () => {
|
||||
const tracker = createResourcePressureTracker(fastThresholds);
|
||||
const oomCounters = (oom: number, oom_kill: number, observedAtMs: number) =>
|
||||
baseSignals({
|
||||
observedAtMs,
|
||||
cgroup: {
|
||||
currentBytes: null,
|
||||
maxBytes: null,
|
||||
highBytes: null,
|
||||
events: { low: 0, high: 0, max: 0, oom, oom_kill },
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(tracker.observe(oomCounters(7, 3, 1)).severity, "normal", "history baselines");
|
||||
assert.equal(tracker.observe(oomCounters(7, 3, 2)).severity, "normal", "unchanged history");
|
||||
|
||||
const event = tracker.observe(oomCounters(8, 3, 3));
|
||||
assert.equal(event.severity, "critical", "a new OOM event is immediately critical");
|
||||
assert.equal(event.reason, "oom_event");
|
||||
|
||||
assert.equal(tracker.observe(oomCounters(8, 3, 4)).severity, "critical");
|
||||
assert.equal(
|
||||
tracker.observe(oomCounters(8, 3, 5)).severity,
|
||||
"normal",
|
||||
"unchanged allows recovery"
|
||||
);
|
||||
});
|
||||
|
||||
it("re-baselines when OOM counters reset or the cgroup event source is replaced", () => {
|
||||
const tracker = createResourcePressureTracker(fastThresholds);
|
||||
const events = (oom: number, oom_kill: number) =>
|
||||
baseSignals({
|
||||
cgroup: {
|
||||
currentBytes: null,
|
||||
maxBytes: null,
|
||||
highBytes: null,
|
||||
events: { low: 0, high: 0, max: 0, oom, oom_kill },
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(tracker.observe(events(10, 4)).severity, "normal");
|
||||
assert.equal(tracker.observe(events(1, 0)).severity, "normal", "counter reset re-baselines");
|
||||
assert.equal(
|
||||
tracker.observe({ ...events(1, 0), cgroup: { ...events(1, 0).cgroup, events: null } })
|
||||
.severity,
|
||||
"normal"
|
||||
);
|
||||
assert.equal(tracker.observe(events(9, 3)).severity, "normal", "replacement re-baselines");
|
||||
});
|
||||
|
||||
it("keeps snapshot state fields and bounded-cardinality values", () => {
|
||||
const tracker = createResourcePressureTracker(fastThresholds);
|
||||
const state: ResourcePressureState = tracker.observe(baseSignals());
|
||||
const severities = new Set<PressureSeverity>(["normal", "high", "critical"]);
|
||||
const reasons = new Set<PressureReason>([
|
||||
"none",
|
||||
"v8_heap_ratio",
|
||||
"v8_heap_absolute",
|
||||
"cgroup_ratio",
|
||||
"cgroup_high",
|
||||
"psi_some",
|
||||
"psi_full",
|
||||
"oom_event",
|
||||
]);
|
||||
assert.ok(severities.has(state.severity));
|
||||
assert.ok(reasons.has(state.reason));
|
||||
assert.deepEqual(Object.keys(state).sort(), [
|
||||
"elevatedStreak",
|
||||
"lastTransitionAtMs",
|
||||
"observedAtMs",
|
||||
"reason",
|
||||
"recoveryStreak",
|
||||
"severity",
|
||||
]);
|
||||
});
|
||||
});
|
||||
330
tests/unit/resource-pressure-runtime.test.ts
Normal file
330
tests/unit/resource-pressure-runtime.test.ts
Normal file
@@ -0,0 +1,330 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
createResourcePressureRuntime,
|
||||
type ResourcePressureRuntime,
|
||||
} from "../../open-sse/utils/resourcePressure.ts";
|
||||
import type { ResourceSignals } from "../../open-sse/utils/resourcePressurePolicy.ts";
|
||||
|
||||
const MiB = 1024 ** 2;
|
||||
|
||||
function signals(observedAtMs: number, heapUsedMb = 100): ResourceSignals {
|
||||
return {
|
||||
observedAtMs,
|
||||
v8: { heapUsedBytes: heapUsedMb * MiB, heapLimitBytes: 1_000 * MiB },
|
||||
process: {
|
||||
rssBytes: 200 * MiB,
|
||||
externalBytes: 10 * MiB,
|
||||
arrayBuffersBytes: MiB,
|
||||
availableBytes: null,
|
||||
constrainedBytes: null,
|
||||
},
|
||||
cgroup: { currentBytes: null, maxBytes: null, highBytes: null, events: null },
|
||||
psi: null,
|
||||
};
|
||||
}
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (reason?: unknown) => void;
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
async function settleRefresh(runtime: ResourcePressureRuntime): Promise<void> {
|
||||
await runtime.whenRefreshSettled();
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
describe("ResourcePressureRuntime stale-while-revalidate cache", () => {
|
||||
it("does no proc/sys I/O in check(), while a cheap first-request heap breach sheds immediately", async () => {
|
||||
let slowSamples = 0;
|
||||
const runtime = createResourcePressureRuntime({
|
||||
heapThresholdMb: 200,
|
||||
immediateHeapUsedMb: () => 201,
|
||||
sample: async () => {
|
||||
slowSamples += 1;
|
||||
return signals(1);
|
||||
},
|
||||
});
|
||||
|
||||
const guard = runtime.check();
|
||||
assert.ok(guard);
|
||||
assert.equal(guard.status, 503);
|
||||
assert.equal(slowSamples, 0, "request-path check must not invoke the async proc/sys sampler");
|
||||
assert.equal(runtime.getObservation().state.reason, "v8_heap_absolute");
|
||||
await settleRefresh(runtime);
|
||||
assert.equal(slowSamples, 1, "refresh may run after the request-path decision");
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
it("serves a fresh cached sample without scheduling another refresh", async () => {
|
||||
let now = 0;
|
||||
let calls = 0;
|
||||
const runtime = createResourcePressureRuntime({
|
||||
nowMs: () => now,
|
||||
staleAfterMs: 100,
|
||||
immediateHeapUsedMb: () => 100,
|
||||
sample: async () => {
|
||||
calls += 1;
|
||||
return signals(now);
|
||||
},
|
||||
});
|
||||
|
||||
runtime.check();
|
||||
await settleRefresh(runtime);
|
||||
assert.equal(calls, 1);
|
||||
now = 99;
|
||||
runtime.check();
|
||||
await settleRefresh(runtime);
|
||||
assert.equal(calls, 1);
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
it("schedules at most one refresh under concurrent stale checks", async () => {
|
||||
let now = 0;
|
||||
let calls = 0;
|
||||
const pending = deferred<ResourceSignals>();
|
||||
const runtime = createResourcePressureRuntime({
|
||||
nowMs: () => now,
|
||||
staleAfterMs: 10,
|
||||
immediateHeapUsedMb: () => 100,
|
||||
sample: async () => {
|
||||
calls += 1;
|
||||
if (calls === 1) return signals(0);
|
||||
return pending.promise;
|
||||
},
|
||||
});
|
||||
|
||||
runtime.check();
|
||||
await settleRefresh(runtime);
|
||||
now = 11;
|
||||
for (let index = 0; index < 50; index += 1) runtime.check();
|
||||
assert.equal(calls, 1, "scheduled work must not run synchronously in check()");
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
assert.equal(calls, 2);
|
||||
pending.resolve(signals(11));
|
||||
await settleRefresh(runtime);
|
||||
assert.equal(calls, 2);
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
it("retains a bounded stale snapshot on refresh failure and retries only after backoff", async () => {
|
||||
let now = 0;
|
||||
let calls = 0;
|
||||
const runtime = createResourcePressureRuntime({
|
||||
nowMs: () => now,
|
||||
staleAfterMs: 10,
|
||||
maxStaleMs: 100,
|
||||
retryAfterMs: 20,
|
||||
immediateHeapUsedMb: () => 100,
|
||||
sample: async () => {
|
||||
calls += 1;
|
||||
if (calls === 1) return signals(0, 950);
|
||||
throw new Error("proc unavailable");
|
||||
},
|
||||
thresholds: {
|
||||
sustainedSamplesCritical: 1,
|
||||
heapAbsoluteThresholdMb: null,
|
||||
},
|
||||
});
|
||||
|
||||
runtime.check();
|
||||
await settleRefresh(runtime);
|
||||
now = 11;
|
||||
runtime.check();
|
||||
await settleRefresh(runtime);
|
||||
assert.equal(calls, 2);
|
||||
assert.equal(runtime.getObservation().signals?.observedAtMs, 0, "failure retains stale data");
|
||||
|
||||
now = 25;
|
||||
runtime.check();
|
||||
await settleRefresh(runtime);
|
||||
assert.equal(calls, 2, "failure backoff prevents a refresh storm");
|
||||
|
||||
now = 31;
|
||||
runtime.check();
|
||||
await settleRefresh(runtime);
|
||||
assert.equal(calls, 3);
|
||||
|
||||
now = 101;
|
||||
assert.equal(runtime.check(), null, "expired stale adaptive pressure fails open");
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
it("measures failure backoff from settlement, not refresh start", async () => {
|
||||
let now = 0;
|
||||
let calls = 0;
|
||||
const pending = deferred<ResourceSignals>();
|
||||
const runtime = createResourcePressureRuntime({
|
||||
nowMs: () => now,
|
||||
staleAfterMs: 10,
|
||||
maxStaleMs: 100,
|
||||
retryAfterMs: 20,
|
||||
immediateHeapUsedMb: () => 100,
|
||||
sample: async () => {
|
||||
calls += 1;
|
||||
if (calls === 1) return signals(0);
|
||||
return pending.promise;
|
||||
},
|
||||
});
|
||||
|
||||
runtime.check();
|
||||
await settleRefresh(runtime);
|
||||
assert.equal(calls, 1);
|
||||
|
||||
now = 11;
|
||||
runtime.check();
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
assert.equal(calls, 2);
|
||||
|
||||
// Slow failure: wall clock advances past retryAfter before the sample rejects.
|
||||
now = 50;
|
||||
pending.reject(new Error("proc unavailable"));
|
||||
await settleRefresh(runtime);
|
||||
assert.equal(calls, 2);
|
||||
|
||||
// Retry must wait full retryAfterMs from settlement (50), not from start (11).
|
||||
now = 69;
|
||||
runtime.check();
|
||||
await settleRefresh(runtime);
|
||||
assert.equal(calls, 2, "failure backoff starts at settlement, not refresh start");
|
||||
|
||||
now = 70;
|
||||
runtime.check();
|
||||
await settleRefresh(runtime);
|
||||
assert.equal(calls, 3);
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
it("measures success freshness from publication, not refresh start", async () => {
|
||||
let now = 0;
|
||||
let calls = 0;
|
||||
const pending = deferred<ResourceSignals>();
|
||||
const runtime = createResourcePressureRuntime({
|
||||
nowMs: () => now,
|
||||
staleAfterMs: 20,
|
||||
maxStaleMs: 100,
|
||||
immediateHeapUsedMb: () => 100,
|
||||
sample: async () => {
|
||||
calls += 1;
|
||||
if (calls === 1) return signals(0);
|
||||
return pending.promise;
|
||||
},
|
||||
});
|
||||
|
||||
runtime.check();
|
||||
await settleRefresh(runtime);
|
||||
assert.equal(calls, 1);
|
||||
|
||||
now = 21;
|
||||
runtime.check();
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
assert.equal(calls, 2);
|
||||
|
||||
// Slow success: wall clock advances past staleAfter before the sample resolves.
|
||||
now = 100;
|
||||
pending.resolve(signals(100));
|
||||
await settleRefresh(runtime);
|
||||
assert.equal(calls, 2);
|
||||
assert.equal(runtime.getObservation().signals?.observedAtMs, 100);
|
||||
|
||||
// Freshness must run full staleAfterMs from publication (100), not start (21).
|
||||
now = 119;
|
||||
runtime.check();
|
||||
await settleRefresh(runtime);
|
||||
assert.equal(calls, 2, "success freshness starts at publication, not refresh start");
|
||||
|
||||
now = 120;
|
||||
runtime.check();
|
||||
await settleRefresh(runtime);
|
||||
assert.equal(calls, 3);
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
it("default scheduler unrefs Immediate; injected schedulers stay caller-owned", async () => {
|
||||
// Injected schedule is never wrapped: the runtime must not call unref on it.
|
||||
let scheduled = 0;
|
||||
let unrefCalled = 0;
|
||||
const injected = (refresh: () => void) => {
|
||||
scheduled += 1;
|
||||
const handle = setImmediate(refresh);
|
||||
const originalUnref = handle.unref.bind(handle);
|
||||
handle.unref = () => {
|
||||
unrefCalled += 1;
|
||||
return originalUnref();
|
||||
};
|
||||
};
|
||||
|
||||
const withInjected = createResourcePressureRuntime({
|
||||
immediateHeapUsedMb: () => 100,
|
||||
sample: async () => signals(1),
|
||||
schedule: injected,
|
||||
});
|
||||
withInjected.check();
|
||||
await settleRefresh(withInjected);
|
||||
assert.equal(scheduled, 1);
|
||||
assert.equal(unrefCalled, 0, "injected schedule handles remain caller-owned");
|
||||
withInjected.dispose();
|
||||
|
||||
// Default schedule path: capture the Immediate and prove it is unref'd so a
|
||||
// pending refresh alone cannot keep the process alive.
|
||||
const originalSetImmediate = globalThis.setImmediate;
|
||||
let captured: NodeJS.Immediate | undefined;
|
||||
globalThis.setImmediate = ((callback: (...args: unknown[]) => void, ...args: unknown[]) => {
|
||||
const handle = originalSetImmediate(callback, ...args);
|
||||
captured = handle;
|
||||
return handle;
|
||||
}) as typeof setImmediate;
|
||||
try {
|
||||
const runtime = createResourcePressureRuntime({
|
||||
immediateHeapUsedMb: () => 100,
|
||||
// Never resolve: we only care about the scheduled Immediate ref state.
|
||||
sample: () => new Promise(() => {}),
|
||||
});
|
||||
runtime.check();
|
||||
assert.ok(captured, "default schedule must use setImmediate");
|
||||
assert.equal(captured.hasRef(), false, "default Immediate must be unref'd");
|
||||
runtime.dispose();
|
||||
if (captured) clearImmediate(captured);
|
||||
} finally {
|
||||
globalThis.setImmediate = originalSetImmediate;
|
||||
}
|
||||
});
|
||||
|
||||
it("dispose ignores late refresh results and independently owned runtimes do not share state", async () => {
|
||||
const pending = deferred<ResourceSignals>();
|
||||
let firstCalls = 0;
|
||||
const first = createResourcePressureRuntime({
|
||||
immediateHeapUsedMb: () => 100,
|
||||
sample: async () => {
|
||||
firstCalls += 1;
|
||||
return pending.promise;
|
||||
},
|
||||
});
|
||||
first.check();
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
assert.equal(firstCalls, 1);
|
||||
first.dispose();
|
||||
pending.resolve(signals(1));
|
||||
await settleRefresh(first);
|
||||
assert.equal(
|
||||
first.getObservation().signals,
|
||||
null,
|
||||
"disposed runtime ignores late refresh results"
|
||||
);
|
||||
|
||||
const second = createResourcePressureRuntime({
|
||||
immediateHeapUsedMb: () => 100,
|
||||
sample: async () => signals(2),
|
||||
});
|
||||
assert.notEqual(first, second);
|
||||
second.check();
|
||||
await settleRefresh(second);
|
||||
assert.equal(second.getObservation().signals?.observedAtMs, 2);
|
||||
second.dispose();
|
||||
});
|
||||
});
|
||||
178
tests/unit/resource-pressure-sampler.test.ts
Normal file
178
tests/unit/resource-pressure-sampler.test.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
decodeMountInfoPath,
|
||||
parseCgroup2Mount,
|
||||
parseCgroupV2Path,
|
||||
resolveCgroupDirectory,
|
||||
sampleResourceSignals,
|
||||
sanitizeMemoryBytes,
|
||||
type ResourcePressureFs,
|
||||
} from "../../open-sse/utils/resourcePressureSampler.ts";
|
||||
|
||||
const MiB = 1024 ** 2;
|
||||
const GiB = 1024 ** 3;
|
||||
|
||||
function memoryUsage(heapUsed = 1): NodeJS.MemoryUsage {
|
||||
return {
|
||||
rss: 500 * MiB,
|
||||
heapTotal: 300 * MiB,
|
||||
heapUsed,
|
||||
external: 12 * MiB,
|
||||
arrayBuffers: 3 * MiB,
|
||||
};
|
||||
}
|
||||
|
||||
function mapFs(entries: ReadonlyArray<readonly [string, string]>): ResourcePressureFs {
|
||||
const files = new Map(entries);
|
||||
return { readText: async (filePath) => files.get(filePath) ?? null };
|
||||
}
|
||||
|
||||
describe("resource pressure cgroup parsers", () => {
|
||||
it("accepts only the exact unified cgroup entry", () => {
|
||||
assert.equal(parseCgroupV2Path("2:cpu:/wrong\n0::/delegated/service\n"), "/delegated/service");
|
||||
assert.equal(parseCgroupV2Path("0:cpu:/wrong\n1::/also-wrong\n"), null);
|
||||
});
|
||||
|
||||
it("decodes mountinfo octal escapes in root and mountpoint", () => {
|
||||
assert.equal(
|
||||
decodeMountInfoPath("/sys/fs/cgroup\\040space\\134unit"),
|
||||
"/sys/fs/cgroup space\\unit"
|
||||
);
|
||||
assert.deepEqual(
|
||||
parseCgroup2Mount(
|
||||
"43 34 0:35 /delegated\\040root /sys/fs/cgroup\\040space rw - cgroup2 cgroup2 rw\n"
|
||||
),
|
||||
{ root: "/delegated root", mountpoint: "/sys/fs/cgroup space" }
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves delegated mount roots within the decoded mountpoint", async () => {
|
||||
const fs = mapFs([
|
||||
["/proc/self/cgroup", "0::/delegated root/team/service\n"],
|
||||
[
|
||||
"/proc/self/mountinfo",
|
||||
"43 34 0:35 /delegated\\040root /sys/fs/cgroup\\040space rw - cgroup2 cgroup2 rw\n",
|
||||
],
|
||||
["/sys/fs/cgroup space/team/service/memory.current", "1\n"],
|
||||
]);
|
||||
|
||||
assert.equal(await resolveCgroupDirectory(fs.readText), "/sys/fs/cgroup space/team/service");
|
||||
});
|
||||
|
||||
it("rejects NUL, traversal, malformed, and out-of-root cgroup paths", async () => {
|
||||
for (const cgroupPath of [
|
||||
"/delegated/../escape",
|
||||
"/delegated/%2e%2e/escape",
|
||||
"/delegated/service\0escape",
|
||||
"delegated/service",
|
||||
"/other/service",
|
||||
]) {
|
||||
const fs = mapFs([
|
||||
["/proc/self/cgroup", `0::${cgroupPath}\n`],
|
||||
["/proc/self/mountinfo", "43 34 0:35 /delegated /safe/cgroup rw - cgroup2 cgroup2 rw\n"],
|
||||
["/safe/cgroup/memory.current", "1\n"],
|
||||
]);
|
||||
assert.equal(
|
||||
await resolveCgroupDirectory(fs.readText, { allowDefaultFallback: false }),
|
||||
null,
|
||||
cgroupPath
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("falls back to the validated default cgroup root when proc metadata is malformed", async () => {
|
||||
const fs = mapFs([
|
||||
["/proc/self/cgroup", "malformed\n"],
|
||||
["/proc/self/mountinfo", "malformed\n"],
|
||||
["/sys/fs/cgroup/memory.current", "123\n"],
|
||||
]);
|
||||
assert.equal(await resolveCgroupDirectory(fs.readText), "/sys/fs/cgroup");
|
||||
});
|
||||
});
|
||||
|
||||
describe("sampleResourceSignals", () => {
|
||||
it("captures process, V8, cgroup, event, and PSI snapshot fields", async () => {
|
||||
const fs = mapFs([
|
||||
["/proc/self/cgroup", "0::/slice/service\n"],
|
||||
["/proc/self/mountinfo", "43 34 0:35 / /sys/fs/cgroup rw - cgroup2 cgroup2 rw\n"],
|
||||
["/sys/fs/cgroup/slice/service/memory.current", `${800 * MiB}\n`],
|
||||
["/sys/fs/cgroup/slice/service/memory.max", `${GiB}\n`],
|
||||
["/sys/fs/cgroup/slice/service/memory.high", "966367641\n"],
|
||||
["/sys/fs/cgroup/slice/service/memory.events", "low 1\nhigh 2\nmax 3\noom 4\noom_kill 5\n"],
|
||||
[
|
||||
"/proc/pressure/memory",
|
||||
"some avg10=1.50 avg60=2.00 avg300=3.25 total=9\nfull avg10=0.25 avg60=0.50 avg300=0.75 total=1\n",
|
||||
],
|
||||
]);
|
||||
|
||||
const signals = await sampleResourceSignals({
|
||||
nowMs: () => 42,
|
||||
memoryUsage: () => memoryUsage(250 * MiB),
|
||||
heapStatistics: () => ({ heap_size_limit: GiB, used_heap_size: 250 * MiB }),
|
||||
availableMemory: () => 4 * GiB,
|
||||
constrainedMemory: () => undefined,
|
||||
fs,
|
||||
});
|
||||
|
||||
assert.equal(signals.observedAtMs, 42);
|
||||
assert.deepEqual(signals.v8, { heapUsedBytes: 250 * MiB, heapLimitBytes: GiB });
|
||||
assert.deepEqual(signals.process, {
|
||||
rssBytes: 500 * MiB,
|
||||
externalBytes: 12 * MiB,
|
||||
arrayBuffersBytes: 3 * MiB,
|
||||
availableBytes: 4 * GiB,
|
||||
constrainedBytes: null,
|
||||
});
|
||||
assert.deepEqual(signals.cgroup, {
|
||||
currentBytes: 800 * MiB,
|
||||
maxBytes: GiB,
|
||||
highBytes: 966367641,
|
||||
events: { low: 1, high: 2, max: 3, oom: 4, oom_kill: 5 },
|
||||
});
|
||||
assert.equal(signals.psi?.someAvg10, 1.5);
|
||||
assert.equal(signals.psi?.fullAvg10, 0.25);
|
||||
});
|
||||
|
||||
it("fails open when platform reads fail or return malformed values", async () => {
|
||||
const signals = await sampleResourceSignals({
|
||||
memoryUsage: () => memoryUsage(),
|
||||
heapStatistics: () => ({ heap_size_limit: GiB, used_heap_size: 1 }),
|
||||
availableMemory: () => {
|
||||
throw new Error("unavailable");
|
||||
},
|
||||
constrainedMemory: () => Number.POSITIVE_INFINITY,
|
||||
fs: {
|
||||
readText: async (filePath) => {
|
||||
if (filePath === "/proc/self/cgroup") throw new Error("unavailable");
|
||||
return "malformed";
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal(signals.process.availableBytes, null);
|
||||
assert.equal(signals.process.constrainedBytes, null);
|
||||
assert.deepEqual(signals.cgroup, {
|
||||
currentBytes: null,
|
||||
maxBytes: null,
|
||||
highBytes: null,
|
||||
events: null,
|
||||
});
|
||||
assert.equal(signals.psi, null);
|
||||
});
|
||||
|
||||
it("treats missing, zero, max, and unsafe memory quantities as unavailable", () => {
|
||||
for (const value of [
|
||||
undefined,
|
||||
"",
|
||||
"max",
|
||||
0,
|
||||
-1,
|
||||
Number.NaN,
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
2 ** 63,
|
||||
]) {
|
||||
assert.equal(sanitizeMemoryBytes(value), null, String(value));
|
||||
}
|
||||
assert.equal(sanitizeMemoryBytes("123"), 123);
|
||||
});
|
||||
});
|
||||
134
tests/unit/resource-pressure.test.ts
Normal file
134
tests/unit/resource-pressure.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
createResourcePressureRuntime,
|
||||
getResourcePressureObservation,
|
||||
reloadResourcePressureRuntime,
|
||||
type ResourceSignals,
|
||||
} from "../../open-sse/utils/resourcePressure.ts";
|
||||
|
||||
const MiB = 1024 ** 2;
|
||||
|
||||
function signals(observedAtMs: number, heapUsedMb: number): ResourceSignals {
|
||||
return {
|
||||
observedAtMs,
|
||||
v8: { heapUsedBytes: heapUsedMb * MiB, heapLimitBytes: 1_000 * MiB },
|
||||
process: {
|
||||
rssBytes: 200 * MiB,
|
||||
externalBytes: 10 * MiB,
|
||||
arrayBuffersBytes: MiB,
|
||||
availableBytes: null,
|
||||
constrainedBytes: null,
|
||||
},
|
||||
cgroup: { currentBytes: null, maxBytes: null, highBytes: null, events: null },
|
||||
psi: null,
|
||||
};
|
||||
}
|
||||
|
||||
describe("resource pressure HTTP guard facade", () => {
|
||||
it("preserves strict immediate first-request heap shedding", async () => {
|
||||
let samples = 0;
|
||||
const runtime = createResourcePressureRuntime({
|
||||
heapThresholdMb: 200,
|
||||
immediateHeapUsedMb: () => 201,
|
||||
sample: async () => {
|
||||
samples += 1;
|
||||
return signals(1, 100);
|
||||
},
|
||||
});
|
||||
|
||||
const guard = runtime.check();
|
||||
assert.ok(guard);
|
||||
assert.equal(guard.status, 503);
|
||||
assert.equal(samples, 0, "the asynchronous sampler cannot run in the request path");
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
it("does not shed when heap usage equals the strict threshold", () => {
|
||||
const runtime = createResourcePressureRuntime({
|
||||
heapThresholdMb: 200,
|
||||
immediateHeapUsedMb: () => 200,
|
||||
sample: async () => signals(1, 100),
|
||||
});
|
||||
assert.equal(runtime.check(), null);
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
it("returns a sanitized standards-correct 503 with Retry-After", async () => {
|
||||
const runtime = createResourcePressureRuntime({
|
||||
heapThresholdMb: 200,
|
||||
immediateHeapUsedMb: () => 987,
|
||||
sample: async () => signals(1, 100),
|
||||
});
|
||||
|
||||
const guard = runtime.check();
|
||||
assert.ok(guard);
|
||||
assert.equal(guard.success, false);
|
||||
assert.equal(guard.status, 503);
|
||||
assert.equal(guard.response.status, 503);
|
||||
assert.equal(guard.response.headers.get("Retry-After"), "5");
|
||||
assert.equal(guard.response.headers.get("Content-Type"), "application/json");
|
||||
const payload = await guard.response.json();
|
||||
assert.deepEqual(payload.error, {
|
||||
message: "Service temporarily unavailable due to resource pressure. Retry shortly.",
|
||||
type: "server_error",
|
||||
code: "resource_pressure",
|
||||
});
|
||||
const clientText = JSON.stringify(payload) + guard.error;
|
||||
assert.ok(!clientText.includes("987"));
|
||||
assert.ok(!/\bMB\b/.test(clientText));
|
||||
runtime.dispose();
|
||||
});
|
||||
|
||||
it("reload atomically replaces and resets the thin default facade", async () => {
|
||||
let firstCalls = 0;
|
||||
reloadResourcePressureRuntime({
|
||||
immediateHeapUsedMb: () => 100,
|
||||
sample: async () => {
|
||||
firstCalls += 1;
|
||||
return signals(1, 100);
|
||||
},
|
||||
});
|
||||
const replacement = reloadResourcePressureRuntime({
|
||||
immediateHeapUsedMb: () => 100,
|
||||
sample: async () => signals(2, 100),
|
||||
});
|
||||
|
||||
assert.deepEqual(getResourcePressureObservation(), {
|
||||
signals: null,
|
||||
state: {
|
||||
severity: "normal",
|
||||
reason: "none",
|
||||
elevatedStreak: 0,
|
||||
recoveryStreak: 0,
|
||||
lastTransitionAtMs: 0,
|
||||
observedAtMs: 0,
|
||||
},
|
||||
});
|
||||
assert.equal(firstCalls, 0, "replaced runtime must not retain or run scheduled work");
|
||||
replacement.dispose();
|
||||
});
|
||||
|
||||
it("exposes all observation snapshot fields", async () => {
|
||||
const runtime = createResourcePressureRuntime({
|
||||
immediateHeapUsedMb: () => 100,
|
||||
sample: async () => signals(42, 100),
|
||||
});
|
||||
assert.deepEqual(runtime.getObservation(), {
|
||||
signals: null,
|
||||
state: {
|
||||
severity: "normal",
|
||||
reason: "none",
|
||||
elevatedStreak: 0,
|
||||
recoveryStreak: 0,
|
||||
lastTransitionAtMs: 0,
|
||||
observedAtMs: 0,
|
||||
},
|
||||
});
|
||||
runtime.check();
|
||||
await runtime.whenRefreshSettled();
|
||||
assert.equal(runtime.getObservation().signals?.observedAtMs, 42);
|
||||
assert.equal(runtime.getObservation().state.observedAtMs, 42);
|
||||
runtime.dispose();
|
||||
});
|
||||
});
|
||||
@@ -80,7 +80,7 @@ test("#9320: authenticated request (valid API key) returns 200 with models", asy
|
||||
// Create a valid API key
|
||||
await apiKeysDb.createApiKey("test-key-9320", "test-machine-9320");
|
||||
const keys = await apiKeysDb.getApiKeys();
|
||||
const apiKey = Array.isArray(keys) ? keys.find((k: any) => k.name === "test-key-9320") : null;
|
||||
const apiKey = Array.isArray(keys) ? keys.find((k) => k.name === "test-key-9320") : null;
|
||||
assert.ok(apiKey, "API key must have been created");
|
||||
|
||||
const res = await v1ModelsCatalog.getUnifiedModelsResponse(
|
||||
|
||||
Reference in New Issue
Block a user