mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-09 00:32:13 +03:00
Compare commits
3 Commits
feat/9490-
...
docs/radar
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f379e9215a | ||
|
|
4f21e663f0 | ||
|
|
f6708c78fb |
134
.github/workflows/quality.yml
vendored
134
.github/workflows/quality.yml
vendored
@@ -151,6 +151,63 @@ jobs:
|
||||
key: eslint-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', 'eslint.complexity-ratchets.config.mjs', 'config/quality/eslint-suppressions.json', 'package-lock.json') }}
|
||||
restore-keys: |
|
||||
eslint-${{ runner.os }}-
|
||||
- run: npm run check:provider-consistency
|
||||
- run: npm run check:fetch-targets
|
||||
# docs-all / openapi-routes / docs-symbols live in docs-gates (path-filtered).
|
||||
- run: npm run check:deps
|
||||
# #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
|
||||
- run: npm run check:db-rules
|
||||
- run: npm run check:known-symbols
|
||||
- run: npm run check:route-guard-membership
|
||||
- run: npm run check:test-discovery
|
||||
- run: npm run check:test-runner-api
|
||||
# Guards tap.testFiles drift: a covering unit test absent from stryker.conf.json
|
||||
# tap.testFiles makes its module's mutants survive on a cold nightly-mutation run,
|
||||
# false-failing the blocking mutationScore ratchet. See check-mutation-test-coverage.mjs.
|
||||
- run: npm run check:mutation-test-coverage
|
||||
- run: npm run check:any-budget:t11
|
||||
# Build-scope guard: fails if worktrees/cruft leak into the tsconfig include
|
||||
# scope (would OOM `next build`). Instant. See incident 2026-06-25 / #5031.
|
||||
- run: npm run check:build-scope
|
||||
# Pack-policy (unexpected-files allowlist) WITHOUT a build — catches a stray file
|
||||
# leaking into the npm tarball (v3.8.36: 6 ops bin/*.sh) per-PR instead of only on
|
||||
# the release PR's heavy Package Artifact job.
|
||||
- run: npm run check:pack-policy
|
||||
# Complexity + cognitive-complexity: ONE ESLint walk (both baselines still
|
||||
# enforced separately by ruleId). Avoids two cold tree walks on fast-path.
|
||||
- run: npm run check:complexity-ratchets
|
||||
# ── G0 (trilho .50): gates do trilho A que faltavam no trilho B ──────────────
|
||||
# The god-file refactor happens in PRs→release/**; without these, the release
|
||||
# rail never sees a new import cycle, dead code, duplication or a security
|
||||
# regression until the release PR to main. Deliberately NOT brought here:
|
||||
# bundle-size (self-skips without a build — this rail's build job is advisory
|
||||
# and uploads nothing, so it would be dead configuration) and the coverage
|
||||
# run (fast-unit already runs the full suite; the coverage ratchet stays on
|
||||
# the main rail via --allow-missing in lint-guard).
|
||||
- run: npm run check:cycles
|
||||
- run: npm run check:lockfile
|
||||
- name: Duplication ratchet
|
||||
run: npm run check:duplication
|
||||
- name: Dead-code ratchet (knip)
|
||||
run: npm run check:dead-code
|
||||
- name: Type coverage ratchet
|
||||
run: npm run check:type-coverage
|
||||
- name: Compression budget ratchet
|
||||
run: npm run check:compression-budget
|
||||
# Security scanners — same hardened install as ci.yml quality-extended
|
||||
# (gh release download = authenticated, 5000 req/hr; curl to api.github.com
|
||||
# is rate-limited to 60/hr and silently no-ops when throttled). The blocking
|
||||
@@ -194,63 +251,30 @@ jobs:
|
||||
"$HOME/.local/bin/osv-scanner" --version || true
|
||||
"$HOME/.local/bin/oasdiff" --version || true
|
||||
zizmor --version || true
|
||||
# Quality gates (all, non-fail-fast) — #8542: replaces 17 bare check:* steps,
|
||||
# 6 G0 gates, 4 ratchet gates, and 3 typecheck steps with a single aggregation
|
||||
# step. Each gate runs in a loop with ::group::; failures are collected and
|
||||
# reported at the end. set -uo pipefail (NOT set -e) so one failing gate does
|
||||
# not abort the job and mask every later gate. Release-added gates are folded
|
||||
# in: open-sse typecheck (#8781) and file-size base-relative mode (#8522).
|
||||
- name: Quality gates (all, non-fail-fast)
|
||||
- name: Secret scan (gitleaks, ratchet, blocking)
|
||||
run: npm run check:secrets -- --ratchet
|
||||
- name: Vulnerability ratchet (osv-scanner, ratchet, blocking)
|
||||
run: npm run check:vuln-ratchet -- --ratchet
|
||||
- name: Workflow lint (actionlint+zizmor, ratchet, blocking)
|
||||
run: npm run check:workflows -- --ratchet
|
||||
# BASE_REF is read by the script from the env (never interpolated into a
|
||||
# shell body) — workflow-injection-safe. actions/checkout fetches remote
|
||||
# refs, not a local branch named github.base_ref, so prefix origin/ or this
|
||||
# gate self-skips every PR with reason=base-unresolved.
|
||||
- name: OpenAPI breaking-change (oasdiff, ratchet, blocking)
|
||||
env:
|
||||
# #8522: base-relative file-size mode on PR events — inherited drift (base
|
||||
# already over frozen cap) must not red an innocent PR. Unset on
|
||||
# workflow_dispatch (no PR base) → absolute comparison.
|
||||
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
BASE_REF: ${{ github.base_ref && format('origin/{0}', github.base_ref) || '' }}
|
||||
run: |
|
||||
set -uo pipefail
|
||||
gates=(
|
||||
provider-consistency fetch-targets deps file-size error-helper
|
||||
migration-numbering public-creds db-rules known-symbols
|
||||
route-guard-membership test-discovery test-runner-api
|
||||
mutation-test-coverage any-budget:t11 build-scope pack-policy
|
||||
complexity-ratchets
|
||||
cycles lockfile duplication dead-code type-coverage compression-budget
|
||||
# #8781: open-sse workspace typecheck gate — the workspace imports @/ which
|
||||
# escapes to src/ via undeclared path aliases. See check-open-sse-typecheck.mjs.
|
||||
open-sse-typecheck
|
||||
)
|
||||
ratchet_gates=(
|
||||
secrets vuln-ratchet workflows openapi-breaking
|
||||
)
|
||||
failed=()
|
||||
for g in "${gates[@]}"; do
|
||||
echo "::group::check:$g"
|
||||
# #8522: file-size is base-relative on PR events (compare against
|
||||
# max(frozen, base)) so inherited drift doesn't red an innocent PR;
|
||||
# workflow_dispatch (no PR base) falls back to absolute comparison.
|
||||
if [ "$g" = "file-size" ] && [ -n "${PR_BASE_SHA:-}" ]; then
|
||||
npm run "check:$g" -- --base-ref "$PR_BASE_SHA" || failed+=("$g")
|
||||
else
|
||||
npm run "check:$g" || failed+=("$g")
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
done
|
||||
for g in "${ratchet_gates[@]}"; do
|
||||
echo "::group::check:$g (ratchet)"
|
||||
npm run "check:$g" -- --ratchet || failed+=("$g")
|
||||
echo "::endgroup::"
|
||||
done
|
||||
echo "::group::typecheck:core"
|
||||
npm run typecheck:core || failed+=("typecheck:core")
|
||||
echo "::endgroup::"
|
||||
echo "::group::check:dashboard-typecheck"
|
||||
npm run check:dashboard-typecheck || failed+=("check:dashboard-typecheck")
|
||||
echo "::endgroup::"
|
||||
if (( ${#failed[@]} )); then
|
||||
printf '::error::%d gate(s) failed: %s\n' "${#failed[@]}" "${failed[*]}"
|
||||
exit 1
|
||||
fi
|
||||
run: npm run check:openapi-breaking -- --ratchet
|
||||
- name: Typecheck (core)
|
||||
run: npm run typecheck:core
|
||||
# #7033: dashboard-scoped typecheck gate — src/app/(dashboard) TSX is not
|
||||
# covered by typecheck:core's curated allowlist. See check-dashboard-typecheck.mjs.
|
||||
- name: Typecheck (dashboard)
|
||||
run: npm run check:dashboard-typecheck
|
||||
# #8781: open-sse workspace typecheck gate — the workspace imports @/ which
|
||||
# escapes to src/ via undeclared path aliases. See check-open-sse-typecheck.mjs.
|
||||
- name: Typecheck (open-sse)
|
||||
run: npm run check:open-sse-typecheck
|
||||
# WS4.2 (v3.8.49 plan): TypeScript 7 native-compiler SHADOW — advisory only.
|
||||
# TS7 went GA 2026-07-08 with 8-12x type-check speedups; its Compiler API only
|
||||
# arrives in 7.1, so typescript-eslint / type-coverage / Stryker stay on 6.x
|
||||
|
||||
@@ -1033,11 +1033,7 @@ export const OmniRoutePlugin: Plugin = async (_input, options) => {
|
||||
// Config hook: keep existing catalog shim, and register slash command
|
||||
// templates that ask the agent to call the force-sync tool (OpenCode has no
|
||||
// Pi-style registerCommand API; tools + command templates are the native path).
|
||||
const baseConfigHook = createOmniRouteConfigHook(resolved, {
|
||||
cache: sharedCache,
|
||||
diskSnapshotReader: defaultDiskSnapshotReader,
|
||||
diskSnapshotWriter: defaultDiskSnapshotWriter,
|
||||
});
|
||||
const baseConfigHook = createOmniRouteConfigHook(resolved, { cache: sharedCache });
|
||||
const configWithSyncCommand = async (input: Config) => {
|
||||
await baseConfigHook(input);
|
||||
const cfg = input as Config & {
|
||||
@@ -4407,11 +4403,11 @@ export function buildStaticProviderEntry(
|
||||
entry.release_date = raw.release_date;
|
||||
}
|
||||
|
||||
// #9175: OC's `getModel` looks the model up by BARE id — the part after
|
||||
// the first `/` in the user's request — so a dict key with an embedded
|
||||
// provider prefix (`<providerId>/<raw-id>`) is unreachable. Keys are the
|
||||
// raw id verbatim; ids that already contain `/` (e.g. `cc/claude-opus-4-7`)
|
||||
// keep it because the slash is part of the upstream model id itself.
|
||||
// OC's static-catalog reader parses each key on `/` and rejects the
|
||||
// entire provider block if ANY key resolves to a parsed providerID that
|
||||
// has no corresponding provider block. So bare keys (no `/`) MUST be
|
||||
// prefixed with the resolved providerId. Already-prefixed keys
|
||||
// (e.g. `cc/claude-opus-4-7`) are left as-is to avoid double-prefixing.
|
||||
models[raw.id] = entry;
|
||||
}
|
||||
|
||||
@@ -4745,7 +4741,7 @@ export type OmniRouteDiskSnapshotWriter = (
|
||||
export type OmniRouteDiskSnapshotReader = (
|
||||
providerId: string,
|
||||
identityFingerprint: string
|
||||
) => Promise<(Omit<OmniRouteFetchCacheEntry, "expiresAt"> & { writtenAt?: number }) | undefined>;
|
||||
) => Promise<Omit<OmniRouteFetchCacheEntry, "expiresAt"> | undefined>;
|
||||
|
||||
/**
|
||||
* Bind a snapshot to the endpoint and effective credential tuple without
|
||||
@@ -4828,36 +4824,15 @@ export const defaultDiskSnapshotReader: OmniRouteDiskSnapshotReader = async (
|
||||
? parsed.rawCompressionCombos
|
||||
: [],
|
||||
rawConnections: Array.isArray(parsed.rawConnections) ? parsed.rawConnections : [],
|
||||
writtenAt: typeof parsed.writtenAt === "number" ? parsed.writtenAt : undefined,
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/** No-op disk-cache pair — used by tests to avoid filesystem side effects.
|
||||
* Also used as the default in createOmniRouteConfigHook so that tests
|
||||
* that don't pass a diskSnapshotReader don't read real snapshot files
|
||||
* from the user's ~/.local/share/opencode/plugins/ directory.
|
||||
* The OmniRoutePlugin function passes the real defaultDiskSnapshotReader
|
||||
* explicitly. */
|
||||
export const noopDiskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
/** No-op disk-cache pair — used by tests to avoid filesystem side effects. */
|
||||
export const noopDiskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
/**
|
||||
* In-flight refresh guard: prevents concurrent refreshes for the same
|
||||
* cacheKey. When a warm snapshot is served, the refresh runs detached; if
|
||||
* a second hook invocation arrives before the refresh completes, it should
|
||||
* piggyback on the in-flight promise rather than starting a second one.
|
||||
* Cleared on settle so it doesn't leak.
|
||||
*/
|
||||
const _inflightRefresh: Map<string, Promise<void>> = new Map();
|
||||
|
||||
/** Reset the in-flight refresh guard (for test isolation). */
|
||||
export function _resetInflightRefresh(): void {
|
||||
_inflightRefresh.clear();
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Debug logging (features.debugLog)
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
@@ -5092,6 +5067,7 @@ export function createDebugLoggingFetch(
|
||||
}
|
||||
};
|
||||
}
|
||||
export const noopDiskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
|
||||
export type OmniRouteReadAuthJson = () => Promise<AuthJsonShape | undefined | null>;
|
||||
|
||||
@@ -5194,8 +5170,8 @@ export function createOmniRouteConfigHook(
|
||||
const compressionMetaFetcher =
|
||||
deps.compressionMetaFetcher ?? defaultOmniRouteCompressionMetaFetcher;
|
||||
const providersFetcher = deps.providersFetcher ?? defaultOmniRouteProvidersFetcher;
|
||||
const diskSnapshotReader = deps.diskSnapshotReader ?? noopDiskSnapshotReader;
|
||||
const diskSnapshotWriter = deps.diskSnapshotWriter ?? noopDiskSnapshotWriter;
|
||||
const diskSnapshotReader = deps.diskSnapshotReader ?? defaultDiskSnapshotReader;
|
||||
const diskSnapshotWriter = deps.diskSnapshotWriter ?? defaultDiskSnapshotWriter;
|
||||
const now = deps.now ?? Date.now;
|
||||
const cache: OmniRouteFetchCache = deps.cache ?? new Map();
|
||||
const logger = deps.logger ?? console;
|
||||
@@ -5290,12 +5266,12 @@ export function createOmniRouteConfigHook(
|
||||
const t = now();
|
||||
const cached = cache.get(cacheKey);
|
||||
|
||||
let rawModels: OmniRouteRawModelEntry[] = [];
|
||||
let rawCombos: OmniRouteRawCombo[] = [];
|
||||
let rawAutoCombos: OmniRouteRawAutoCombo[] = [];
|
||||
let rawEnrichment: OmniRouteEnrichmentMap = new Map();
|
||||
let rawCompressionCombos: OmniRouteCompressionCombo[] = [];
|
||||
let rawConnections: OmniRouteProviderConnection[] = [];
|
||||
let rawModels: OmniRouteRawModelEntry[];
|
||||
let rawCombos: OmniRouteRawCombo[];
|
||||
let rawAutoCombos: OmniRouteRawAutoCombo[];
|
||||
let rawEnrichment: OmniRouteEnrichmentMap;
|
||||
let rawCompressionCombos: OmniRouteCompressionCombo[];
|
||||
let rawConnections: OmniRouteProviderConnection[];
|
||||
|
||||
if (cached && cached.expiresAt > t) {
|
||||
rawModels = cached.rawModels;
|
||||
@@ -5305,275 +5281,160 @@ export function createOmniRouteConfigHook(
|
||||
rawCompressionCombos = cached.rawCompressionCombos;
|
||||
rawConnections = cached.rawConnections;
|
||||
} else {
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Warm startup: read the disk snapshot before fetching so the provider
|
||||
// registers immediately with the last-known-good catalog. The live
|
||||
// fetch then refreshes in the background (detached) and updates the
|
||||
// cache + snapshot. Gated by features.diskCache (default-on).
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
let warmSnapshot: Omit<OmniRouteFetchCacheEntry, "expiresAt"> | undefined;
|
||||
if (wantDiskCache) {
|
||||
const snapshotResult = await diskSnapshotReader(resolved.providerId, snapshotFingerprint);
|
||||
if (snapshotResult && snapshotResult.rawModels.length > 0) {
|
||||
warmSnapshot = snapshotResult;
|
||||
// Log snapshot age (accept any age — instant beats empty).
|
||||
const age = (snapshotResult as { writtenAt?: number }).writtenAt;
|
||||
const ageLabel = typeof age === "number" ? `${Math.round((Date.now() - age) / 3_600_000)}h` : "unknown";
|
||||
// Fail-open fetcher errors: on /v1/models throw, fall back to empty
|
||||
// catalog (still publish a stub block so OC has a complete-shape
|
||||
// entry); on /api/combos throw, publish models-only. Disk-cache
|
||||
// fallback below recovers the last-known-good catalog when the
|
||||
// fetcher threw (network down / 403 / timeout) AND features.diskCache
|
||||
// !== false. A 0-entry SUCCESS (fresh tenant) does NOT trigger
|
||||
// disk fallback — that's a valid empty catalog.
|
||||
let modelsFetchThrew = false;
|
||||
try {
|
||||
rawModels = await fetcher(baseURL, apiKey, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /v1/models fetch failed; publishing stub provider entry",
|
||||
err
|
||||
);
|
||||
rawModels = [];
|
||||
modelsFetchThrew = true;
|
||||
}
|
||||
const modelsFetchOk = !modelsFetchThrew && rawModels.length > 0;
|
||||
|
||||
rawCombos = [];
|
||||
try {
|
||||
rawCombos = await combosFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /api/combos fetch failed; publishing models-only static catalog",
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
rawAutoCombos = [];
|
||||
if (wantAutoCombos) {
|
||||
try {
|
||||
rawAutoCombos = await autoCombosFetcher(baseURL, managementReadToken, 5_000);
|
||||
} catch {
|
||||
// Already handled inside the default fetcher
|
||||
}
|
||||
}
|
||||
|
||||
// Eagerly fetch enrichment so the static block can overlay human
|
||||
// display names on raw model ids. On OC ≤1.15.5 the dynamic
|
||||
// `provider.models` hook never fires in `serve` mode, so the static
|
||||
// block IS what reaches `/provider` and the TUI model picker.
|
||||
// Gated by `features.enrichment` (default-on). Soft-fail on error —
|
||||
// we still publish a name-less catalog if /api/pricing/models is
|
||||
// unreachable.
|
||||
rawEnrichment = new Map();
|
||||
if (wantEnrichment) {
|
||||
try {
|
||||
rawEnrichment = await enrichmentFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`[omniroute-plugin] config shim: warm startup from disk snapshot (${snapshotResult.rawModels.length} models, age ${ageLabel})`
|
||||
"[omniroute-plugin] config shim: /api/pricing/models fetch failed; publishing raw-id static catalog",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Parallel refresh: all six fetchers run concurrently via
|
||||
// Promise.allSettled. Each wrapper never rejects (catches internally)
|
||||
// so partial failure is tolerated — same soft-fail semantics as the
|
||||
// old sequential chain, but ~6x faster.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
const doRefresh = async (): Promise<void> => {
|
||||
let modelsFetchThrew = false;
|
||||
let localRawModels: OmniRouteRawModelEntry[] = [];
|
||||
let localRawCombos: OmniRouteRawCombo[] = [];
|
||||
let localRawAutoCombos: OmniRouteRawAutoCombo[] = [];
|
||||
let localRawEnrichment: OmniRouteEnrichmentMap = new Map();
|
||||
let localRawCompressionCombos: OmniRouteCompressionCombo[] = [];
|
||||
let localRawConnections: OmniRouteProviderConnection[] = [];
|
||||
|
||||
// Each wrapper keeps the existing try/catch, default value, and
|
||||
// exact warn message so per-endpoint fallbacks are preserved.
|
||||
const doModels = async (): Promise<void> => {
|
||||
try {
|
||||
localRawModels = await fetcher(baseURL, apiKey, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /v1/models fetch failed; publishing stub provider entry",
|
||||
err
|
||||
);
|
||||
localRawModels = [];
|
||||
modelsFetchThrew = true;
|
||||
}
|
||||
};
|
||||
|
||||
const doCombos = async (): Promise<void> => {
|
||||
try {
|
||||
localRawCombos = await combosFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /api/combos fetch failed; publishing models-only static catalog",
|
||||
err
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const doAutoCombos = async (): Promise<void> => {
|
||||
if (!wantAutoCombos) return;
|
||||
try {
|
||||
localRawAutoCombos = await autoCombosFetcher(baseURL, managementReadToken, 5_000);
|
||||
} catch {
|
||||
// Already handled inside the default fetcher
|
||||
}
|
||||
};
|
||||
|
||||
const doEnrichment = async (): Promise<void> => {
|
||||
if (!wantEnrichment) return;
|
||||
try {
|
||||
localRawEnrichment = await enrichmentFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /api/pricing/models fetch failed; publishing raw-id static catalog",
|
||||
err
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const doCompression = async (): Promise<void> => {
|
||||
if (!wantCompressionMeta) return;
|
||||
try {
|
||||
localRawCompressionCombos = await compressionMetaFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /api/context/combos fetch failed; publishing combos without compression suffix",
|
||||
err
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const doConnections = async (): Promise<void> => {
|
||||
if (!wantUsableOnly) return;
|
||||
try {
|
||||
localRawConnections = await providersFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /api/providers fetch failed; usableOnly filter disabled for this refresh",
|
||||
err
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.allSettled([
|
||||
doModels(),
|
||||
doCombos(),
|
||||
doAutoCombos(),
|
||||
doEnrichment(),
|
||||
doCompression(),
|
||||
doConnections(),
|
||||
]);
|
||||
|
||||
const modelsFetchOk = !modelsFetchThrew && localRawModels.length > 0;
|
||||
|
||||
// Disk-cache fallback (cold first run, no warm snapshot): when the
|
||||
// live fetch returned no models AND features.diskCache !== false,
|
||||
// hydrate from the last-known-good snapshot so OC still surfaces a
|
||||
// usable catalog (e.g. IP whitelist drop, offline laptop).
|
||||
if (modelsFetchThrew && wantDiskCache && !warmSnapshot) {
|
||||
const snapshot = await diskSnapshotReader(resolved.providerId, snapshotFingerprint);
|
||||
if (snapshot && snapshot.rawModels.length > 0) {
|
||||
logger.warn(
|
||||
`[omniroute-plugin] config shim: /v1/models unreachable; using stale disk cache (${snapshot.rawModels.length} models)`
|
||||
);
|
||||
localRawModels = snapshot.rawModels;
|
||||
localRawCombos = snapshot.rawCombos;
|
||||
localRawAutoCombos = snapshot.rawAutoCombos ?? [];
|
||||
localRawEnrichment = snapshot.rawEnrichment;
|
||||
localRawCompressionCombos = snapshot.rawCompressionCombos;
|
||||
localRawConnections = snapshot.rawConnections;
|
||||
}
|
||||
// Compression-metadata fetch — opt-in via features.compressionMetadata.
|
||||
// When on, the default pipeline is appended to every combo `name` so
|
||||
// the TUI picker advertises which compression a combo applies.
|
||||
rawCompressionCombos = [];
|
||||
if (wantCompressionMeta) {
|
||||
try {
|
||||
rawCompressionCombos = await compressionMetaFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /api/context/combos fetch failed; publishing combos without compression suffix",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Cache even partial results — a subsequent provider-hook call should
|
||||
// not re-burn the timeout window on the same broken endpoint.
|
||||
cache.set(cacheKey, {
|
||||
rawModels: localRawModels,
|
||||
rawCombos: localRawCombos,
|
||||
rawAutoCombos: localRawAutoCombos,
|
||||
rawEnrichment: localRawEnrichment,
|
||||
rawCompressionCombos: localRawCompressionCombos,
|
||||
rawConnections: localRawConnections,
|
||||
expiresAt: now() + resolved.modelCacheTtl,
|
||||
// Provider-connections fetch — opt-in via features.usableOnly. When
|
||||
// on, the static catalog filters out models/combos whose canonical
|
||||
// provider has no active connection. Soft-fail (empty list) disables
|
||||
// the filter for this refresh, never hiding the whole catalog.
|
||||
rawConnections = [];
|
||||
if (wantUsableOnly) {
|
||||
try {
|
||||
rawConnections = await providersFetcher(baseURL, managementReadToken, 10_000);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"[omniroute-plugin] config shim: /api/providers fetch failed; usableOnly filter disabled for this refresh",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Disk-cache fallback: when the live fetch returned no models AND
|
||||
// features.diskCache !== false, hydrate from the last-known-good
|
||||
// snapshot so OC still surfaces a usable catalog (e.g. IP whitelist
|
||||
// drop, offline laptop). The snapshot is whatever we last wrote on
|
||||
// a healthy refresh; staleness is bounded only by how recently the
|
||||
// user was online.
|
||||
if (modelsFetchThrew && wantDiskCache) {
|
||||
const snapshot = await diskSnapshotReader(resolved.providerId, snapshotFingerprint);
|
||||
if (snapshot && snapshot.rawModels.length > 0) {
|
||||
logger.warn(
|
||||
`[omniroute-plugin] config shim: /v1/models unreachable; using stale disk cache (${snapshot.rawModels.length} models)`
|
||||
);
|
||||
rawModels = snapshot.rawModels;
|
||||
rawCombos = snapshot.rawCombos;
|
||||
rawAutoCombos = snapshot.rawAutoCombos ?? [];
|
||||
rawEnrichment = snapshot.rawEnrichment;
|
||||
rawCompressionCombos = snapshot.rawCompressionCombos;
|
||||
rawConnections = snapshot.rawConnections;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache even partial results — a subsequent provider-hook call should
|
||||
// not re-burn the timeout window on the same broken endpoint.
|
||||
cache.set(cacheKey, {
|
||||
rawModels,
|
||||
rawCombos,
|
||||
rawAutoCombos,
|
||||
rawEnrichment,
|
||||
rawCompressionCombos,
|
||||
rawConnections,
|
||||
expiresAt: t + resolved.modelCacheTtl,
|
||||
});
|
||||
|
||||
// Startup diagnostics (file-based) — fires at startup via config hook
|
||||
if (resolved.features?.startupDebug === true) {
|
||||
await writeStartupDiagnostics({
|
||||
providerId: resolved.providerId,
|
||||
baseURL,
|
||||
modelCount: rawModels.length,
|
||||
comboCount: rawCombos.length,
|
||||
enrichmentSize: rawEnrichment.size,
|
||||
autoComboCount: rawAutoCombos.length,
|
||||
enrichment: rawEnrichment,
|
||||
autoCombos: rawAutoCombos,
|
||||
features: resolved.features,
|
||||
});
|
||||
}
|
||||
|
||||
// Startup diagnostics (file-based) — fires at startup via config hook
|
||||
if (resolved.features?.startupDebug === true) {
|
||||
await writeStartupDiagnostics({
|
||||
providerId: resolved.providerId,
|
||||
baseURL,
|
||||
modelCount: localRawModels.length,
|
||||
comboCount: localRawCombos.length,
|
||||
enrichmentSize: localRawEnrichment.size,
|
||||
autoComboCount: localRawAutoCombos.length,
|
||||
enrichment: localRawEnrichment,
|
||||
autoCombos: localRawAutoCombos,
|
||||
features: resolved.features,
|
||||
});
|
||||
}
|
||||
|
||||
// Disk-cache write: persist the last successful (or any non-empty)
|
||||
// catalog so a subsequent cold start with a failed fetch can recover.
|
||||
// Best-effort; soft-fail keeps us moving when the data dir isn't
|
||||
// writable (e.g. read-only container). A failed refresh never
|
||||
// overwrites the snapshot (modelsFetchOk gate).
|
||||
if (modelsFetchOk && wantDiskCache) {
|
||||
await diskSnapshotWriter(
|
||||
resolved.providerId,
|
||||
{
|
||||
rawModels: localRawModels,
|
||||
rawCombos: localRawCombos,
|
||||
rawAutoCombos: localRawAutoCombos,
|
||||
rawEnrichment: localRawEnrichment,
|
||||
rawCompressionCombos: localRawCompressionCombos,
|
||||
rawConnections: localRawConnections,
|
||||
},
|
||||
snapshotFingerprint
|
||||
);
|
||||
}
|
||||
|
||||
// Re-publish a fresh block via the shared cache so OC >=1.14.49's
|
||||
// dynamic provider hook picks it up from the cache. When the models
|
||||
// fetch threw and a warm snapshot was served, keep the warm block
|
||||
// (no downgrade to stub).
|
||||
if (modelsFetchOk || !warmSnapshot) {
|
||||
const freshBlock = buildStaticProviderEntry(
|
||||
localRawModels,
|
||||
localRawCombos,
|
||||
resolved,
|
||||
baseURL,
|
||||
apiKey,
|
||||
localRawEnrichment,
|
||||
localRawCompressionCombos,
|
||||
localRawConnections,
|
||||
localRawAutoCombos
|
||||
);
|
||||
const inputWithProvider2 = input as { provider?: Record<string, unknown> };
|
||||
if (inputWithProvider2.provider) {
|
||||
inputWithProvider2.provider[resolved.providerId] = freshBlock;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (warmSnapshot) {
|
||||
// Warm startup: publish the snapshot block immediately, then run
|
||||
// the refresh detached (never a floating unhandled rejection).
|
||||
rawModels = warmSnapshot.rawModels;
|
||||
rawCombos = warmSnapshot.rawCombos;
|
||||
rawAutoCombos = warmSnapshot.rawAutoCombos ?? [];
|
||||
rawEnrichment = warmSnapshot.rawEnrichment;
|
||||
rawCompressionCombos = warmSnapshot.rawCompressionCombos;
|
||||
rawConnections = warmSnapshot.rawConnections;
|
||||
|
||||
// In-flight guard: if a refresh is already running for this
|
||||
// cacheKey, piggyback on it instead of starting a second one.
|
||||
const existing = _inflightRefresh.get(cacheKey);
|
||||
if (existing) {
|
||||
// Another refresh is in-flight — don't start a second one.
|
||||
// The existing refresh will update the cache when it completes.
|
||||
} else {
|
||||
const refreshP = doRefresh()
|
||||
.catch((err: unknown) => {
|
||||
logger.warn("[omniroute-plugin] config shim: background refresh failed", err);
|
||||
})
|
||||
.finally(() => {
|
||||
_inflightRefresh.delete(cacheKey);
|
||||
});
|
||||
_inflightRefresh.set(cacheKey, refreshP);
|
||||
}
|
||||
} else {
|
||||
// Cold first run (no warm snapshot): await the refresh so the
|
||||
// first publish is always correct. In-flight guard still applies.
|
||||
const existing = _inflightRefresh.get(cacheKey);
|
||||
if (existing) {
|
||||
await existing;
|
||||
// After the in-flight refresh completes, the cache has the data.
|
||||
const fresh = cache.get(cacheKey);
|
||||
if (fresh) {
|
||||
rawModels = fresh.rawModels;
|
||||
rawCombos = fresh.rawCombos;
|
||||
rawAutoCombos = fresh.rawAutoCombos;
|
||||
rawEnrichment = fresh.rawEnrichment;
|
||||
rawCompressionCombos = fresh.rawCompressionCombos;
|
||||
rawConnections = fresh.rawConnections;
|
||||
}
|
||||
} else {
|
||||
const refreshP = doRefresh()
|
||||
.catch((err: unknown) => {
|
||||
logger.warn("[omniroute-plugin] config shim: refresh failed", err);
|
||||
})
|
||||
.finally(() => {
|
||||
_inflightRefresh.delete(cacheKey);
|
||||
});
|
||||
_inflightRefresh.set(cacheKey, refreshP);
|
||||
await refreshP;
|
||||
// After the refresh, the cache has the data.
|
||||
const fresh = cache.get(cacheKey);
|
||||
if (fresh) {
|
||||
rawModels = fresh.rawModels;
|
||||
rawCombos = fresh.rawCombos;
|
||||
rawAutoCombos = fresh.rawAutoCombos;
|
||||
rawEnrichment = fresh.rawEnrichment;
|
||||
rawCompressionCombos = fresh.rawCompressionCombos;
|
||||
rawConnections = fresh.rawConnections;
|
||||
}
|
||||
}
|
||||
// Disk-cache write: persist the last successful (or any non-empty)
|
||||
// catalog so a subsequent cold start with a failed fetch can recover.
|
||||
// Best-effort; soft-fail keeps us moving when the data dir isn't
|
||||
// writable (e.g. read-only container).
|
||||
if (modelsFetchOk && wantDiskCache) {
|
||||
await diskSnapshotWriter(
|
||||
resolved.providerId,
|
||||
{
|
||||
rawModels,
|
||||
rawCombos,
|
||||
rawAutoCombos,
|
||||
rawEnrichment,
|
||||
rawCompressionCombos,
|
||||
rawConnections,
|
||||
},
|
||||
snapshotFingerprint
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
createOmniRouteProviderHook,
|
||||
OmniRoutePlugin,
|
||||
resolveOmniRoutePluginOptions,
|
||||
_resetInflightRefresh,
|
||||
type OmniRouteCombosFetcher,
|
||||
type OmniRouteEnrichmentEntry,
|
||||
type OmniRouteEnrichmentFetcher,
|
||||
@@ -48,16 +47,6 @@ import {
|
||||
type OmniRouteStaticProviderEntry,
|
||||
} from "../src/index.js";
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Test isolation: reset the module-level in-flight refresh guard between
|
||||
// tests so a detached refresh from a previous test doesn't leak into the
|
||||
// next one.
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test.beforeEach(() => {
|
||||
_resetInflightRefresh();
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Fixtures
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
@@ -238,7 +227,7 @@ test("config: with valid auth.json + apiKey + baseURL → mutates input.provider
|
||||
// Stripped per-model shape: name + cap flags + modalities + (optional)
|
||||
// cost. OC's SDK static schema accepts only `limit.{context,output}` —
|
||||
// `limit.input` is NOT in the SDK shape and gets dropped silently.
|
||||
const claude = entry.models["claude-sonnet-4-6"];
|
||||
const claude = entry.models["opencode-omniroute/claude-sonnet-4-6"];
|
||||
assert.ok(claude, "claude model surfaced");
|
||||
assert.equal(claude.name, "claude-sonnet-4-6");
|
||||
assert.equal(claude.attachment, true);
|
||||
@@ -259,7 +248,7 @@ test("config: with valid auth.json + apiKey + baseURL → mutates input.provider
|
||||
|
||||
// Combo surfaces under bare key + LCD'd
|
||||
// (gemini's reasoning=false → combo reasoning=false).
|
||||
const combo = entry.models["claude-tier"];
|
||||
const combo = entry.models["omniroute/claude-tier"];
|
||||
assert.ok(combo, "combo surfaced under bare key");
|
||||
assert.equal(combo.name, "Claude Tier");
|
||||
assert.equal(combo.reasoning, false, "LCD: any member reasoning=false → combo reasoning=false");
|
||||
@@ -482,10 +471,10 @@ test("config: combos fetcher throws → emit models-only catalog (no combos in m
|
||||
assert.ok(entry);
|
||||
const ids = Object.keys(entry.models).sort();
|
||||
assert.deepEqual(ids, [
|
||||
"claude-sonnet-4-6",
|
||||
"gemini-3-flash",
|
||||
"opencode-omniroute/claude-sonnet-4-6",
|
||||
"opencode-omniroute/gemini-3-flash",
|
||||
]);
|
||||
assert.equal(entry.models["claude-tier"], undefined, "no combo entry");
|
||||
assert.equal(entry.models["omniroute/claude-tier"], undefined, "no combo entry");
|
||||
assert.ok(
|
||||
logger.entries.some((e) => String(e[0]).includes("/api/combos fetch failed")),
|
||||
"combos-fetch breadcrumb emitted"
|
||||
@@ -734,7 +723,7 @@ test("buildStaticProviderEntry: stripped per-model shape matches sibling @omniro
|
||||
}
|
||||
|
||||
// Sanity: claude entry has all expected stripped fields.
|
||||
const claude = block.models["claude-sonnet-4-6"];
|
||||
const claude = block.models["opencode-omniroute/claude-sonnet-4-6"];
|
||||
assert.equal(typeof claude.name, "string");
|
||||
assert.equal(typeof claude.attachment, "boolean");
|
||||
assert.equal(typeof claude.reasoning, "boolean");
|
||||
@@ -759,8 +748,8 @@ test("buildStaticProviderEntry: hidden combos are excluded", () => {
|
||||
"https://or.example/v1",
|
||||
"sk-test"
|
||||
);
|
||||
assert.equal(block.models["claude-tier"], undefined);
|
||||
assert.ok(block.models["claude-sonnet-4-6"]);
|
||||
assert.equal(block.models["omniroute/claude-tier"], undefined);
|
||||
assert.ok(block.models["opencode-omniroute/claude-sonnet-4-6"]);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
@@ -776,7 +765,7 @@ test("buildStaticProviderEntry: emits modalities.input from raw.input_modalities
|
||||
"https://or.example/v1",
|
||||
"sk-test"
|
||||
);
|
||||
const claude = block.models["claude-sonnet-4-6"];
|
||||
const claude = block.models["opencode-omniroute/claude-sonnet-4-6"];
|
||||
assert.deepEqual(claude.modalities?.input, ["text", "image"]);
|
||||
assert.deepEqual(claude.modalities?.output, ["text"]);
|
||||
});
|
||||
@@ -790,7 +779,7 @@ test("buildStaticProviderEntry: never emits limit.input (OC SDK rejects it)", ()
|
||||
"https://or.example/v1",
|
||||
"sk-test"
|
||||
);
|
||||
const claude = block.models["claude-sonnet-4-6"];
|
||||
const claude = block.models["opencode-omniroute/claude-sonnet-4-6"];
|
||||
assert.equal((claude.limit as Record<string, unknown>).input, undefined);
|
||||
assert.equal(typeof claude.limit?.context, "number");
|
||||
assert.equal(typeof claude.limit?.output, "number");
|
||||
@@ -818,7 +807,7 @@ test("buildStaticProviderEntry: emits cost when enrichment carries pricing", ()
|
||||
"sk-test",
|
||||
enrichment
|
||||
);
|
||||
const claude = block.models["claude-sonnet-4-6"];
|
||||
const claude = block.models["opencode-omniroute/claude-sonnet-4-6"];
|
||||
assert.equal(claude.cost?.input, 3);
|
||||
assert.equal(claude.cost?.output, 15);
|
||||
assert.equal(claude.cost?.cache_read, 0.3);
|
||||
@@ -839,8 +828,8 @@ test("buildStaticProviderEntry: emits release_date when raw carries it; omits wh
|
||||
"https://or.example/v1",
|
||||
"sk-test"
|
||||
);
|
||||
assert.equal(block.models["claude-with-date"].release_date, "2026-02-19");
|
||||
assert.equal(block.models["gemini-3-flash"].release_date, undefined);
|
||||
assert.equal(block.models["opencode-omniroute/claude-with-date"].release_date, "2026-02-19");
|
||||
assert.equal(block.models["opencode-omniroute/gemini-3-flash"].release_date, undefined);
|
||||
});
|
||||
|
||||
test("buildStaticProviderEntry: combo modalities = intersection of members (LCD)", () => {
|
||||
@@ -869,7 +858,7 @@ test("buildStaticProviderEntry: combo modalities = intersection of members (LCD)
|
||||
"https://or.example/v1",
|
||||
"sk-test"
|
||||
);
|
||||
const combo = block.models["mixed-tier"];
|
||||
const combo = block.models["omniroute/mixed-tier"];
|
||||
assert.ok(combo, "combo emitted under slug key");
|
||||
// claude has text+image, text-only has text → intersection drops image.
|
||||
assert.deepEqual(combo.modalities?.input, ["text"]);
|
||||
@@ -978,10 +967,10 @@ test("config: enrichment fetched + name overlaid on raw-model entries", async ()
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry);
|
||||
assert.equal(entry.models["claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
|
||||
assert.equal(entry.models["gemini-3-flash"].name, "Gemini 3 Flash");
|
||||
assert.equal(entry.models["opencode-omniroute/claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
|
||||
assert.equal(entry.models["opencode-omniroute/gemini-3-flash"].name, "Gemini 3 Flash");
|
||||
// Combo names still come from /api/combos — enrichment overlay does NOT touch combos.
|
||||
assert.equal(entry.models["claude-tier"].name, "Claude Tier");
|
||||
assert.equal(entry.models["omniroute/claude-tier"].name, "Claude Tier");
|
||||
assert.equal(enrichmentFetcher.callCount(), 1);
|
||||
});
|
||||
|
||||
@@ -1011,7 +1000,7 @@ test("config: features.enrichment=false skips enrichment fetch + keeps raw-id na
|
||||
assert.ok(entry);
|
||||
assert.equal(enrichmentFetcher.callCount(), 0, "enrichment fetch suppressed by feature flag");
|
||||
assert.equal(
|
||||
entry.models["claude-sonnet-4-6"].name,
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
|
||||
"claude-sonnet-4-6",
|
||||
"raw id retained"
|
||||
);
|
||||
@@ -1038,7 +1027,7 @@ test("config: enrichment fetcher throws → soft-fail (warn + raw-id static cata
|
||||
];
|
||||
assert.ok(entry, "static block still published on enrichment failure");
|
||||
assert.equal(
|
||||
entry.models["claude-sonnet-4-6"].name,
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
|
||||
"claude-sonnet-4-6",
|
||||
"raw id retained"
|
||||
);
|
||||
@@ -1240,20 +1229,17 @@ test("config: diskCache hydrates stale snapshot when /v1/models throws", async (
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(
|
||||
entry.models["claude-sonnet-4-6"],
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||
"stale snapshot hydrated into static block"
|
||||
);
|
||||
assert.equal(
|
||||
entry.models["claude-sonnet-4-6"].name,
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
|
||||
"Claude Sonnet 4.6 (cached)",
|
||||
"stale enrichment also reused"
|
||||
);
|
||||
assert.equal(writes, 0, "disk write skipped when live fetch failed");
|
||||
assert.ok(
|
||||
logger.entries.some((e) =>
|
||||
String(e[0]).includes("using stale disk cache") ||
|
||||
String(e[0]).includes("warm startup from disk snapshot")
|
||||
),
|
||||
logger.entries.some((e) => String(e[0]).includes("using stale disk cache")),
|
||||
"disk-cache hydration breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
@@ -1295,7 +1281,7 @@ test("config: cached rawEnrichment from earlier provider hook is reused (no refe
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.equal(entry.models["claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
|
||||
assert.equal(entry.models["opencode-omniroute/claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
@@ -1346,12 +1332,12 @@ test("config: providerTag (default-on) prepends '<provider> - ' to enriched raw-
|
||||
];
|
||||
assert.ok(entry);
|
||||
assert.equal(
|
||||
entry.models["claude-sonnet-4-6"].name,
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
|
||||
"Claude - Claude Sonnet 4.6"
|
||||
);
|
||||
assert.equal(entry.models["gemini-3-flash"].name, "Gemini - Gemini 3 Flash");
|
||||
assert.equal(entry.models["opencode-omniroute/gemini-3-flash"].name, "Gemini - Gemini 3 Flash");
|
||||
// Combos stay untouched — `Combo: ` prefix already conveys multi-upstream.
|
||||
assert.equal(entry.models["claude-tier"].name, "Claude Tier");
|
||||
assert.equal(entry.models["omniroute/claude-tier"].name, "Claude Tier");
|
||||
});
|
||||
|
||||
test("config: providerTag=false suppresses the suffix", async () => {
|
||||
@@ -1378,7 +1364,7 @@ test("config: providerTag=false suppresses the suffix", async () => {
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.equal(
|
||||
entry.models["claude-sonnet-4-6"].name,
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
|
||||
"Claude Sonnet 4.6",
|
||||
"enriched name kept, provider tag suppressed"
|
||||
);
|
||||
@@ -1410,7 +1396,7 @@ test("config: providerTag falls back to UPPER(alias) when providerDisplayName mi
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.equal(entry.models["claude-sonnet-4-6"].name, "CC - Claude Sonnet 4.6");
|
||||
assert.equal(entry.models["opencode-omniroute/claude-sonnet-4-6"].name, "CC - Claude Sonnet 4.6");
|
||||
});
|
||||
|
||||
test("config: providerTag skipped entirely when neither providerDisplayName nor providerAlias set", async () => {
|
||||
@@ -1437,7 +1423,7 @@ test("config: providerTag skipped entirely when neither providerDisplayName nor
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.equal(entry.models["claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
|
||||
assert.equal(entry.models["opencode-omniroute/claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
|
||||
});
|
||||
|
||||
test("config: providerTag is idempotent — second hook call doesn't double-suffix", async () => {
|
||||
@@ -1465,7 +1451,7 @@ test("config: providerTag is idempotent — second hook call doesn't double-suff
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.equal(
|
||||
entryA.models["claude-sonnet-4-6"].name,
|
||||
entryA.models["opencode-omniroute/claude-sonnet-4-6"].name,
|
||||
"Claude - Claude Sonnet 4.6"
|
||||
);
|
||||
|
||||
@@ -1476,7 +1462,7 @@ test("config: providerTag is idempotent — second hook call doesn't double-suff
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.equal(
|
||||
entryB.models["claude-sonnet-4-6"].name,
|
||||
entryB.models["opencode-omniroute/claude-sonnet-4-6"].name,
|
||||
"Claude - Claude Sonnet 4.6"
|
||||
);
|
||||
});
|
||||
@@ -1530,7 +1516,7 @@ test("buildStaticProviderEntry: nested combo-ref context is the bottleneck acros
|
||||
);
|
||||
// Pre-fix: Parent would advertise 200_000 (only raw-big counted).
|
||||
// Post-fix: Parent should advertise 8_000 (TinyCombo bottleneck).
|
||||
const parent = block.models["parent"];
|
||||
const parent = block.models["omniroute/parent"];
|
||||
assert.ok(parent, "Parent combo must be in the static catalog");
|
||||
assert.equal(parent.limit?.context, 8_000);
|
||||
});
|
||||
|
||||
@@ -111,9 +111,7 @@ test("#6859: createOmniRouteProviderHook end-to-end — catalog keys/providerID
|
||||
// `opencode-omniroute`. Confirmed against the issue's own curl repro
|
||||
// (`model: "opencode-omniroute/hermes-smart-stack"` → "No active
|
||||
// credentials for provider: opencode-omniroute").
|
||||
// #9175 tightened this further: OC's `getModel` looks models up by BARE id,
|
||||
// so combo dict keys now carry NO prefix at all (not even `omniroute/`).
|
||||
test("#7976/#9175: buildStaticProviderEntry keys combos by bare slug (no prefix at all — never the OC-gate providerId)", () => {
|
||||
test("#7976: buildStaticProviderEntry keys bare-slug combo ids with the unprefixed omnirouteProviderId (no double OC-gate prefix)", () => {
|
||||
const resolved = resolveOmniRoutePluginOptions({ providerId: "omniroute" });
|
||||
assert.equal(resolved.providerId, "opencode-omniroute");
|
||||
assert.equal(resolved.omnirouteProviderId, "omniroute");
|
||||
@@ -133,7 +131,7 @@ test("#7976/#9175: buildStaticProviderEntry keys combos by bare slug (no prefix
|
||||
"sk-test"
|
||||
);
|
||||
|
||||
assert.deepEqual(Object.keys(block.models), ["hermes-smart-stack"]);
|
||||
assert.deepEqual(Object.keys(block.models), ["omniroute/hermes-smart-stack"]);
|
||||
assert.equal(
|
||||
block.models["opencode-omniroute/hermes-smart-stack"],
|
||||
undefined,
|
||||
|
||||
@@ -1,827 +0,0 @@
|
||||
/**
|
||||
* Warm-startup + parallel-refresh tests for the opencode-plugin config shim.
|
||||
*
|
||||
* Covers `createOmniRouteConfigHook(opts, deps)`:
|
||||
* - (a) Warm startup: cache miss + matching snapshot → provider block
|
||||
* populated from snapshot data (not live fetch data).
|
||||
* - (b) Fingerprint mismatch: reader returns undefined → no warm publish,
|
||||
* falls through to awaited fetch (cold-start behavior).
|
||||
* - (c) Successful parallel refresh: all fetchers resolve → cache updated,
|
||||
* disk snapshot written.
|
||||
* - (d) Failed refresh keeps the snapshot: warm-served + models fetcher
|
||||
* rejects → no disk overwrite, block stays at warm-snapshot shape.
|
||||
* - (e) Parallelism: all six fetchers start concurrently (not sequential).
|
||||
* - (f) Soft-fail parity under Promise.allSettled: per-endpoint
|
||||
* fallbacks + logger.warn breadcrumbs preserved.
|
||||
* - (g) No double-refresh: concurrent hook invocations on the same cacheKey
|
||||
* trigger only one refresh (in-flight guard).
|
||||
* - (h) features.diskCache: false disables the warm read entirely.
|
||||
*
|
||||
* Mocking strategy: every dependency is DI-injected at hook construction
|
||||
* (same pattern as config-shim.test.ts). No global monkey-patching.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import type { Config } from "@opencode-ai/plugin";
|
||||
|
||||
import {
|
||||
createOmniRouteConfigHook,
|
||||
resolveOmniRoutePluginOptions,
|
||||
_resetInflightRefresh,
|
||||
type OmniRouteAutoCombosFetcher,
|
||||
type OmniRouteCombosFetcher,
|
||||
type OmniRouteCompressionMetaFetcher,
|
||||
type OmniRouteEnrichmentEntry,
|
||||
type OmniRouteEnrichmentFetcher,
|
||||
type OmniRouteEnrichmentMap,
|
||||
type OmniRouteFetchCache,
|
||||
type OmniRouteModelsFetcher,
|
||||
type OmniRouteProviderConnection,
|
||||
type OmniRouteProvidersFetcher,
|
||||
type OmniRouteRawAutoCombo,
|
||||
type OmniRouteRawCombo,
|
||||
type OmniRouteRawModelEntry,
|
||||
type OmniRouteReadAuthJson,
|
||||
type OmniRouteStaticProviderEntry,
|
||||
type OmniRouteDiskSnapshotReader,
|
||||
type OmniRouteDiskSnapshotWriter,
|
||||
type OmniRouteCompressionCombo,
|
||||
} from "../src/index.js";
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Test isolation: reset the module-level in-flight refresh guard between
|
||||
// tests so a detached refresh from a previous test doesn't leak into the
|
||||
// next one (same cacheKey, different cache instance).
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test.beforeEach(() => {
|
||||
_resetInflightRefresh();
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Fixtures
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const MODEL_CLAUDE: OmniRouteRawModelEntry = {
|
||||
id: "claude-sonnet-4-6",
|
||||
capabilities: {
|
||||
tool_calling: true,
|
||||
reasoning: true,
|
||||
vision: true,
|
||||
thinking: false,
|
||||
temperature: true,
|
||||
},
|
||||
context_length: 200_000,
|
||||
max_output_tokens: 64_000,
|
||||
max_input_tokens: 180_000,
|
||||
input_modalities: ["text", "image"],
|
||||
output_modalities: ["text"],
|
||||
};
|
||||
|
||||
const MODEL_GEMINI: OmniRouteRawModelEntry = {
|
||||
id: "gemini-3-flash",
|
||||
capabilities: { tool_calling: true, reasoning: false, vision: true, thinking: false },
|
||||
context_length: 1_000_000,
|
||||
max_output_tokens: 8_192,
|
||||
input_modalities: ["text", "image"],
|
||||
output_modalities: ["text"],
|
||||
};
|
||||
|
||||
const COMBO_CLAUDE_TIER: OmniRouteRawCombo = {
|
||||
id: "combo-claude-tier",
|
||||
name: "Claude Tier",
|
||||
models: [
|
||||
{ id: "s1", kind: "model", model: "claude-sonnet-4-6", weight: 100 },
|
||||
{ id: "s2", kind: "model", model: "gemini-3-flash", weight: 50 },
|
||||
],
|
||||
};
|
||||
|
||||
const AUTO_COMBO: OmniRouteRawAutoCombo = {
|
||||
id: "auto",
|
||||
name: "Auto",
|
||||
};
|
||||
|
||||
const COMPRESSION_COMBO: OmniRouteCompressionCombo = {
|
||||
id: "ctx-combo-1",
|
||||
name: "Context Combo",
|
||||
pipeline: "gzip",
|
||||
};
|
||||
|
||||
const CONNECTION_CLAUDE: OmniRouteProviderConnection = {
|
||||
id: "c1",
|
||||
provider: "claude",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
};
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// DI stub helpers
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function stubReadAuthJson(
|
||||
value: Record<string, unknown> | undefined | null
|
||||
): OmniRouteReadAuthJson {
|
||||
return async () => value as never;
|
||||
}
|
||||
|
||||
function immediateFetcher<T extends (...args: unknown[]) => Promise<unknown>>(
|
||||
payload: ReturnType<T> extends Promise<infer U> ? U : never
|
||||
): T & { callCount: () => number; startedAt: () => number | undefined } {
|
||||
let n = 0;
|
||||
let start: number | undefined;
|
||||
const f = async (..._args: unknown[]) => {
|
||||
start = Date.now();
|
||||
n++;
|
||||
return payload;
|
||||
};
|
||||
return Object.assign(f as T, { callCount: () => n, startedAt: () => start });
|
||||
}
|
||||
|
||||
function throwingFetcher<T extends (...args: unknown[]) => Promise<unknown>>(
|
||||
msg = "ECONNREFUSED"
|
||||
): T & { callCount: () => number } {
|
||||
let n = 0;
|
||||
const f = async (..._args: unknown[]) => {
|
||||
n++;
|
||||
throw new Error(msg);
|
||||
};
|
||||
return Object.assign(f as T, { callCount: () => n });
|
||||
}
|
||||
|
||||
interface WarnCapture {
|
||||
warn: (...args: unknown[]) => void;
|
||||
entries: unknown[][];
|
||||
}
|
||||
|
||||
function captureWarn(): WarnCapture {
|
||||
const entries: unknown[][] = [];
|
||||
return {
|
||||
warn: (...args: unknown[]) => {
|
||||
entries.push(args);
|
||||
},
|
||||
entries,
|
||||
};
|
||||
}
|
||||
|
||||
function makeInput(initialProvider: Record<string, unknown> = {}): Config {
|
||||
return { provider: initialProvider } as unknown as Config;
|
||||
}
|
||||
|
||||
/** Build a valid auth.json stub for the default providerId. */
|
||||
function authStub() {
|
||||
return stubReadAuthJson({
|
||||
"opencode-omniroute": {
|
||||
type: "api",
|
||||
key: "sk-test",
|
||||
baseURL: "https://or.example.com/v1",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (a) Warm startup: cache miss + matching snapshot → provider block populated
|
||||
// from snapshot data (not live fetch data)
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: snapshot data used when snapshot is present", async () => {
|
||||
// Live fetch returns MODEL_CLAUDE, but snapshot has MODEL_GEMINI.
|
||||
// With warm startup, the block should contain the snapshot data.
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const autoCombosFetcher = immediateFetcher<OmniRouteAutoCombosFetcher>([]);
|
||||
const enrichmentFetcher = immediateFetcher<OmniRouteEnrichmentFetcher>(new Map());
|
||||
const compressionMetaFetcher = immediateFetcher<OmniRouteCompressionMetaFetcher>([]);
|
||||
const providersFetcher = immediateFetcher<OmniRouteProvidersFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> = {
|
||||
rawModels: [MODEL_GEMINI],
|
||||
rawCombos: [],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
};
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
autoCombosFetcher,
|
||||
enrichmentFetcher,
|
||||
compressionMetaFetcher,
|
||||
providersFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const provider = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider;
|
||||
const entry = provider["opencode-omniroute"];
|
||||
assert.ok(entry, "provider entry published");
|
||||
|
||||
// With warm startup, the block should contain the snapshot data (GEMINI),
|
||||
// not the live fetch data (CLAUDE). This is the key assertion: the warm
|
||||
// snapshot is served first, and the live refresh updates the cache in the
|
||||
// background. On the next hook invocation, the cache will have the fresh data.
|
||||
const hasGemini = entry.models["opencode-omniroute/gemini-3-flash"] !== undefined;
|
||||
const hasClaude = entry.models["opencode-omniroute/claude-sonnet-4-6"] !== undefined;
|
||||
assert.ok(
|
||||
hasGemini || hasClaude,
|
||||
"provider block has at least one model"
|
||||
);
|
||||
|
||||
// The warm-startup breadcrumb should be emitted.
|
||||
assert.ok(
|
||||
logger.entries.some((e) =>
|
||||
String(e[0]).includes("warm startup from disk snapshot")
|
||||
),
|
||||
"warm-startup breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (b) Fingerprint mismatch: reader returns undefined → no warm publish,
|
||||
// falls through to awaited fetch
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: fingerprint mismatch → no warm publish, awaited fetch", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
// Reader returns undefined → fingerprint mismatch or missing snapshot.
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published from live fetch");
|
||||
// Live fetch data, not snapshot data.
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||
"live fetch model present"
|
||||
);
|
||||
assert.equal(fetcher.callCount(), 1, "fetcher was called (awaited cold path)");
|
||||
// No warm-startup breadcrumb when no snapshot.
|
||||
assert.ok(
|
||||
!logger.entries.some((e) =>
|
||||
String(e[0]).includes("warm startup from disk snapshot")
|
||||
),
|
||||
"no warm-startup breadcrumb when no snapshot"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (c) Successful parallel refresh: all fetchers resolve → cache updated,
|
||||
// disk snapshot written, block re-published with fresh data
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: parallel refresh updates cache + writes snapshot", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([COMBO_CLAUDE_TIER]);
|
||||
const autoCombosFetcher = immediateFetcher<OmniRouteAutoCombosFetcher>([AUTO_COMBO]);
|
||||
const enrichmentFetcher = immediateFetcher<OmniRouteEnrichmentFetcher>(
|
||||
new Map<string, OmniRouteEnrichmentEntry>([
|
||||
["claude-sonnet-4-6", { name: "Claude Sonnet 4.6" }],
|
||||
])
|
||||
);
|
||||
const compressionMetaFetcher = immediateFetcher<OmniRouteCompressionMetaFetcher>([
|
||||
COMPRESSION_COMBO,
|
||||
]);
|
||||
const providersFetcher = immediateFetcher<OmniRouteProvidersFetcher>([CONNECTION_CLAUDE]);
|
||||
const logger = captureWarn();
|
||||
|
||||
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> = {
|
||||
rawModels: [MODEL_GEMINI],
|
||||
rawCombos: [],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
};
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
|
||||
let snapshotWrites = 0;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {
|
||||
snapshotWrites++;
|
||||
};
|
||||
|
||||
const sharedCache: OmniRouteFetchCache = new Map();
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", modelCacheTtl: 60_000 },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
autoCombosFetcher,
|
||||
enrichmentFetcher,
|
||||
compressionMetaFetcher,
|
||||
providersFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
cache: sharedCache,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
// Warm block should have been published.
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "warm provider entry published");
|
||||
|
||||
// Give detached refresh time to complete.
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
|
||||
// After parallel refresh, the cache should have the fresh data.
|
||||
const cacheKey = Array.from(sharedCache.keys())[0];
|
||||
assert.ok(cacheKey, "cache entry created");
|
||||
const cached = sharedCache.get(cacheKey)!;
|
||||
assert.ok(cached.expiresAt > 0, "cache entry has expiresAt");
|
||||
// Fresh data from the live fetchers (not the stale snapshot).
|
||||
assert.equal(cached.rawModels.length, 1, "cache has fresh models");
|
||||
assert.equal(cached.rawModels[0].id, "claude-sonnet-4-6", "cache has correct model");
|
||||
|
||||
// Disk snapshot should have been written.
|
||||
assert.equal(snapshotWrites, 1, "disk snapshot written after successful refresh");
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (d) Failed refresh keeps the snapshot: warm-served + models fetcher
|
||||
// rejects → no disk overwrite, block stays at warm-snapshot shape
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: failed refresh keeps the snapshot, no disk overwrite", async () => {
|
||||
const fetcher = throwingFetcher<OmniRouteModelsFetcher>();
|
||||
const combosFetcher = throwingFetcher<OmniRouteCombosFetcher>();
|
||||
const logger = captureWarn();
|
||||
|
||||
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> = {
|
||||
rawModels: [MODEL_GEMINI],
|
||||
rawCombos: [COMBO_CLAUDE_TIER],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
};
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
|
||||
let snapshotWrites = 0;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {
|
||||
snapshotWrites++;
|
||||
};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "warm provider entry published");
|
||||
|
||||
// The block should contain the warm snapshot data (gemini), not be
|
||||
// downgraded to a stub.
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/gemini-3-flash"],
|
||||
"warm snapshot model preserved (not downgraded to stub)"
|
||||
);
|
||||
|
||||
// Give detached refresh time to complete.
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
|
||||
// No disk write on failed refresh.
|
||||
assert.equal(snapshotWrites, 0, "no disk snapshot written when models fetch failed");
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (e) Parallelism: all six fetchers start concurrently (not sequential)
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: all fetchers start concurrently (parallel fan-out)", async () => {
|
||||
const startTimes: number[] = [];
|
||||
const barrier = new Promise<void>((r) => {
|
||||
setTimeout(r, 30);
|
||||
});
|
||||
|
||||
function instrumentedFetcher<T extends (...args: unknown[]) => Promise<unknown>>(
|
||||
payload: ReturnType<T> extends Promise<infer U> ? U : never
|
||||
): T & { callCount: () => number } {
|
||||
let n = 0;
|
||||
const f = async (..._args: unknown[]) => {
|
||||
startTimes.push(Date.now());
|
||||
n++;
|
||||
await barrier;
|
||||
return payload;
|
||||
};
|
||||
return Object.assign(f as T, { callCount: () => n });
|
||||
}
|
||||
|
||||
const fetcher = instrumentedFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = instrumentedFetcher<OmniRouteCombosFetcher>([]);
|
||||
const autoCombosFetcher = instrumentedFetcher<OmniRouteAutoCombosFetcher>([]);
|
||||
const enrichmentFetcher = instrumentedFetcher<OmniRouteEnrichmentFetcher>(new Map());
|
||||
const compressionMetaFetcher = instrumentedFetcher<OmniRouteCompressionMetaFetcher>([]);
|
||||
const providersFetcher = instrumentedFetcher<OmniRouteProvidersFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
// No snapshot → cold path (awaited). All fetchers must still start
|
||||
// concurrently.
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", features: { enrichment: true, compressionMetadata: true, usableOnly: true } },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
autoCombosFetcher,
|
||||
enrichmentFetcher,
|
||||
compressionMetaFetcher,
|
||||
providersFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
// All fetchers should have been called.
|
||||
assert.equal(fetcher.callCount(), 1, "models fetcher called");
|
||||
assert.equal(combosFetcher.callCount(), 1, "combos fetcher called");
|
||||
assert.equal(autoCombosFetcher.callCount(), 1, "autoCombos fetcher called");
|
||||
assert.equal(enrichmentFetcher.callCount(), 1, "enrichment fetcher called");
|
||||
assert.equal(compressionMetaFetcher.callCount(), 1, "compressionMeta fetcher called");
|
||||
assert.equal(providersFetcher.callCount(), 1, "providers fetcher called");
|
||||
|
||||
// All start times should be within 20ms of each other (parallel fan-out),
|
||||
// NOT sequential (which would show ~30ms gaps between each).
|
||||
assert.ok(startTimes.length >= 6, "all 6 fetchers started");
|
||||
const minStart = Math.min(...startTimes);
|
||||
const maxStart = Math.max(...startTimes);
|
||||
assert.ok(
|
||||
maxStart - minStart < 20,
|
||||
`all fetchers started within 20ms (spread: ${maxStart - minStart}ms) — parallel fan-out confirmed`
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (f) Soft-fail parity under Promise.allSettled: per-endpoint fallbacks +
|
||||
// logger.warn breadcrumbs preserved
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: combos reject → models-only catalog with warn", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = throwingFetcher<OmniRouteCombosFetcher>("403 Forbidden");
|
||||
const logger = captureWarn();
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published");
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||
"models-only catalog (no combos)"
|
||||
);
|
||||
assert.ok(
|
||||
logger.entries.some((e) => String(e[0]).includes("/api/combos fetch failed")),
|
||||
"combos-fetch breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
|
||||
test("warm-startup: enrichment rejects → raw-id catalog with warn", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const enrichmentFetcher = throwingFetcher<OmniRouteEnrichmentFetcher>("ETIMEDOUT");
|
||||
const logger = captureWarn();
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
enrichmentFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published");
|
||||
assert.equal(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
|
||||
"claude-sonnet-4-6",
|
||||
"raw id retained (no enrichment)"
|
||||
);
|
||||
assert.ok(
|
||||
logger.entries.some((e) => String(e[0]).includes("/api/pricing/models fetch failed")),
|
||||
"enrichment-fetch breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
|
||||
test("warm-startup: providers reject → usableOnly filter disabled with warn", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const providersFetcher = throwingFetcher<OmniRouteProvidersFetcher>("ETIMEDOUT");
|
||||
const logger = captureWarn();
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", features: { usableOnly: true } },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
providersFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published");
|
||||
// Soft-fail: model kept (filter disabled).
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||
"model kept (usableOnly filter disabled)"
|
||||
);
|
||||
assert.ok(
|
||||
logger.entries.some((e) => String(e[0]).includes("/api/providers fetch failed")),
|
||||
"providers-fetch breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (g) No double-refresh: concurrent hook invocations on the same cacheKey
|
||||
// trigger only one refresh (in-flight guard)
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: concurrent hook invocations dedupe refresh", async () => {
|
||||
let fetchCount = 0;
|
||||
const slowResolve = new Promise<void>((r) => {
|
||||
setTimeout(r, 100);
|
||||
});
|
||||
|
||||
const fetcher: OmniRouteModelsFetcher = async () => {
|
||||
fetchCount++;
|
||||
await slowResolve;
|
||||
return [MODEL_CLAUDE];
|
||||
};
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const sharedCache: OmniRouteFetchCache = new Map();
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", modelCacheTtl: 60_000 },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
cache: sharedCache,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
// Fire two concurrent hook invocations on the same cache.
|
||||
const inputA = makeInput();
|
||||
const inputB = makeInput();
|
||||
await Promise.all([hook(inputA), hook(inputB)]);
|
||||
|
||||
// Both should have published, but the refresh should only run once.
|
||||
assert.equal(
|
||||
fetchCount,
|
||||
1,
|
||||
"models fetcher called only once across concurrent invocations (in-flight guard)"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (h) features.diskCache: false disables the warm read entirely
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: diskCache=false disables warm read, falls through to awaited fetch", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
let readerCalled = false;
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => {
|
||||
readerCalled = true;
|
||||
return {
|
||||
rawModels: [MODEL_GEMINI],
|
||||
rawCombos: [],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
};
|
||||
};
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", features: { diskCache: false } },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
assert.equal(readerCalled, false, "disk snapshot reader NOT called when diskCache=false");
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published from live fetch");
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||
"live fetch model present (not snapshot)"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Warm startup: snapshot age logged
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: snapshot age is logged when warm-starting from disk", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> & {
|
||||
writtenAt?: number;
|
||||
} = {
|
||||
rawModels: [MODEL_GEMINI],
|
||||
rawCombos: [],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
writtenAt: Date.now() - 3_600_000, // 1 hour ago
|
||||
};
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
// The log should mention "warm startup from disk snapshot".
|
||||
assert.ok(
|
||||
logger.entries.some((e) =>
|
||||
String(e[0]).includes("warm startup from disk snapshot")
|
||||
),
|
||||
"warm-startup breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Warm startup: empty snapshot (rawModels.length === 0) is skipped
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: empty snapshot (rawModels.length=0) is skipped, falls through to fetch", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => ({
|
||||
rawModels: [],
|
||||
rawCombos: [],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
});
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published from live fetch");
|
||||
// Live data, not empty snapshot.
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||
"live fetch model present (empty snapshot skipped)"
|
||||
);
|
||||
assert.equal(fetcher.callCount(), 1, "fetcher was called (awaited cold path)");
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
feature: 9490
|
||||
---
|
||||
|
||||
**Warm catalog startup from disk snapshot + parallel refresh** (opencode-plugin): The config-shim hook now reads the last disk snapshot *before* fetching, so the provider registers immediately with the last-known-good catalog (~1-2s vs ~30s on a warm gateway). All six fetchers run concurrently via `Promise.allSettled` instead of sequentially. A failed refresh keeps the snapshot (no overwrite). An in-flight guard prevents concurrent refreshes for the same cache key. The `features.diskCache: false` opt-out disables the warm read entirely.
|
||||
@@ -1 +0,0 @@
|
||||
- fix(ci): aggregate all fast-gates into non-fail-fast loop so one red gate no longer masks later gates (#8542)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(github): add targetFormat to GPT-5.6 Sol/Terra/Luna models (#8951)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(translator):** the Responses-to-Chat promotion path called `normalizeResponsesReasoningEffort` without the model argument, so GPT-5.6 Sol/Terra/Luna requests with `reasoning.effort: "max""` were downgraded to `"xhigh"`. The model is now threaded through, preserving `max` for GPT-5.6 while keeping the legacy downgrade for older models ([#8997](https://github.com/diegosouzapw/OmniRoute/pull/8997))
|
||||
@@ -1 +0,0 @@
|
||||
- fix(api): use configured prefix instead of raw node UUID for alias-backed model id in /v1/models (#9034)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(api): auto/* routing aliases bypass API-key allowedConnections/disableNonPublicModels (#9057)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(providers): admit audio-speech/audio-transcriptions apiType in audio route provider-node filters (#9096)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(providers): resolve combo names in audio transcriptions route so /v1/models stays honest (#9134)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(vscode): allow built-in auto-routing models in VS Code model filter (#9140)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(classify429):** add missing `have exhausted their quota` pattern so the synthetic 429 from auth.ts is recognized as quota exhaustion, preventing the combo loop from burning retries against the same provider instead of falling back to a healthy one ([#9269](https://github.com/diegosouzapw/OmniRoute/issues/9269))
|
||||
@@ -1 +0,0 @@
|
||||
- fix(sse): broaden OMNIROUTE_SSE_COMMENTS to accept 'false','0','no' and gate metadata comment emission (#9305)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(lmarena): encode SSE stream chunks as Uint8Array to prevent TextDecoder TypeError (#9306)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(docker):** standalone co-location now completes packages Next's file tracing materialized partially (package.json without its `main` payload) — unblocks the Docker Hub publish that failed on every v3.8.50 push with `Cannot find module '@atjsh/llmlingua-2/dist/index.js'` ([#9615](https://github.com/diegosouzapw/OmniRoute/pull/9615))
|
||||
@@ -1 +0,0 @@
|
||||
- Fixed `GET`/`PUT`/`DELETE /api/memory/[id]` always failing with a 500 (`Primary backend "sqlite" not registered`) when the route was reached before any other memory endpoint in the same process.
|
||||
@@ -1 +0,0 @@
|
||||
- Replaced hand-rolled body type checks with Zod validation in the plugins marketplace install route and the three Dario admin routes, restoring the `t06:route-validation` gate (Hard Rule #7).
|
||||
1
changelog.d/fixes/9776-radar-entitlement-refresh.md
Normal file
1
changelog.d/fixes/9776-radar-entitlement-refresh.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(radar):** refresh signed catalog/referral caches when supporter entitlement changes, preserve the one-time live-to-community downgrade, and test real provider connection IDs from the setup tour
|
||||
@@ -1 +0,0 @@
|
||||
- **test(cli):** OpenCode plugin suite realigned to the bare-key static-catalog contract from #9178/#9175 (21 tests were red on every opencode-plugin CI run; 287/287 after) ([#9614](https://github.com/diegosouzapw/OmniRoute/pull/9614))
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"_rebaseline_2026_08_08_v3850_base_drift_batch_9757": "Base drift on release/v3.8.50, not own growth: the 08-06..08-08 merge batches grew 12 already-frozen (or newly-landed) files without carrying their rebaselines — the dedicated rebaseline PR #9616 was closed as 'superseded' but its file-size entries never actually reached the base, and later merges (#8894 combos page, #9539 EditConnectionModal, #8895 models route, #9294/#9293 catalog, #9541 db/core, #8970 tokenHealthCheck, #8925 mcp schemas+server, #8890 accountFallback, #9467 chat.ts, #8931 openai-to-kiro, ProxyRegistryManager) kept growing them. All 12 values re-measured on THIS branch's tree (= pure tip + this PR's 1-line chat.ts fix, which adds zero lines). This PR's own source changes (chat.ts identifier restore, stream.ts format carve-out) do not grow any frozen file past these values.",
|
||||
"_rebaseline_2026_08_08_migration_135_collision": "fix(db): resolve migration version 135 numbering collision — #9449's 135_connection_runtime_state.sql and #8908's 135_migrate_model_capability_max_token.sql both claimed version 135 (#9449 branched before #8908 merged and never got renumbered before landing on release/v3.8.50), which threw 'Migration version collision detected' the moment ANY code touched the database — a fresh install/deploy from this tip cannot even boot. Renumbered the later-landing file to 140 (next free slot) and added the matching isSchemaAlreadyApplied('140') retroactive guard, matching the established pattern already used for the prior 135/136 -> 137/138 renumber in the same file. Own growth: src/lib/db/migrationRunner.ts 1084->1094 (+10, the new case block) — irreducible, matches the existing per-case guard pattern exactly. Covered by tests/unit/migration-135-numbering-collision.test.ts (2/2), confirmed failing (reproducing the exact live crash) against the pre-fix colliding filenames, passing after.",
|
||||
"_rebaseline_2026_08_02_9259_rolling_rpm": "PR #9259 (issue #8733) own growth: open-sse/services/rateLimitManager.ts baseline 1060->1167 (+107; final source 1153). The existing withRateLimit chokepoint now composes process-local rolling RPM leases with Bottleneck admission, releases pre-dispatch leases on queue timeout/abort/connection disable, preserves caller abort reasons, and wires 429/header state into the extracted rollingRpmGate.ts. The remaining growth is irreducible lifecycle wiring at the dispatch boundary plus the real watchdog test hooks needed to verify queued-wedge recovery; moving it further would obscure lease ownership and Bottleneck cleanup. Covered by the focused rate-limit manager/sliding-window suite (33/33); distributed multi-instance coordination remains explicitly out of scope.",
|
||||
"_rebaseline_2026_07_24_8470_hyperagent_sticky_thread": "PR #8470 (artickc, fix/hyperagent-tool-loop-thread-sticky) own growth: open-sse/executors/hyperagent.ts 936->1025 (wc -l; check-file-size.mjs counts via split(\"\\n\").length so the gate sees 937->1026, +89, crosses the 1000 cap). Fixes a real bug where a reverse-conversion proxy (text-Intent/JSON to Claude Code native tool_calls) rewrites assistant messages between agentic tool-loop turns, breaking HyperAgent’s conversation-prefix fingerprint and cold-starting the thread mid tool-loop. Adds Anthropic tool_use/tool_result flattening to extractMessageText() plus a new rootUserFingerprint()/root-key lookup tier in resolveHyperAgentThreadBinding()/storeHyperAgentThreadAfterTurn() so the thread stays sticky across the tool loop. Cohesive additions inside the existing single-file executor; not extractable without splitting the executor mid-request-flow. Covered by tests/unit/executor-hyperagent.test.ts (19/19, +5 new cases for tool_result/tool_use flattening + root-key stickiness). Pre-merge review flagged a cross-conversation root-key collision risk (tracked in the PR’s own mandatory pre-merge checklist, not yet addressed) — unrelated to this file-size ratchet, tracked separately by /fix-prs.",
|
||||
"_rebaseline_2026_07_25_8494_capability_filter_fail_closed": "PR #8494 (fix/capability-filters-fail-closed, #8488) own growth: open-sse/services/combo.ts 3640->3693 (+53) adds a fail-closed guard after filterTargetsByRequestCompatibility() — when every eligible target is excluded by request-capability filtering (vision/tools/etc) instead of quota/health, the combo now returns an explicit `capability_mismatch` 400 (describeCapabilityFilterExhaustion, imported from combo/comboStructure.ts) rather than silently falling through to a generic no-targets error, plus a `compatFilterFailOpen` escape hatch (combo config OR settings) mirrored at both the main/auto and round-robin call sites for symmetry. combo/comboStructure.ts (previously under cap, un-frozen) grows 794->918 (+124) — new home for describeCapabilityFilterExhaustion + providerSupportsEmulatedToolCalling (#5240 emulated tool-calling exemption so fail-closed does not regress prompt-emulation-only combos like all-chatgpt-web). Irreducible orchestration wiring at the existing filter chokepoint (same precedent as #7301's universal-cooldown-retry generalization). Companion test tests/unit/combo-routing-engine.test.ts 3409->3449 (+40, fail-closed/fail-open coverage across both call sites) also rebaselined. Covered by tests/unit/8488-capability-filter-fail-closed.test.ts (new) + 95/95 passing across both files. Structural shrink of combo.ts tracked in #3501.",
|
||||
"_rebaseline_2026_07_25_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).",
|
||||
@@ -13,6 +16,7 @@
|
||||
"_rebaseline_2026_07_19_7546_ghe_copilot_route": "PR #7546 (GHE Copilot OAuth provider) own growth: oauth/[provider]/[action]/route.ts 960->963 (gate units, +3 = ghe-copilot device-code wiring at the existing multi-provider device-code branch — reading + HTTPS-validating the gheUrl search param (isValidGheUrl guards at both raw entry points, security-review hardening, 963->970), adding ghe-copilot to the no-PKCE provider set, and building the provider config override / threading gheUrl through poll->postExchange extraData). Mirrors the existing kiro/amazon-q startUrl override pattern right above it in the same branch; cohesive with the existing device-code dispatch chokepoint, not separately extractable without splitting a single provider-switch mid-branch. Frozen so can only shrink; structural shrink tracked in #3501.",
|
||||
"_rebaseline_2026_07_19_6846_nvidia_concurrency_gate": "Issue #6846 Phase 1 (nvidia NIM local RPM budget + per-model lockout + per-connection concurrency cap) own growth: open-sse/executors/default.ts 877->890 (+13 = the irreducible call-site wiring at DefaultExecutor.execute(), the only place nvidia requests dispatch through — the existing session-pool body was extracted verbatim into a new private executeWithSessionPool() so the outer execute() can wrap it in the nvidia concurrency-gate acquire/finally-release). All actual gating logic (semaphore key + cap resolution) lives in the new leaf open-sse/executors/default/nvidiaConcurrencyGate.ts (not frozen, well under cap). Covered by tests/unit/nvidia-quota-phase1.test.ts.",
|
||||
"_rebaseline_2026_07_18_v3849_provider_detail_wiring": "Merge campaign R2/R3 (2026-07-18): three authorized PRs each add irreducible call-site wiring to ProviderDetailPageClient.tsx — #7360 +5 (ProviderQuotaVisibilityToggle render, component extracted), #7419 +4 (NoAuthProviderControls wiring), #7062 +3 (Dahl provider hook) = 786->798. All three follow the extracted-component pattern (AgentrouterConsoleFields precedent); the frozen file only takes the wiring. Structural shrink tracked in #3501.",
|
||||
"_rebaseline_2026_07_25_dario_upstream_proxy_selector": "PR #8523 (Dario embedded service): upstream-proxy mode selector replaces the binary CLIProxyAPI toggle with Native/CLIProxyAPI/Dario/Fallback + a fallback-backend picker. ProviderDetailPageClient.tsx 798->804 (+6, new hook fields threaded through to ConnectionsListPanel), ConnectionRow.tsx 942->958 (+16, the mode <select> + conditional fallback-backend <select> replacing a single pill button), useProviderConnections.ts 954->986 (+32, upstreamProxyMode/upstreamProxyFallbackBackend state + handleSetUpstreamProxyMode, handleToggleCliproxyapiMode kept as a thin backward-compat wrapper for the existing hook-shape test). All additive UI/state for the new modes — no unrelated refactor.",
|
||||
"_rebaseline_2026_07_18_pr7653_chat_tracker_import": "PR #7653 merge-interaction growth: release moved chat.ts to its 1796 cap while this PR adds the single side-effect import 'quotaTrackersBatch.ts' (line 130) — chat.ts IS the canonical quota-fetcher registration point (codex/bailian/deepseek/openrouter/opencode/generic all import+register there), so the +1 is irreducible call-site wiring. 1796->1797. Covered by tests/unit/{agentrouter,v0,freemodel}-quota-fetcher.test.ts.",
|
||||
"_rebaseline_2026_07_17_pr7653_agentrouter_console_fields": "PR #7653 own growth (missing acceptance criterion: the AgentRouter quota tracker (#6850) read providerSpecificData.consoleApiKey/newApiUserId but neither field had dashboard UI for provider agentrouter — consoleApiKey was gated to bailian-coding-plan only and newApiUserId had zero UI). AddApiKeyModal.tsx 961->967 (+6) and EditConnectionModal.tsx 1278->1286 (+8) = import + a single <AgentrouterConsoleFields .../> render call plus the newApiUserId formData init field. The actual Input rendering (both consoleApiKey reuse + the new newApiUserId field) was EXTRACTED into a new leaf src/app/(dashboard)/dashboard/providers/[id]/components/modals/AgentrouterConsoleFields.tsx (48 LOC, <cap), mirroring the QuotaScrapingFields.tsx / GlmTeamQuotaFields.tsx precedent (#6351) so the frozen modals only carry the irreducible call-site wiring. Persist logic lives in connectionProviderSpecificData.ts (not frozen). Covered by tests/unit/agentrouter-connection-modal-fields.test.ts.",
|
||||
"_rebaseline_2026_07_17_v3849_6842_free_window_wiring": "PR #7651 (openrouter :free-window quota tracking) follow-up: the counter shipped built but never wired into the request pipeline, so combos kept spending guaranteed-429 requests on exhausted free-tier targets. Own growth: src/sse/services/auth.ts 2461->2462 (+1, irreducible at the existing model-aware preflight chokepoint — the `provider === \"codex\"` check that forwards requestedModel into the connection arg is extended to also cover `openrouter`, one added boolean + a doc comment, offset to a single net line by dropping the now-redundant inline condition). Enforcement itself lives in open-sse/services/openrouterQuotaFetcher.ts (not frozen) and the dispatch-time record/correct hooks live in open-sse/executors/base.ts (not frozen). Covered by tests/unit/openrouter-free-window-wiring-6842.test.ts.",
|
||||
@@ -157,8 +161,134 @@
|
||||
"_rebaseline_2026_06_20_1409_1294_models": "Re-baseline src/lib/db/models.ts 1184->1221: combined growth of sibling fixes #1409 (cascade-delete orphaned model aliases when a provider is removed) + #1294 (persist max_input_tokens/max_output_tokens on custom models), both adding CRUD at the existing models domain module. Cohesive db module; not extractable.",
|
||||
"_rebaseline_2026_06_20_4389_thinking_toolchoice": "Re-baseline base.ts 1387->1399 (#4389): tool_choice-forced thinking guard at the existing Claude wire-image injection chokepoint (effThinking gate avoids the Anthropic 400 when tool_choice forces a tool). Cohesive guard; structural shrink tracked in #3501.",
|
||||
"_rebaseline_2026_07_18_6979_codex_test": "PR #6979 own growth: executor-codex.test.ts 1340->1347 (+7 = generalized ensureThinkingBudget assertion added to the existing codex thinking-budget cases). antigravity-test bump 942->977 REVERTED here: #7408's test split dropped that file to 888, so this PR's +35 fits under the original 942 frozen cap.",
|
||||
"_rebaseline_2026_07_24_8354_logs_timeline_sidebar": "PR #8354 (hartmark, feature/scrolling-log) own growth: src/shared/constants/sidebarVisibility/sections.ts 812->820 (+8, the single new logs-timeline SidebarItemDefinition entry added to LOGS_GROUP.items for the new /dashboard/logs/timeline scrolling request-timeline page). Irreducible data-literal wiring at the existing sidebar-sections chokepoint, same shape as every other item in the file; not extractable without an ad-hoc single-item exception to the file's otherwise-uniform multi-line item style.",
|
||||
"cap": 1000,
|
||||
"frozen": {
|
||||
"_rebaseline_2026_06_22_4644_deepseek_web_tools": "PR #4644 (BugsBag/robust deepseek-web tool-call parsing): open-sse/executors/deepseek-web.ts 1117->1125 (+8). The new agentic tool-call path emits surrounding text + reasoning before tool_calls and swaps to the dedicated deepseekWebTools.ts parser; the +8 lines are cohesive wiring at the existing transformSSE chokepoint (the parser itself lives in the new deepseekWebTools.ts file, already under cap). The PR's own fast-gate (PR->release) does not run check:file-size, so this surfaced only at release reconcile. Covered by tests/unit/deepseek-web-tools-variants.test.ts + deepseek-web-tools-execute.test.ts.",
|
||||
"_rebaseline_2026_06_23_4712_deepseek_web_tool_results": "PR for #4712 (deepseek-web drops role:tool): open-sse/executors/deepseek-web.ts 1125->1148 (+23). messagesToPrompt() now folds role:\"tool\" results into the single-prompt transcript (recovering the tool name from the preceding assistant tool_calls by tool_call_id) instead of silently dropping them; the lines are cohesive wiring inside the existing function. Covered by tests/unit/deepseek-web-tool-result-prompt-4712.test.ts.",
|
||||
"_rebaseline_2026_06_24_headroom_strategy": "Headroom-aware connection selection (dario technique): combo.ts 3168->3180 (+12 = a new `else if (strategy === \"headroom\")` dispatch branch in handleComboChat that delegates to orderTargetsByHeadroom + its log line, plus the import). The actual logic lives OUT of the god-file: the pure ranker rankByHeadroom/computeHeadroom is the new leaf open-sse/services/combo/headroomRanking.ts (91 LOC, <cap) and the async orderer orderTargetsByHeadroom is appended to the existing open-sse/services/combo/quotaStrategies.ts (<cap) next to its sibling reset-aware/reset-window orderers (reuses their connection-expansion machinery). headroom = 1 - max(util_5h, util_7d) from getSaturation (src/lib/quota/saturationSignals.ts), prefers the connection with the most free capacity. Only the dispatch wiring is irreducible at the existing combo strategy chokepoint (mirrors the reset-aware/reset-window/context-optimized branches); not extractable without hiding the call site. fill-first stays default; all existing strategies untouched. Covered by tests/unit/combo-headroom-ranking.test.ts (pure helper) + tests/unit/combo-headroom-strategy.test.ts (orderer, saturation injected). Structural shrink of combo.ts tracked in #3501.",
|
||||
"_rebaseline_2026_06_24_quota_share_strategy": "Dedicated quota-share strategy (Phase 3 #9): combo.ts 3180->3190 (+10 = one new `else if (strategy === \"quota-share\")` dispatch branch in handleComboChat that delegates 100% to selectQuotaShareTarget + its log line, plus the import). All the new logic lives OUT of the god-file in two new leaves under open-sse/services/combo/: quotaShareInflight.ts (in-flight counter with TTL/lease, ~150 LOC <cap) and quotaShareStrategy.ts (per-model bucket gating via isBucketSaturated + DRR proportional to weight + P2C over in-flight, ~240 LOC <cap). Only the dispatch wiring is irreducible at the existing combo strategy chokepoint (mirrors the headroom/reset-aware/reset-window/context-optimized branches); not extractable without hiding the call site. ZERO existing strategy cases were modified — only this branch was added, and the qtSd/ combos switched from fill-first to quota-share in src/lib/quota/quotaCombos.ts. Covered by tests/unit/quota-share-strategy.test.ts (gating, DRR fairness, P2C in-flight, fail-open, activation). Structural shrink of combo.ts tracked in #3501.",
|
||||
"_rebaseline_2026_06_24_task_aware_routing": "Task-aware routing strategy (port PR #2045, OmniRoute #4945): combo.ts 3190->3225 (+35) = one new `else if (strategy === \"task-aware\")` dispatch branch delegating 100% to selectTaskAwareTarget + its imports/log lines. All scoring/classification logic lives OUT of the god-file in the new leaf open-sse/services/taskAwareRouting.ts (553 LOC <cap). Only the dispatch wiring is irreducible at the existing combo strategy chokepoint (mirrors quota-share/headroom/reset-aware branches). ZERO existing strategy cases modified. Covered by tests/unit/combo-task-aware.test.ts (35 tests). Structural shrink of combo.ts tracked in #3501.",
|
||||
"_rebaseline_2026_06_26_fidelity_gate_extraction": "Milestone-B fidelity-gate wiring residual: bodyToText+gateAdvance extracted to fidelityGateStep.ts (889->854, -35), but the StackOptions.fidelityGate field, the `const fidelityGate` reads at the two stacked-loop dispatch chokepoints, and the import of FidelityGateConfig are irreducible wiring that cannot leave strategySelector without an architectural refactor of the pre-existing stacked pipeline. Net: 889->854 (+6 vs the pre-Milestone-B frozen 848). Covered by tests/unit/compression/*.test.ts (940 pass).",
|
||||
"_rebaseline_2026_06_27_5193_5203_antigravity_oauthmodal": "Antigravity remote-login own growth: OAuthModal.tsx 960->969 (gate units). #5193 (+~4: remote paste instruction shown for all remote incl. Google + its rationale comment) and #5203 (+~5: handleManualSubmit credential-blob branch + button guard; submit logic extracted to oauthBlobSubmit.ts to minimize). Frozen set to the SUM so either merge order passes. Cohesive at the existing manual-submit chokepoint.",
|
||||
"_rebaseline_2026_06_27_5193_antigravity_basered": "Base-red (pre-existing release drift, fast-gate PR->release skips check:file-size): accountFallback.ts 1773->1777 and src/app/api/providers/[id]/test/route.ts 924->940 were already over their frozen caps on release/v3.8.39 independent of any antigravity change. Owner chose to rebaseline (keep the documented issue-reference comments #1846/#1449/#347 etc.) rather than accept the contributor comment-stripping in #5200/#5198. Reverted #5200 to restore the comments; bumped these two frozen caps to the actual base sizes. No logic change.",
|
||||
"_rebaseline_2026_06_28_5237_impersonation_ua_refresh": "PR #5237 (refresh impersonation UAs): grok-web.ts 1871->1873 (+2), muse-spark-web.ts 1284->1302 (+18), perplexity-web.ts 1013->1032 (+19). Net semantic change in each file is a single User-Agent constant (Chrome 147->149 for grok/muse; perplexity kept at Firefox 148 to stay matched with the firefox_148 TLS profile — the contributor's 152 bump was reverted to avoid a UA-vs-JA3 mismatch, #2459). The growth is Prettier reflow that lint-staged unavoidably applies to these grandfathered long-line files the moment they are touched; not extractable. src/sse/services/auth.ts 2336->2401 in the same reconcile is #5222's antigravity-LRU-retry growth that merged via --admin without a baseline bump.",
|
||||
"_rebaseline_2026_06_28_5243_risk_gate_prepass": "PR #5243 (compression risk-gate pre-pass) own growth: open-sse/services/compression/strategySelector.ts 854->899 (+45). The three exported entry points (applyCompression/applyStackedCompression/applyStackedCompressionAsync) become thin wrappers over pure-extracted private bodies (runCompression/runStackedCompression/runStackedCompressionAsync) so the risk-gate mask->run->restore wrapper sits strictly OUTSIDE the per-step loop — a single universal integration point. The wrapper logic itself (resolveRiskGate/withRiskGate) lives in the new riskGate/strategyWrap.ts (<cap); the residual growth is the duplicated thin-wrapper signatures + the extracted bodies' dispatch boundary, guarded by a byte-identical parity test (riskGateIntegration). Default off (DEFAULT_COMPRESSION_CONFIG unchanged). Not extractable without hiding the dispatch boundary, mirroring prior compression rebaselines. Structural shrink tracked in #3501.",
|
||||
"_rebaseline_2026_06_28_5275_correlation_id_extract": "Extraction of the safe CorrelationId subset of #5275 (hartmark) — request correlation id stored in call_logs (migration 109) and returned via the X-Correlation-Id response header, WITHOUT the combo/resilience or build/lazy-loading changes (those stay in #5275). Own growth: callLogs.ts 975->985 (correlation_id column on CallLogSummaryRow + read/map), usageHistory.ts 983->988 (correlationId metadata normalize), chat.ts 1575->1632 (withCorrelationId response wiring + combo-failure log carrying correlationId), chatHelpers.ts new 811 (withCorrelationId helper + reqId threading; was 791<cap pre-feature). Cohesive request/logging chokepoint wiring; structural shrink of chat.ts tracked in #3501.",
|
||||
"_rebaseline_2026_06_29_4038_cas_guard": "PR (#4038) own growth: tokenRefresh.ts 2103->2181 (+78 = the compare-and-swap guard on the refresh persist — runWithCasGuard/getActiveCasGuard AsyncLocalStorage pair mirroring runWithOnPersist, casGuardShouldSkipPersist that rereads the row right before persisting and skips the write when a concurrent writer already rotated the refresh_token past the one presented, plus getCasGuardStats counters). Fixes the sibling-rotation-revert → token-family-revocation storm. Gated behind an active guard (opt-in; no guard => byte-identical). Wiring lives at the two persist chokepoints inside getAccessToken; the comparison reuses wasRefreshTokenRotated from refreshSerializer. Not extractable without splitting the refresh hot path.",
|
||||
"_rebaseline_2026_06_29_5286_memoization": "PR #5286 own growth: strategySelector.ts 899->960 (+61 = the opt-in result-memoization branches in applyCompression/applyCompressionAsync — principal+determinism gate, makeMemoKey lookup/store with model+supportsVision folded into the key, recompute-with-memo-off). Default off (memoizeCompressionResults), so zero behavior change. The memo helpers live in the leaf resultMemo.ts (<cap); the chokepoint wiring here is not extractable. Structural shrink of this hot-path file tracked in #3501.",
|
||||
"_rebaseline_2026_07_01_v3843_release_5609": "Rebaseline v3.8.43 (PR #5609 release reconciliation). DRIFT dos 109 commits do ciclo: 8 god-files existentes cresceram (ApiManagerPageClient 2983->3017, combos/page 4594->4608, AddApiKeyModal 868->869, providerPageHelpers 974->996, chat.ts 1635->1647, auth.ts 2401->2403, batchProcessor 828->915, combo.ts 3368->3387) + 2 novos acima do cap (huggingchat.ts 813, tests web-cookie-providers-new 827) + 4 test files cresceram. Modularizacao deferida (blast-radius mid-release); congelado no estado atual p/ o proximo ciclo ratchetar daqui.",
|
||||
"_rebaseline_2026_07_02_5816_qoder": "PR #5816 (@AgentKiller45, qoder PAT via qodercli): qoderCli.ts 666->989, new-above-cap frozen (owner-approved baseline freeze). The growth is the legitimate PAT job-token exchange + quota parsing CLI transport (the pure-JS Cosy path 500'd on every PAT request); extracting the spawn/parse helpers now would just add indirection to a contributor PR mid-merge. Test frozen also raised for this PR's coverage growth: providers-page-utils.test.ts 1052->1092. Additionally clears an inherited base-red from the already-merged #5933 (codex json_schema->text.format): translator-openai-responses-req.test.ts 1097->1172 (+75 regression tests, no offending branch left). All remain frozen (cannot grow further); release captain's rebaseline-at-release supersedes.",
|
||||
"_rebaseline_2026_07_09_6126_clinepass_dual_auth": "PR #6126 (@hajilok, dual-auth ClinePass) own growth: tokenRefresh.ts 2181->2182 (+1 = a single `case \"clinepass\":` fallthrough label added to the existing `case \"cline\":` in _getAccessTokenInternal's provider switch, so clinepass token refresh dispatches to the already-shared refreshClineToken() instead of silently falling through to the generic OAuth refresh). Irreducible 1-line switch-case wiring at the existing chokepoint; the header-building logic for the same feature was extracted to a new leaf src/shared/utils/clineAuth.ts::buildClinepassHeaders() (well under cap) to avoid growing open-sse/executors/default.ts. Covered by tests/unit/clinepass-provider.test.ts.",
|
||||
"_rebaseline_2026_07_09_6363_kiro_external_idp": "PR #6363 (@artickc, Kiro external IdP) own growth: tokenRefresh.ts 2182->2249 (+67 = the external_idp refresh branch inside refreshKiroToken — standard public-client OAuth2 refresh_token grant against the org IdP tokenEndpoint via buildExternalIdpRefreshParams/isExternalIdpAuthMethod from the new leaf open-sse/services/kiroExternalIdp.ts, with invalid_grant/invalid_client -> unrecoverable_refresh_error mapping). Cohesive addition at the existing refreshKiroToken chokepoint. Covered by tests/unit/kiro-external-idp.test.ts.",
|
||||
"_rebaseline_2026_07_09_6587_kiro_api_key_auth": "PR #6587 (@strangersp) own growth for Kiro long-lived API-key auth, merged onto v3.8.47 tip: openai-to-kiro.ts 890->912 (+22, auth-header selection for API-key-vs-OAuth-token connections), providerLimits.ts 998->1000 (+2, API-key auth-type branch), translator-openai-to-kiro.test.ts 1234->1257 (+23), providers-page-utils.test.ts 1109->1107 (net -2 after merging with parallel release drift; connectionMatchesProviderCard api_key coverage added), provider-validation-specialty.test.ts 2856->2980 (+124 net after merge with parallel release drift; this PR also removed the file's `@typescript-eslint/no-explicit-any` eslint-suppression entry by fixing all `any` usages, adding typed replacements). Cohesive additive feature growth, well tested; not extractable without splitting the existing chokepoints mid-merge.",
|
||||
"_rebaseline_2026_07_09_6678_routing_strategy_9router": "#6678 (SeaXen) — 9router-parity Routing Strategy settings card + per-provider/combo sticky-round-robin override. Own growth: ProviderDetailPageClient.tsx 784->786 (single ProviderAccountRoutingCard mount + import), auth.ts 2448->2458 (providerStrategies override resolution: fallbackStrategy/stickyRoundRobinLimit per-provider cascade in getProviderCredentials). Both additive, zero unrelated refactor; new UI/logic lives in new files (ProviderAccountRoutingCard.tsx, RoutingStrategyCard.tsx, rrState.ts::resolveComboStickyRoundRobinLimit). chat.ts value below reflects the current release tip (grown by other concurrent PRs, e.g. #6640), not this PR own change.",
|
||||
"_rebaseline_2026_07_10_6318_omp_letta": "PR #6318 (@hamsa0x7, omp+letta CLI integrations) own growth: cliTools.ts (+53 = 2 registry entries incl. omp docsUrl) and cliRuntime.ts (+18 = runtime-detection wiring for the 2 new tools). Cohesive registry/wiring growth at the existing chokepoints; scope reduced from the original 5 tools (pi/codewhale/jcode shipped separately).",
|
||||
"_rebaseline_2026_07_10_gcf_v3_2_decode": "PR #6838 own growth: new vendored file open-sse/services/compression/engines/headroom/gcf/decode_generic.ts frozen at 880 (> 800 cap). It is the vendored GCF generic-profile decoder (spec v3.2 nested flattening plus the prototype-pollution / hasOwnProperty hardening added in this PR's Gemini review). Kept as one file faithful to upstream gcf-typescript so re-vendoring stays a clean copy rather than a re-split each cycle (sibling generic.ts/scalar.ts stay < cap; extraction would also fragment the file's frozen eslint no-explicit-any suppressions). Round-trip + prototype-pollution regression coverage in tests/unit/compression/headroom-smartcrusher.test.ts. Frozen: only shrinks from here.",
|
||||
"_rebaseline_2026_07_12_v3847_mergeprs_tail": "v3.8.47 /merge-prs tail (owner-approved): src/lib/localDb.ts NEW>800 (799->805, +6 re-exports countFreeProxies + recordFreeProxySyncErrors/clearFreeProxySyncErrors/getFreeProxySyncErrors + FreeProxySyncErrors type for #6909 free-pool relay-repair; re-export-only per Hard Rule #2, not extractable).",
|
||||
"_rebaseline_2026_07_15_7070_combos_memo": "PR #7070 (perf/p1-memo) own growth: src/app/(dashboard)/dashboard/combos/page.tsx 4655->4656 (+1 = React.memo wrapping of ComboCard). Covered by tests/unit/ui/combos-page-smoke.test.tsx.",
|
||||
"_rebaseline_2026_07_18_7399_xai_oauth_modal": "PR #7399 (xAI OAuth PKCE) own growth: OAuthModal.tsx 993->998 (+5 = provider entry + PKCE flow branch wiring at the existing provider-switch chokepoint; the provider logic itself lives in src/lib/oauth/providers/xai-oauth.ts, new leaf). Third irreducible wiring bump on this modal (969->989->993->998); structural shrink tracked in #3501.",
|
||||
"_rebaseline_2026_07_19_6636_codex_session_json": "#6636 own growth: OAuthModal.tsx 998->1030 (gate units, split(\"\\n\").length incl. trailing newline; +32 = session-JSON paste branch for handleManualSubmit plus a shared submitCodexAccessToken() helper extracted from the pre-existing bare-JWT branch, mirroring the #5203 oauthBlobSubmit.ts extraction precedent; the normalizer logic itself lives in the new src/lib/oauth/utils/codexSessionImport.ts leaf module, not here). Fourth irreducible wiring bump on this modal (969->989->993->998->1030); structural shrink tracked in #3501.",
|
||||
"_rebaseline_2026_07_19_7546_ghe_copilot_modal": "PR #7546 (GHE Copilot OAuth provider) own growth: OAuthModal.tsx 1030->1056 (gate units). Adds a gheUrl input state, routes ghe-copilot through the existing device-code branch, and threads gheUrl into the device-code request/poll extraData at the existing provider-switch chokepoints (+~24 lines, cohesive with the same pattern as #7399/#6636). The standalone GHE enterprise-URL config step JSX (originally +31 lines inline) was extracted to the new src/shared/components/oauthModal/GheConfigStep.tsx leaf component to minimize the bump; what remains is the irreducible provider-branch wiring. Fifth bump on this modal (969->989->993->998->1030->1056); structural shrink tracked in #3501.",
|
||||
"_rebaseline_2026_07_19_7787_ic2_localdb_reexports": "PR #7787 (IC2 raw connections cache + lazy-decrypt) own growth: localDb.ts 805->807 (gate units, +2). localDb.ts is the re-export-only layer (hard rule #2 — no logic); the PR adds 4 new db/readCache re-exports (touchConnectionLastUsed, getCachedRawProviderConnections, getCachedProviderConnectionById, getCachedProviderNodes) required by existing barrel importers. Irreducible for a re-export list; frozen so it can only shrink.",
|
||||
"_rebaseline_2026_07_20_7779_routingcombo_thread": "PR #7779 own growth: chatHelpers.ts 876->877 (+1, thread routingComboId into executeChatWithBreaker for compression-combo assignment). Frozen so it can only shrink.",
|
||||
"_rebaseline_2026_07_20_7819_autocandidateoverrides_reexport": "PR for #7819 (Level 1+2: read-only auto/* candidate transparency + per-API-key exclusions) own growth: localDb.ts 807->808 (+1). Adds a single `export * from \"./db/autoCandidateOverrides\"` barrel re-export (hard rule #2 — no logic) for the new DB module backing per-apiKey candidate exclusions. Irreducible for a re-export list; frozen so it can only shrink.",
|
||||
"_rebaseline_2026_07_21_8027_grok_cli_auth_json_paste": "PR #8027 (RaviTharuma, fix(grok-cli) #7610) own growth: OAuthModal.tsx 1080->1100 (gate units). Requires the full ~/.grok/auth.json (with refresh_token) on the paste-import path instead of a bare JWT, at the existing paste-token chokepoint (renamed tab label, updated instructions/placeholder, textarea for the auth.json blob, inline error surface). The validation logic itself (parseGrokCliPasteToken, previously an inline ~75-line function) was extracted to the new src/lib/oauth/utils/grokCliAuthJson.ts leaf module — mirroring the #6636/#7546 extraction precedent — so only the irreducible UI wiring remains here. Sixth bump on this modal (969->989->993->998->1030->1056->1100); structural shrink tracked in #3501.",
|
||||
"_rebaseline_2026_07_21_8034_compression_exclusions_sidebar": "#8034 (compression exclusions dashboard tab) own growth: sections.ts 796->806 (+10, one new COMPRESSION_CONTEXT_GROUP sidebar item linking /dashboard/compression/exclusions). The file was already 796/800 before this PR (organic growth from prior sidebar entries), so a single new nav item pushed it 6 lines over cap. Freezing at 806 (cannot grow further); the sidebar item array is data, not extractable logic.",
|
||||
"_rebaseline_2026_07_22_7936_namespace_roundtrip": "#7936 (@RCrushMe, Responses-Chat namespace round-trip identity seam) own growth: open-sse/translator/response/openai-responses.ts 1092->1125 (+33) and open-sse/utils/stream.ts 2814->2869 (+55) — threading the namespace-identity seam through the Responses↔Chat translation + stream paths so tool-call namespaces survive the round-trip. Cohesive translation/stream wiring at existing chokepoints, frozen at new size.",
|
||||
"_rebaseline_2026_07_22_8010_codex_responses_engine": "PR #8010 (@JxnLexn) own growth: open-sse/mcp-server/schemas/tools.ts 1497->1505 (+8 = threading the new \"codex-responses\" literal into the compressionConfigureInput strategy/autoTriggerMode Zod enums and setCompressionEngineInput engine enum, mirroring the existing rtk/omniglyph enum entries; no new tool). open-sse/services/compression/strategySelector.ts 1043->1054 (+11 = one new `if (mode === \"codex-responses\")` dispatch branch in runCompression that delegates 100% to the new codexResponsesEngine.apply, mirroring the existing rtk single-mode dispatch, plus threading config.codexResponsesConfig.preserveToolNames into the shared adaptBodyForCompression call at the 3 existing call sites). src/lib/db/compression.ts (untracked, new-file cap 800) 794->845 (+51 = normalizeCodexResponsesConfig, mirroring the existing normalizeRtkConfig normalizer, plus registering \"codex-responses\" in the COMPRESSION_MODES/STACKED_PIPELINE_ENGINE_IDS/SINGLE_MODE_ENGINE sets and the getCompressionSettings load/save switch) — added to the baseline at its current size. All three are cohesive dispatch/normalizer wiring at existing chokepoints (mirroring the prior compression-mode rebaselines #6534/#6556), not extractable without hiding the mode-dispatch boundary. Covered by tests/unit/compression/codex-responses.test.ts (6) + omniglyph-registries.test.ts/types.test.ts (22, updated for the new mode).",
|
||||
"_rebaseline_2026_07_22_8034_compression_exclusions_persistence": "#8034 (compression exclusions) own growth: src/lib/db/compression.ts 845->850 (+5 = threading the new compressionExclusions field through the existing getCompressionSettings/saveCompressionSettings load/save switch over the shared key_value compression namespace — no new table, no raw SQL). Mirrors the prior compression-field rebaselines (#8010 codex-responses normalizer at the same chokepoint); the load/save switch is a single dispatch boundary, not extractable without hiding it. Covered by the PR's 8 node:test + 3 vitest cases.",
|
||||
"_rebaseline_2026_07_22_8050_model_lockout_exact_family": "#8050 (@AndrianBalanescu) own growth: accountFallback.ts 1864->1892 (+28) — exact-vs-family model-lockout scoping (getModelLockKey/isModelLocked/clearModelLock/getModelLockoutInfo) so an Antigravity 404 for one bare model no longer hijacks the whole family cooldown. Cohesive lockout logic; frozen at new size.",
|
||||
"_rebaseline_2026_07_22_8056_headroom_minrows": "#8056 (@RaviTharuma, persist Headroom minRows) own growth: src/lib/db/compression.ts 850->866 (+16 HeadroomConfig+DEFAULT_HEADROOM_CONFIG+normalize/store in get/updateCompressionSettings) and open-sse/services/compression/strategySelector.ts 1054->1060 (+6 merge settings.headroom into stacked stepConfig). Cohesive settings-persistence + stacked-merge wiring at existing chokepoints, frozen at new size.",
|
||||
"_rebaseline_2026_07_22_8081_reasoning_placeholder_guard": "#8081 (@Dingding-leo) own growth: openai-responses.ts 1125->1137 (+12) restructuring the reasoning-placeholder guard so it skips only the empty content block and still emits finish_reason/tool_calls in the same chunk. Cohesive translator wiring; frozen at new size.",
|
||||
"_rebaseline_2026_07_22_8210_openrouter_midstream_error": "PR #8210 (hartmark, fix/openrouter-midstream-error-surfacing) own growth: open-sse/translator/response/openai-responses.ts 1137->1163 (+26) measured on the merged tip (release 1137 + this PR own growth). Adds a single new branch inside openaiToOpenAIResponsesResponse() that detects an OpenRouter-style mid-stream aggregator error (HTTP 200 SSE chunk with empty choices + a top-level error object) and surfaces it as state.upstreamError instead of silently falling through to the no-op/awaitingTrailingUsage path, which previously masked the failure as a false empty-success completion and skipped combo fallback. Irreducible call-site addition at the existing chunk-dispatch chokepoint (mirrors the Gemini-to-OpenAI translator's #4177 precedent for the same class of upstream error surfacing). Note: this baseline entry does NOT cover the separate pre-existing +11 drift already on the release tip from #8081/#8162 (1125->1136, unrelated reasoning-placeholder-stripping fix merged after this PR branched) — that drift belongs to the maintainer's rebaseline, not this PR.",
|
||||
"_rebaseline_2026_07_22_8211_gemini_malformed_tool_choice": "PR #8211 (hartmark, fix/gemini-malformed-function-call-tool-choice) own growth: open-sse/translator/response/gemini-to-openai.ts 771->821 (+50, entirely this PR's diff — no other commit touched this file between the PR's merge-base and the release tip). Adds MALFORMED_FUNCTION_CALL/UNEXPECTED_TOOL_CALL handling inside geminiToOpenAIResponse(): synthesizes a `malformed_tool_call` tool_calls entry so finish_reason normalizes to the standard \"tool_calls\" instead of an unrecognized raw enum value that OpenAI-compatible clients (e.g. OpenClaw) silently ignore, and always synthesizes (rather than skipping when a real tool call already exists) so a malformed attempt alongside a real one in the same turn is not silently discarded. Irreducible cohesive addition at the existing candidate/finishReason translation chokepoint (mirrors the 9router#2462 raw-finish-reason precedent immediately below it in the same function). Covered by the PR's own tests/unit test additions for both the malformed-only and malformed-plus-real-call cases.",
|
||||
"_rebaseline_2026_07_22_8213_chat_abandoned_target_abort": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: src/sse/handlers/chat.ts 1794->1860 (+66, measured against the PR's own merge-base — the release tip separately carries an unrelated -5 net shrink from #8013's antigravity callable-catalog alignment, which this PR's branch does not include and this entry does not cover). Adds resolveDispatchClientRawRequest(): merges a per-target modelAbortSignal into clientRawRequest.signal (via mergeAbortSignals) so a combo target abandoned by comboTargetTimeoutMs actually observes its own abort and reaches its cleanup path, instead of hanging forever inside withRateLimit/acquireAccountSemaphore and leaking a permanent 'pending' dashboard entry (live incident, log id 1784418258231-14961a). Also wires combo-exhausted rejection logging to capture request body + attempted models via the new rejectedRequestUsage helper. Irreducible additions at the existing chat dispatch chokepoint. Covered by the PR's own combo-config + integration test additions.",
|
||||
"_rebaseline_2026_07_22_8213_combo_cooldown_wait_recording": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: open-sse/services/combo.ts 3548->3604 (+56, entirely this PR's diff — no other commit touched this file between the PR's merge-base and the release tip). Fixes combo cooldown-wait state recording so a bogus 503 is no longer crystallized when the cooldown-wait vars reset every setTry, adds an OpenAI-format SSE error frame path for combo-exhausted rejections (capturing request body + attempted models), and gives an abandoned per-target dispatch its own timeout instead of leaking a permanent 'pending' dashboard entry. Irreducible additions at the existing handleComboChat dispatch/retry chokepoint (mirrors the prior quota-share/headroom/task-aware strategy-branch precedents already frozen in this file). Covered by the PR's own combo-config + Gemini TPM-ceiling benchmark test additions.",
|
||||
"_rebaseline_2026_07_22_8213_gemini_tpm_quota_cooldown_wait": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: open-sse/services/accountFallback.ts 1857->1932 on the merged tip (release 1892 incl #8050 +35, plus this PR own growth +40); measured against the PR own merge-base was 1857->1898 (+41 — the release tip separately carries an unrelated +34 from #8050's antigravity 404 model-not-found lockout scoping, which this PR's branch does not include and this entry does not cover). Own growth is the Gemini TPM-ceiling classification + cooldown-wait wiring feeding into the combo cooldown-wait state machine (rate-limit wedge recovery) introduced by this PR's commit series. Irreducible additions at the existing account-fallback/model-lockout chokepoint. Covered by the PR's own gemini-rate-limit-tracker and TPM-ceiling benchmark test additions.",
|
||||
"_rebaseline_2026_07_22_8213_health_unblock_model_cooldowns": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: src/app/(dashboard)/dashboard/health/page.tsx 1094->1165 (+71, entirely this PR's diff — no other commit touched this file between the PR's merge-base and the release tip). Adds handleUnblockAll/handleUnblockOne dashboard actions (DELETE /api/resilience/model-cooldowns) so an operator can manually clear a Gemini TPM-wedge model lockout surfaced by this PR's cooldown-wait fixes, instead of waiting out the ceiling. Irreducible UI wiring at the existing health-page action chokepoint. Covered by the PR's own dashboard/resilience test additions.",
|
||||
"_rebaseline_2026_07_22_8213_requestloggerdetail_unblock_ui": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: src/shared/components/RequestLoggerDetail.tsx 799->941 (+142, entirely this PR's diff — no other commit touched this file between the PR's merge-base and the release tip; crosses the general 800-line new-file cap so is frozen here for the first time). Adds a collapsible section header (open/expand-less toggle) plus per-log-entry unblock (`unblocking`/`cleared` state, isCombo503 detection) so the request-logger detail panel surfaces the same Gemini TPM cooldown-wait / model-lockout unblock action introduced by this PR at the individual-request level (mirrors the health-page bulk unblock action added in the same PR). Covered by the PR's own dashboard/resilience test additions.",
|
||||
"_rebaseline_2026_07_22_fusion_8013_8098_antigravity": "Fusion of #8013 (backryun, catalog/IDE-CLI-split rewrite) + #8098 (nguyenha935, protocol-fidelity/fail-closed/credits/tool-cloaking): open-sse/services/usage/antigravity.ts NEW 802 (>cap 800, +2 — #8098 credits/tier usage service on #8013's profile-aware headers). Test growth (models-catalog-route 1605->1608, provider-models-route 1752->1757 from #8013 Gemini 3.6 catalog) tracked in testFrozen.",
|
||||
"_rebaseline_2026_07_23_8127_grok_weekly_quota": "#8127 (@apoapostolov) own growth: src/sse/handlers/chat.ts 1861->1865 (+4) — weekly quota tracking for grok-web wires a quota-fetch hook at the existing dispatch chokepoint. Thin wiring mirroring adjacent provider-quota branches; not extractable. Covered by tests/unit/grok-quota-fetcher.test.ts.",
|
||||
"_rebaseline_2026_07_23_8143_empty_catch_logging": "#8143 (@chirag127) own growth: open-sse/utils/stream.ts 2869->2887 (+18) — replacing empty catch blocks in the SSE stream subsystem with console.debug logging (Rule #6 silent-swallow fix, issues #8138-#8142). Cohesive logging additions at the existing catch chokepoints, not extractable; frozen at new size. Covered by tests/unit/stream-handler-catch-logging-8143.test.ts.",
|
||||
"_rebaseline_2026_07_23_8219_cache_ttl_settings_sidebar": "#8219 (@oyi77) own growth: sections.ts 806->813 (+7) — configurable model-catalog cache-TTL settings adds a new sidebar nav entry + its visibility wiring. Sidebar item array is data, not extractable logic; frozen at new size.",
|
||||
"_rebaseline_2026_07_23_8247_8248_model_unhealthy": "#8247+#8248 own growth: accountFallback.ts 1940->1941 (+1, irreducible import statement only — the substantive #8248 DEGRADED-pattern classifier was extracted into open-sse/config/errorConfig.ts, which has ample headroom, instead of growing this frozen file; #8247's fix is a single existing-line condition change, net zero lines). Scoping the credits-exhausted 403/429 branch to isCompatibleProvider() (per-model-quota openai/anthropic-compatible-* nicknames) so it stays model-scoped instead of terminalling the whole connection, and classifying NVIDIA NIM 'Function ... DEGRADED' 400 bodies as model-access-denied instead of a raw passthrough 400. Covered by tests/unit/8247-accountfallback-model-unhealthy.test.ts and tests/unit/8248-accountfallback-nvidia-degraded.test.ts.",
|
||||
"_rebaseline_2026_07_23_8252_combo_400_advance": "#8252 (@RaviTharuma) own growth: accountFallback.ts 1932->1940 (+8) + combo.ts 3604->3630 (+26) — advance combo on model-scoped 400s wrapped as invalid/Bad-Request. Irreducible wiring at existing account-fallback + combo dispatch chokepoints. Covered by combo-model-scoped-400-advance.test.ts.",
|
||||
"_rebaseline_2026_07_23_8266_alibaba_media": "#8266 (@backryun) own growth: imageRegistry.ts 821->979 (+158) — Alibaba-family media models (Qwen image/video, Bailian, Wan) added to the image/video registry. Registry model data, not extractable logic; frozen at new size.",
|
||||
"_rebaseline_2026_07_24_8388_compression_detail_persist": "#8388 (compression engine DETAIL settings — Headroom/session-dedup/CCR — dropped on save) own growth: src/lib/db/compression.ts 866->872 (+6 = irreducible call-site wiring at the existing getCompressionSettings/updateCompressionSettings chokepoint: one import line, one `...buildDetailConfigDefaults()` spread in the seed config, and one `case \"sessionDedup\": case \"ccr\": applyDetailConfigUpdate(config, key, parsed); break;` load-switch case, mirroring the existing headroom/#8056 case immediately above it). The actual normalizer logic (normalizeSessionDedupConfig/normalizeCcrConfig, matching SESSION_DEDUP_SCHEMA/CCR_SCHEMA bounds) was EXTRACTED into a new leaf src/lib/db/compressionDetailNormalizers.ts (well under cap) so this frozen file only carries the minimal dispatch wiring. Covered by tests/unit/8388-compression-detail-persist.test.ts (schema-accept + full DB save->reload round-trip for both new sub-objects, plus a no-regression assertion on the existing headroom round-trip).",
|
||||
"_rebaseline_2026_07_24_responses_toolcalls_log_summary": "hartmark, fix/responses-tool-calls-log-summary own growth: open-sse/translator/response/openai-responses.ts 1163->1174 (+11). closeToolCall() now also writes the completed tool call into the shared state.toolCalls Map (already populated by the openai-to-claude / claude-to-openai / gemini-to-openai response translators) so stream.ts's completion-log summary builder (which reads state.toolCalls, not this translator's own funcCallIds/funcNames/funcArgsBuf bookkeeping) reports finish_reason \"tool_calls\" and message.tool_calls for openai->openai-responses translated streams instead of always logging \"stop\" with no tool_calls — the actual client-facing SSE events were already correct; only the persisted call-log summary was wrong. Irreducible call-site addition at the existing tool-call-close chokepoint. Covered by the new regression test in tests/unit/translator-resp-openai-responses.test.ts.",
|
||||
"_rebaseline_2026_07_25_8476_combo_input_bound_homogeneous_scope": "PR #8476 (herjarsa, fix/8375-8459-combo-image-fixes, #8375) own growth: open-sse/services/combo.ts 3642->3679 (+37 net: +29 the PR's own isInputBoundFailure short-circuit for deterministic context_length_exceeded/context_window_exceeded failures, +8 a /green-prs pre-merge fix scoping that short-circuit to homogeneous remainders only — the shipped code fired unconditionally on ANY target, regressing the intentional heterogeneous-combo fallback #6637/isContextOverflow400 protects, exactly as flagged by this PR's own review evidence but never actually implemented in the branch). The fix compares orderedTargets[i+1..] modelStr against the failing target's modelStr at the existing executeTarget dispatch chokepoint (mirrors the sameProviderNext precedent a few lines below) — irreducible call-site wiring, not extractable without hiding the dispatch boundary. Covered by tests/unit/combo-input-bound-failure-8375.test.ts (homogeneous pool still short-circuits) and the new tests/unit/combo-input-bound-heterogeneous-8375.test.ts (heterogeneous combo now correctly falls through to the larger-context target).",
|
||||
"_rebaseline_2026_07_25_adobe_firefly_reference_images": "Follow-up to #8006: storage upload + referenceBlobs for image/video and /v1/images/edits dispatch. adobeFireflyClient.ts 1958->2317 (+upload helpers, extract sources, resolve blob ids). Note: 2317 not 2316 — check-file-size.mjs counts LOC via split(\"\\n\").length (counts the trailing-newline empty element), which is 1 higher than `wc -l` on a file ending in \\n; the PR's original entry (2316) was measured with wc -l and undercounted by 1 against the actual gate.",
|
||||
"_rebaseline_pr1043_minimax_tts": "Upstream port decolua/9router#1043 (toanalien) own growth: audioSpeech.ts 965->1061 (+96). Adds MiniMax T2A v2 TTS dispatch (handleMinimaxSpeech + hexToBytes helper) — provider entry was already in audioRegistry (format: minimax-tts) but no handler existed, falling through to the OpenAI-compatible default that fails (T2A has custom shape + hex-encoded audio + base_resp envelope). New branch sits next to the other inline provider branches (xiaomi-mimo, coqui, tortoise, aws-polly) — extracting would just create indirection. Covered by tests/unit/minimax-tts-1043.test.ts (3 tests, GREEN: success, base_resp error, invalid-hex).",
|
||||
"_rebaseline_pr4592_exclude_exhausted_auto": "Reconcile #4592 already-merged growth: combo.ts 2991->3036 (+45, terminal-status quota-cutoff exclusion in buildAutoCandidates + opt-in gate). Fast-gate PR->release does not run check:file-size.",
|
||||
"open-sse/executors/antigravity.ts": 1528,
|
||||
"open-sse/executors/base.ts": 1640,
|
||||
"open-sse/executors/chatgpt-web.ts": 3241,
|
||||
"open-sse/executors/codex.ts": 1562,
|
||||
"open-sse/executors/cursor.ts": 1563,
|
||||
"open-sse/executors/deepseek-web.ts": 1148,
|
||||
"open-sse/executors/grok-web.ts": 1044,
|
||||
"open-sse/executors/muse-spark-web.ts": 1405,
|
||||
"open-sse/handlers/chatCore.ts": 5034,
|
||||
"open-sse/handlers/imageGeneration.ts": 3101,
|
||||
"open-sse/handlers/responseSanitizer.ts": 1128,
|
||||
"open-sse/handlers/search.ts": 1536,
|
||||
"open-sse/handlers/videoGeneration.ts": 1063,
|
||||
"open-sse/mcp-server/schemas/tools.ts": 1553,
|
||||
"open-sse/mcp-server/server.ts": 1448,
|
||||
"open-sse/mcp-server/tools/advancedTools.ts": 1120,
|
||||
"open-sse/services/accountFallback.ts": 1978,
|
||||
"open-sse/services/adobeFireflyClient.ts": 2385,
|
||||
"open-sse/services/claudeCodeCompatible.ts": 1202,
|
||||
"open-sse/services/combo.ts": 3648,
|
||||
"open-sse/services/compression/strategySelector.ts": 1060,
|
||||
"open-sse/services/rateLimitManager.ts": 1167,
|
||||
"open-sse/translator/response/openai-responses.ts": 1204,
|
||||
"open-sse/utils/cursorAgentProtobuf.ts": 1505,
|
||||
"open-sse/utils/stream.ts": 2889,
|
||||
"src/app/(dashboard)/dashboard/HomePageClient.tsx": 1388,
|
||||
"src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx": 1031,
|
||||
"src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx": 3117,
|
||||
"src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx": 1067,
|
||||
"src/app/(dashboard)/dashboard/combos/page.tsx": 4703,
|
||||
"src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx": 1283,
|
||||
"src/app/(dashboard)/dashboard/costs/quota-share/components/PoolWizard.tsx": 1022,
|
||||
"src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": 2615,
|
||||
"src/app/(dashboard)/dashboard/health/page.tsx": 1165,
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": 1324,
|
||||
"src/app/(dashboard)/dashboard/providers/page.tsx": 1944,
|
||||
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1201,
|
||||
"src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": 1019,
|
||||
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1470,
|
||||
"src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": 1123,
|
||||
"src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1629,
|
||||
"src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1573,
|
||||
"src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx": 1028,
|
||||
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2148,
|
||||
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1119,
|
||||
"src/app/api/providers/[id]/models/route.ts": 2361,
|
||||
"src/app/api/v1/models/catalog.ts": 1590,
|
||||
"src/lib/tokenHealthCheck.ts": 1053,
|
||||
"src/lib/db/apiKeys.ts": 1529,
|
||||
"src/lib/db/core.ts": 1639,
|
||||
"src/lib/db/migrationRunner.ts": 1094,
|
||||
"src/lib/db/models.ts": 1097,
|
||||
"src/lib/db/providers.ts": 1034,
|
||||
"src/lib/memory/retrieval.ts": 1073,
|
||||
"src/lib/tailscaleTunnel.ts": 1202,
|
||||
"src/lib/usage/providerLimits.ts": 1013,
|
||||
"src/shared/components/OAuthModal.tsx": 1134,
|
||||
"src/shared/components/RequestLoggerV2.tsx": 1629,
|
||||
"src/shared/components/analytics/charts.tsx": 1035,
|
||||
"src/shared/services/cliRuntime.ts": 1122,
|
||||
"src/sse/handlers/chat.ts": 1904,
|
||||
"src/sse/services/auth.ts": 2508,
|
||||
"tests/unit/account-fallback-service.test.ts": 1572,
|
||||
"tests/unit/provider-validation-specialty.test.ts": 2985,
|
||||
"open-sse/executors/hyperagent.ts": 1026,
|
||||
"open-sse/executors/default.ts": 1042,
|
||||
"open-sse/executors/kiro.ts": 1069,
|
||||
"open-sse/translator/request/openai-to-kiro.ts": 1057
|
||||
},
|
||||
"testCap": 1000,
|
||||
"testFrozen": {
|
||||
"_rebaseline_2026_06_27_5193_antigravity_test": "#5193 own test growth: oauth-providers-config.test.ts 870->873 (+3: antigravity projectId assertion + 50ms tick for the now fire-and-forget onboarding, matching the no-PKCE/no-openid flow).",
|
||||
@@ -283,134 +413,7 @@
|
||||
"_rebaseline_2026_07_27_3850_relax_filesize_cap_v2_20pct": "OWNER-APPROVED TEMPORARY relax for v3.8.50-3.8.54 PREPARE phase (docs/ROADMAP.md). v1 was cap 800->900 / testCap 800->900 on 2026-07-27; v2 = v1 +20% buffer = cap 900->1000 (+100), testCap 900->1000 (+100). Justification: same as complexity v2 — the v3.8.50 release cut coincides with high-merge activity; owner accepted enlarging the headroom to cover the entire PREPARE phase (5 minor cycles .50-.54) without per-PR rebaseline noise. Targets: decompose-existing-frozen unchanged (frozen still only-shrink — see frozen[] entries and the 105 files >900 that still need structural decomposition regardless of cap); this only relaxes the cap for NEW files in the decompose/extract-while-PREPARE phase (.51='executor registry in-place' and .52='combo.ts decomposition' create new leaf modules above 800). RE-TIGHTENING MANDATORY in v3.8.51: cap target 850 = 850 once decomposition wave stabilizes (gives 150 units of post-tighten headroom vs the new 1000 ceiling). Tracked via same roadmap issue as complexity v2. Window: v3.8.50 (release cut) → v3.8.54 close (RE-TIGHTEN at v3.8.51 prep merge per ROADMAP.md). Last entry unless measured regression. v1 entry retained below for audit trail.",
|
||||
"_rebaseline_2026_07_27_3850_relax_filesize_cap": "OWNER-APPROVED TEMPORARY relax for v3.8.50-3.8.54 PREPARE phase (docs/ROADMAP.md). cap 800->900 (+100), testCap 800->900 (+100). Targets: decompose-existing-frozen unchanged (frozen still only-shrink); this only relaxes the cap for NEW files in the decompose/extract-while-PREPARE phase (.51='executor registry in-place' and .52='combo.ts decomposition' create new leaf modules above 800). RE-TIGHTENING MANDATORY in v3.8.51: cap target 850 = 850 once decomposition wave stabilizes. SUPERSEDED by _rebaseline_2026_07_27_3850_relax_filesize_cap_v2_20pct (v1 +20% buffer) — retained for audit. Tracked via same roadmap issue.",
|
||||
"_rebaseline_2026_07_27_v3849_train1h": "Merge-train 1H (31 PRs) — owner-approved 2026-07-27. Two distinct causes, kept separate on purpose: (1) GENUINE irreducible growth at existing chokepoints — providerLimits/auth (#8632 Kimi quota-reset recovery), rateLimitManager (#8616 idle wedged limiters), models-catalog-route.test (#8610 OpenCode Go effort aliases); (2) COLLISION with #8585, which banked shrinks measured on the pre-train release tip while 30 sibling PRs in the SAME train grew those files again — chat/accountFallback (#8628), chatCore (#8613), videoGeneration (#8581), imageGeneration. The zero-headroom frozen entries cannot absorb either. Ceilings re-pinned to the post-merge tip; #8612 (also in this train) automates shrink-banking so this self-inflicted drift stops recurring. Detail: src/lib/usage/providerLimits.ts 1006->1013 (#8632); src/sse/services/auth.ts 2492->2508 (#8632); open-sse/services/rateLimitManager.ts 1014->1060 (#8616); src/sse/handlers/chat.ts 1842->1845 (#8628); open-sse/handlers/chatCore.ts 4939->4955 (#8613); open-sse/handlers/imageGeneration.ts 3100->3101 ((sem PR — teto do #8585)); open-sse/handlers/videoGeneration.ts 1038->1063 (#8581); open-sse/services/accountFallback.ts 1965->1966 (#8628); tests/unit/models-catalog-route.test.ts 1608->1636 (#8610)",
|
||||
"frozen": {
|
||||
"_rebaseline_2026_06_22_4644_deepseek_web_tools": "PR #4644 (BugsBag/robust deepseek-web tool-call parsing): open-sse/executors/deepseek-web.ts 1117->1125 (+8). The new agentic tool-call path emits surrounding text + reasoning before tool_calls and swaps to the dedicated deepseekWebTools.ts parser; the +8 lines are cohesive wiring at the existing transformSSE chokepoint (the parser itself lives in the new deepseekWebTools.ts file, already under cap). The PR's own fast-gate (PR->release) does not run check:file-size, so this surfaced only at release reconcile. Covered by tests/unit/deepseek-web-tools-variants.test.ts + deepseek-web-tools-execute.test.ts.",
|
||||
"_rebaseline_2026_06_23_4712_deepseek_web_tool_results": "PR for #4712 (deepseek-web drops role:tool): open-sse/executors/deepseek-web.ts 1125->1148 (+23). messagesToPrompt() now folds role:\"tool\" results into the single-prompt transcript (recovering the tool name from the preceding assistant tool_calls by tool_call_id) instead of silently dropping them; the lines are cohesive wiring inside the existing function. Covered by tests/unit/deepseek-web-tool-result-prompt-4712.test.ts.",
|
||||
"_rebaseline_2026_06_24_headroom_strategy": "Headroom-aware connection selection (dario technique): combo.ts 3168->3180 (+12 = a new `else if (strategy === \"headroom\")` dispatch branch in handleComboChat that delegates to orderTargetsByHeadroom + its log line, plus the import). The actual logic lives OUT of the god-file: the pure ranker rankByHeadroom/computeHeadroom is the new leaf open-sse/services/combo/headroomRanking.ts (91 LOC, <cap) and the async orderer orderTargetsByHeadroom is appended to the existing open-sse/services/combo/quotaStrategies.ts (<cap) next to its sibling reset-aware/reset-window orderers (reuses their connection-expansion machinery). headroom = 1 - max(util_5h, util_7d) from getSaturation (src/lib/quota/saturationSignals.ts), prefers the connection with the most free capacity. Only the dispatch wiring is irreducible at the existing combo strategy chokepoint (mirrors the reset-aware/reset-window/context-optimized branches); not extractable without hiding the call site. fill-first stays default; all existing strategies untouched. Covered by tests/unit/combo-headroom-ranking.test.ts (pure helper) + tests/unit/combo-headroom-strategy.test.ts (orderer, saturation injected). Structural shrink of combo.ts tracked in #3501.",
|
||||
"_rebaseline_2026_06_24_quota_share_strategy": "Dedicated quota-share strategy (Phase 3 #9): combo.ts 3180->3190 (+10 = one new `else if (strategy === \"quota-share\")` dispatch branch in handleComboChat that delegates 100% to selectQuotaShareTarget + its log line, plus the import). All the new logic lives OUT of the god-file in two new leaves under open-sse/services/combo/: quotaShareInflight.ts (in-flight counter with TTL/lease, ~150 LOC <cap) and quotaShareStrategy.ts (per-model bucket gating via isBucketSaturated + DRR proportional to weight + P2C over in-flight, ~240 LOC <cap). Only the dispatch wiring is irreducible at the existing combo strategy chokepoint (mirrors the headroom/reset-aware/reset-window/context-optimized branches); not extractable without hiding the call site. ZERO existing strategy cases were modified — only this branch was added, and the qtSd/ combos switched from fill-first to quota-share in src/lib/quota/quotaCombos.ts. Covered by tests/unit/quota-share-strategy.test.ts (gating, DRR fairness, P2C in-flight, fail-open, activation). Structural shrink of combo.ts tracked in #3501.",
|
||||
"_rebaseline_2026_06_24_task_aware_routing": "Task-aware routing strategy (port PR #2045, OmniRoute #4945): combo.ts 3190->3225 (+35) = one new `else if (strategy === \"task-aware\")` dispatch branch delegating 100% to selectTaskAwareTarget + its imports/log lines. All scoring/classification logic lives OUT of the god-file in the new leaf open-sse/services/taskAwareRouting.ts (553 LOC <cap). Only the dispatch wiring is irreducible at the existing combo strategy chokepoint (mirrors quota-share/headroom/reset-aware branches). ZERO existing strategy cases modified. Covered by tests/unit/combo-task-aware.test.ts (35 tests). Structural shrink of combo.ts tracked in #3501.",
|
||||
"_rebaseline_2026_06_26_fidelity_gate_extraction": "Milestone-B fidelity-gate wiring residual: bodyToText+gateAdvance extracted to fidelityGateStep.ts (889->854, -35), but the StackOptions.fidelityGate field, the `const fidelityGate` reads at the two stacked-loop dispatch chokepoints, and the import of FidelityGateConfig are irreducible wiring that cannot leave strategySelector without an architectural refactor of the pre-existing stacked pipeline. Net: 889->854 (+6 vs the pre-Milestone-B frozen 848). Covered by tests/unit/compression/*.test.ts (940 pass).",
|
||||
"_rebaseline_2026_06_27_5193_5203_antigravity_oauthmodal": "Antigravity remote-login own growth: OAuthModal.tsx 960->969 (gate units). #5193 (+~4: remote paste instruction shown for all remote incl. Google + its rationale comment) and #5203 (+~5: handleManualSubmit credential-blob branch + button guard; submit logic extracted to oauthBlobSubmit.ts to minimize). Frozen set to the SUM so either merge order passes. Cohesive at the existing manual-submit chokepoint.",
|
||||
"_rebaseline_2026_06_27_5193_antigravity_basered": "Base-red (pre-existing release drift, fast-gate PR->release skips check:file-size): accountFallback.ts 1773->1777 and src/app/api/providers/[id]/test/route.ts 924->940 were already over their frozen caps on release/v3.8.39 independent of any antigravity change. Owner chose to rebaseline (keep the documented issue-reference comments #1846/#1449/#347 etc.) rather than accept the contributor comment-stripping in #5200/#5198. Reverted #5200 to restore the comments; bumped these two frozen caps to the actual base sizes. No logic change.",
|
||||
"_rebaseline_2026_06_28_5237_impersonation_ua_refresh": "PR #5237 (refresh impersonation UAs): grok-web.ts 1871->1873 (+2), muse-spark-web.ts 1284->1302 (+18), perplexity-web.ts 1013->1032 (+19). Net semantic change in each file is a single User-Agent constant (Chrome 147->149 for grok/muse; perplexity kept at Firefox 148 to stay matched with the firefox_148 TLS profile — the contributor's 152 bump was reverted to avoid a UA-vs-JA3 mismatch, #2459). The growth is Prettier reflow that lint-staged unavoidably applies to these grandfathered long-line files the moment they are touched; not extractable. src/sse/services/auth.ts 2336->2401 in the same reconcile is #5222's antigravity-LRU-retry growth that merged via --admin without a baseline bump.",
|
||||
"_rebaseline_2026_06_28_5243_risk_gate_prepass": "PR #5243 (compression risk-gate pre-pass) own growth: open-sse/services/compression/strategySelector.ts 854->899 (+45). The three exported entry points (applyCompression/applyStackedCompression/applyStackedCompressionAsync) become thin wrappers over pure-extracted private bodies (runCompression/runStackedCompression/runStackedCompressionAsync) so the risk-gate mask->run->restore wrapper sits strictly OUTSIDE the per-step loop — a single universal integration point. The wrapper logic itself (resolveRiskGate/withRiskGate) lives in the new riskGate/strategyWrap.ts (<cap); the residual growth is the duplicated thin-wrapper signatures + the extracted bodies' dispatch boundary, guarded by a byte-identical parity test (riskGateIntegration). Default off (DEFAULT_COMPRESSION_CONFIG unchanged). Not extractable without hiding the dispatch boundary, mirroring prior compression rebaselines. Structural shrink tracked in #3501.",
|
||||
"_rebaseline_2026_06_28_5275_correlation_id_extract": "Extraction of the safe CorrelationId subset of #5275 (hartmark) — request correlation id stored in call_logs (migration 109) and returned via the X-Correlation-Id response header, WITHOUT the combo/resilience or build/lazy-loading changes (those stay in #5275). Own growth: callLogs.ts 975->985 (correlation_id column on CallLogSummaryRow + read/map), usageHistory.ts 983->988 (correlationId metadata normalize), chat.ts 1575->1632 (withCorrelationId response wiring + combo-failure log carrying correlationId), chatHelpers.ts new 811 (withCorrelationId helper + reqId threading; was 791<cap pre-feature). Cohesive request/logging chokepoint wiring; structural shrink of chat.ts tracked in #3501.",
|
||||
"_rebaseline_2026_06_29_4038_cas_guard": "PR (#4038) own growth: tokenRefresh.ts 2103->2181 (+78 = the compare-and-swap guard on the refresh persist — runWithCasGuard/getActiveCasGuard AsyncLocalStorage pair mirroring runWithOnPersist, casGuardShouldSkipPersist that rereads the row right before persisting and skips the write when a concurrent writer already rotated the refresh_token past the one presented, plus getCasGuardStats counters). Fixes the sibling-rotation-revert → token-family-revocation storm. Gated behind an active guard (opt-in; no guard => byte-identical). Wiring lives at the two persist chokepoints inside getAccessToken; the comparison reuses wasRefreshTokenRotated from refreshSerializer. Not extractable without splitting the refresh hot path.",
|
||||
"_rebaseline_2026_06_29_5286_memoization": "PR #5286 own growth: strategySelector.ts 899->960 (+61 = the opt-in result-memoization branches in applyCompression/applyCompressionAsync — principal+determinism gate, makeMemoKey lookup/store with model+supportsVision folded into the key, recompute-with-memo-off). Default off (memoizeCompressionResults), so zero behavior change. The memo helpers live in the leaf resultMemo.ts (<cap); the chokepoint wiring here is not extractable. Structural shrink of this hot-path file tracked in #3501.",
|
||||
"_rebaseline_2026_07_01_v3843_release_5609": "Rebaseline v3.8.43 (PR #5609 release reconciliation). DRIFT dos 109 commits do ciclo: 8 god-files existentes cresceram (ApiManagerPageClient 2983->3017, combos/page 4594->4608, AddApiKeyModal 868->869, providerPageHelpers 974->996, chat.ts 1635->1647, auth.ts 2401->2403, batchProcessor 828->915, combo.ts 3368->3387) + 2 novos acima do cap (huggingchat.ts 813, tests web-cookie-providers-new 827) + 4 test files cresceram. Modularizacao deferida (blast-radius mid-release); congelado no estado atual p/ o proximo ciclo ratchetar daqui.",
|
||||
"_rebaseline_2026_07_02_5816_qoder": "PR #5816 (@AgentKiller45, qoder PAT via qodercli): qoderCli.ts 666->989, new-above-cap frozen (owner-approved baseline freeze). The growth is the legitimate PAT job-token exchange + quota parsing CLI transport (the pure-JS Cosy path 500'd on every PAT request); extracting the spawn/parse helpers now would just add indirection to a contributor PR mid-merge. Test frozen also raised for this PR's coverage growth: providers-page-utils.test.ts 1052->1092. Additionally clears an inherited base-red from the already-merged #5933 (codex json_schema->text.format): translator-openai-responses-req.test.ts 1097->1172 (+75 regression tests, no offending branch left). All remain frozen (cannot grow further); release captain's rebaseline-at-release supersedes.",
|
||||
"_rebaseline_2026_07_09_6126_clinepass_dual_auth": "PR #6126 (@hajilok, dual-auth ClinePass) own growth: tokenRefresh.ts 2181->2182 (+1 = a single `case \"clinepass\":` fallthrough label added to the existing `case \"cline\":` in _getAccessTokenInternal's provider switch, so clinepass token refresh dispatches to the already-shared refreshClineToken() instead of silently falling through to the generic OAuth refresh). Irreducible 1-line switch-case wiring at the existing chokepoint; the header-building logic for the same feature was extracted to a new leaf src/shared/utils/clineAuth.ts::buildClinepassHeaders() (well under cap) to avoid growing open-sse/executors/default.ts. Covered by tests/unit/clinepass-provider.test.ts.",
|
||||
"_rebaseline_2026_07_09_6363_kiro_external_idp": "PR #6363 (@artickc, Kiro external IdP) own growth: tokenRefresh.ts 2182->2249 (+67 = the external_idp refresh branch inside refreshKiroToken — standard public-client OAuth2 refresh_token grant against the org IdP tokenEndpoint via buildExternalIdpRefreshParams/isExternalIdpAuthMethod from the new leaf open-sse/services/kiroExternalIdp.ts, with invalid_grant/invalid_client -> unrecoverable_refresh_error mapping). Cohesive addition at the existing refreshKiroToken chokepoint. Covered by tests/unit/kiro-external-idp.test.ts.",
|
||||
"_rebaseline_2026_07_09_6587_kiro_api_key_auth": "PR #6587 (@strangersp) own growth for Kiro long-lived API-key auth, merged onto v3.8.47 tip: openai-to-kiro.ts 890->912 (+22, auth-header selection for API-key-vs-OAuth-token connections), providerLimits.ts 998->1000 (+2, API-key auth-type branch), translator-openai-to-kiro.test.ts 1234->1257 (+23), providers-page-utils.test.ts 1109->1107 (net -2 after merging with parallel release drift; connectionMatchesProviderCard api_key coverage added), provider-validation-specialty.test.ts 2856->2980 (+124 net after merge with parallel release drift; this PR also removed the file's `@typescript-eslint/no-explicit-any` eslint-suppression entry by fixing all `any` usages, adding typed replacements). Cohesive additive feature growth, well tested; not extractable without splitting the existing chokepoints mid-merge.",
|
||||
"_rebaseline_2026_07_09_6678_routing_strategy_9router": "#6678 (SeaXen) — 9router-parity Routing Strategy settings card + per-provider/combo sticky-round-robin override. Own growth: ProviderDetailPageClient.tsx 784->786 (single ProviderAccountRoutingCard mount + import), auth.ts 2448->2458 (providerStrategies override resolution: fallbackStrategy/stickyRoundRobinLimit per-provider cascade in getProviderCredentials). Both additive, zero unrelated refactor; new UI/logic lives in new files (ProviderAccountRoutingCard.tsx, RoutingStrategyCard.tsx, rrState.ts::resolveComboStickyRoundRobinLimit). chat.ts value below reflects the current release tip (grown by other concurrent PRs, e.g. #6640), not this PR own change.",
|
||||
"_rebaseline_2026_07_10_6318_omp_letta": "PR #6318 (@hamsa0x7, omp+letta CLI integrations) own growth: cliTools.ts (+53 = 2 registry entries incl. omp docsUrl) and cliRuntime.ts (+18 = runtime-detection wiring for the 2 new tools). Cohesive registry/wiring growth at the existing chokepoints; scope reduced from the original 5 tools (pi/codewhale/jcode shipped separately).",
|
||||
"_rebaseline_2026_07_10_gcf_v3_2_decode": "PR #6838 own growth: new vendored file open-sse/services/compression/engines/headroom/gcf/decode_generic.ts frozen at 880 (> 800 cap). It is the vendored GCF generic-profile decoder (spec v3.2 nested flattening plus the prototype-pollution / hasOwnProperty hardening added in this PR's Gemini review). Kept as one file faithful to upstream gcf-typescript so re-vendoring stays a clean copy rather than a re-split each cycle (sibling generic.ts/scalar.ts stay < cap; extraction would also fragment the file's frozen eslint no-explicit-any suppressions). Round-trip + prototype-pollution regression coverage in tests/unit/compression/headroom-smartcrusher.test.ts. Frozen: only shrinks from here.",
|
||||
"_rebaseline_2026_07_12_v3847_mergeprs_tail": "v3.8.47 /merge-prs tail (owner-approved): src/lib/localDb.ts NEW>800 (799->805, +6 re-exports countFreeProxies + recordFreeProxySyncErrors/clearFreeProxySyncErrors/getFreeProxySyncErrors + FreeProxySyncErrors type for #6909 free-pool relay-repair; re-export-only per Hard Rule #2, not extractable).",
|
||||
"_rebaseline_2026_07_15_7070_combos_memo": "PR #7070 (perf/p1-memo) own growth: src/app/(dashboard)/dashboard/combos/page.tsx 4655->4656 (+1 = React.memo wrapping of ComboCard). Covered by tests/unit/ui/combos-page-smoke.test.tsx.",
|
||||
"_rebaseline_2026_07_18_7399_xai_oauth_modal": "PR #7399 (xAI OAuth PKCE) own growth: OAuthModal.tsx 993->998 (+5 = provider entry + PKCE flow branch wiring at the existing provider-switch chokepoint; the provider logic itself lives in src/lib/oauth/providers/xai-oauth.ts, new leaf). Third irreducible wiring bump on this modal (969->989->993->998); structural shrink tracked in #3501.",
|
||||
"_rebaseline_2026_07_19_6636_codex_session_json": "#6636 own growth: OAuthModal.tsx 998->1030 (gate units, split(\"\\n\").length incl. trailing newline; +32 = session-JSON paste branch for handleManualSubmit plus a shared submitCodexAccessToken() helper extracted from the pre-existing bare-JWT branch, mirroring the #5203 oauthBlobSubmit.ts extraction precedent; the normalizer logic itself lives in the new src/lib/oauth/utils/codexSessionImport.ts leaf module, not here). Fourth irreducible wiring bump on this modal (969->989->993->998->1030); structural shrink tracked in #3501.",
|
||||
"_rebaseline_2026_07_19_7546_ghe_copilot_modal": "PR #7546 (GHE Copilot OAuth provider) own growth: OAuthModal.tsx 1030->1056 (gate units). Adds a gheUrl input state, routes ghe-copilot through the existing device-code branch, and threads gheUrl into the device-code request/poll extraData at the existing provider-switch chokepoints (+~24 lines, cohesive with the same pattern as #7399/#6636). The standalone GHE enterprise-URL config step JSX (originally +31 lines inline) was extracted to the new src/shared/components/oauthModal/GheConfigStep.tsx leaf component to minimize the bump; what remains is the irreducible provider-branch wiring. Fifth bump on this modal (969->989->993->998->1030->1056); structural shrink tracked in #3501.",
|
||||
"_rebaseline_2026_07_19_7787_ic2_localdb_reexports": "PR #7787 (IC2 raw connections cache + lazy-decrypt) own growth: localDb.ts 805->807 (gate units, +2). localDb.ts is the re-export-only layer (hard rule #2 — no logic); the PR adds 4 new db/readCache re-exports (touchConnectionLastUsed, getCachedRawProviderConnections, getCachedProviderConnectionById, getCachedProviderNodes) required by existing barrel importers. Irreducible for a re-export list; frozen so it can only shrink.",
|
||||
"_rebaseline_2026_07_20_7779_routingcombo_thread": "PR #7779 own growth: chatHelpers.ts 876->877 (+1, thread routingComboId into executeChatWithBreaker for compression-combo assignment). Frozen so it can only shrink.",
|
||||
"_rebaseline_2026_07_20_7819_autocandidateoverrides_reexport": "PR for #7819 (Level 1+2: read-only auto/* candidate transparency + per-API-key exclusions) own growth: localDb.ts 807->808 (+1). Adds a single `export * from \"./db/autoCandidateOverrides\"` barrel re-export (hard rule #2 — no logic) for the new DB module backing per-apiKey candidate exclusions. Irreducible for a re-export list; frozen so it can only shrink.",
|
||||
"_rebaseline_2026_07_21_8027_grok_cli_auth_json_paste": "PR #8027 (RaviTharuma, fix(grok-cli) #7610) own growth: OAuthModal.tsx 1080->1100 (gate units). Requires the full ~/.grok/auth.json (with refresh_token) on the paste-import path instead of a bare JWT, at the existing paste-token chokepoint (renamed tab label, updated instructions/placeholder, textarea for the auth.json blob, inline error surface). The validation logic itself (parseGrokCliPasteToken, previously an inline ~75-line function) was extracted to the new src/lib/oauth/utils/grokCliAuthJson.ts leaf module — mirroring the #6636/#7546 extraction precedent — so only the irreducible UI wiring remains here. Sixth bump on this modal (969->989->993->998->1030->1056->1100); structural shrink tracked in #3501.",
|
||||
"_rebaseline_2026_07_21_8034_compression_exclusions_sidebar": "#8034 (compression exclusions dashboard tab) own growth: sections.ts 796->806 (+10, one new COMPRESSION_CONTEXT_GROUP sidebar item linking /dashboard/compression/exclusions). The file was already 796/800 before this PR (organic growth from prior sidebar entries), so a single new nav item pushed it 6 lines over cap. Freezing at 806 (cannot grow further); the sidebar item array is data, not extractable logic.",
|
||||
"_rebaseline_2026_07_22_7936_namespace_roundtrip": "#7936 (@RCrushMe, Responses-Chat namespace round-trip identity seam) own growth: open-sse/translator/response/openai-responses.ts 1092->1125 (+33) and open-sse/utils/stream.ts 2814->2869 (+55) — threading the namespace-identity seam through the Responses↔Chat translation + stream paths so tool-call namespaces survive the round-trip. Cohesive translation/stream wiring at existing chokepoints, frozen at new size.",
|
||||
"_rebaseline_2026_07_22_8010_codex_responses_engine": "PR #8010 (@JxnLexn) own growth: open-sse/mcp-server/schemas/tools.ts 1497->1505 (+8 = threading the new \"codex-responses\" literal into the compressionConfigureInput strategy/autoTriggerMode Zod enums and setCompressionEngineInput engine enum, mirroring the existing rtk/omniglyph enum entries; no new tool). open-sse/services/compression/strategySelector.ts 1043->1054 (+11 = one new `if (mode === \"codex-responses\")` dispatch branch in runCompression that delegates 100% to the new codexResponsesEngine.apply, mirroring the existing rtk single-mode dispatch, plus threading config.codexResponsesConfig.preserveToolNames into the shared adaptBodyForCompression call at the 3 existing call sites). src/lib/db/compression.ts (untracked, new-file cap 800) 794->845 (+51 = normalizeCodexResponsesConfig, mirroring the existing normalizeRtkConfig normalizer, plus registering \"codex-responses\" in the COMPRESSION_MODES/STACKED_PIPELINE_ENGINE_IDS/SINGLE_MODE_ENGINE sets and the getCompressionSettings load/save switch) — added to the baseline at its current size. All three are cohesive dispatch/normalizer wiring at existing chokepoints (mirroring the prior compression-mode rebaselines #6534/#6556), not extractable without hiding the mode-dispatch boundary. Covered by tests/unit/compression/codex-responses.test.ts (6) + omniglyph-registries.test.ts/types.test.ts (22, updated for the new mode).",
|
||||
"_rebaseline_2026_07_22_8034_compression_exclusions_persistence": "#8034 (compression exclusions) own growth: src/lib/db/compression.ts 845->850 (+5 = threading the new compressionExclusions field through the existing getCompressionSettings/saveCompressionSettings load/save switch over the shared key_value compression namespace — no new table, no raw SQL). Mirrors the prior compression-field rebaselines (#8010 codex-responses normalizer at the same chokepoint); the load/save switch is a single dispatch boundary, not extractable without hiding it. Covered by the PR's 8 node:test + 3 vitest cases.",
|
||||
"_rebaseline_2026_07_22_8050_model_lockout_exact_family": "#8050 (@AndrianBalanescu) own growth: accountFallback.ts 1864->1892 (+28) — exact-vs-family model-lockout scoping (getModelLockKey/isModelLocked/clearModelLock/getModelLockoutInfo) so an Antigravity 404 for one bare model no longer hijacks the whole family cooldown. Cohesive lockout logic; frozen at new size.",
|
||||
"_rebaseline_2026_07_22_8056_headroom_minrows": "#8056 (@RaviTharuma, persist Headroom minRows) own growth: src/lib/db/compression.ts 850->866 (+16 HeadroomConfig+DEFAULT_HEADROOM_CONFIG+normalize/store in get/updateCompressionSettings) and open-sse/services/compression/strategySelector.ts 1054->1060 (+6 merge settings.headroom into stacked stepConfig). Cohesive settings-persistence + stacked-merge wiring at existing chokepoints, frozen at new size.",
|
||||
"_rebaseline_2026_07_22_8081_reasoning_placeholder_guard": "#8081 (@Dingding-leo) own growth: openai-responses.ts 1125->1137 (+12) restructuring the reasoning-placeholder guard so it skips only the empty content block and still emits finish_reason/tool_calls in the same chunk. Cohesive translator wiring; frozen at new size.",
|
||||
"_rebaseline_2026_07_22_8210_openrouter_midstream_error": "PR #8210 (hartmark, fix/openrouter-midstream-error-surfacing) own growth: open-sse/translator/response/openai-responses.ts 1137->1163 (+26) measured on the merged tip (release 1137 + this PR own growth). Adds a single new branch inside openaiToOpenAIResponsesResponse() that detects an OpenRouter-style mid-stream aggregator error (HTTP 200 SSE chunk with empty choices + a top-level error object) and surfaces it as state.upstreamError instead of silently falling through to the no-op/awaitingTrailingUsage path, which previously masked the failure as a false empty-success completion and skipped combo fallback. Irreducible call-site addition at the existing chunk-dispatch chokepoint (mirrors the Gemini-to-OpenAI translator's #4177 precedent for the same class of upstream error surfacing). Note: this baseline entry does NOT cover the separate pre-existing +11 drift already on the release tip from #8081/#8162 (1125->1136, unrelated reasoning-placeholder-stripping fix merged after this PR branched) — that drift belongs to the maintainer's rebaseline, not this PR.",
|
||||
"_rebaseline_2026_07_22_8211_gemini_malformed_tool_choice": "PR #8211 (hartmark, fix/gemini-malformed-function-call-tool-choice) own growth: open-sse/translator/response/gemini-to-openai.ts 771->821 (+50, entirely this PR's diff — no other commit touched this file between the PR's merge-base and the release tip). Adds MALFORMED_FUNCTION_CALL/UNEXPECTED_TOOL_CALL handling inside geminiToOpenAIResponse(): synthesizes a `malformed_tool_call` tool_calls entry so finish_reason normalizes to the standard \"tool_calls\" instead of an unrecognized raw enum value that OpenAI-compatible clients (e.g. OpenClaw) silently ignore, and always synthesizes (rather than skipping when a real tool call already exists) so a malformed attempt alongside a real one in the same turn is not silently discarded. Irreducible cohesive addition at the existing candidate/finishReason translation chokepoint (mirrors the 9router#2462 raw-finish-reason precedent immediately below it in the same function). Covered by the PR's own tests/unit test additions for both the malformed-only and malformed-plus-real-call cases.",
|
||||
"_rebaseline_2026_07_22_8213_chat_abandoned_target_abort": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: src/sse/handlers/chat.ts 1794->1860 (+66, measured against the PR's own merge-base — the release tip separately carries an unrelated -5 net shrink from #8013's antigravity callable-catalog alignment, which this PR's branch does not include and this entry does not cover). Adds resolveDispatchClientRawRequest(): merges a per-target modelAbortSignal into clientRawRequest.signal (via mergeAbortSignals) so a combo target abandoned by comboTargetTimeoutMs actually observes its own abort and reaches its cleanup path, instead of hanging forever inside withRateLimit/acquireAccountSemaphore and leaking a permanent 'pending' dashboard entry (live incident, log id 1784418258231-14961a). Also wires combo-exhausted rejection logging to capture request body + attempted models via the new rejectedRequestUsage helper. Irreducible additions at the existing chat dispatch chokepoint. Covered by the PR's own combo-config + integration test additions.",
|
||||
"_rebaseline_2026_07_22_8213_combo_cooldown_wait_recording": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: open-sse/services/combo.ts 3548->3604 (+56, entirely this PR's diff — no other commit touched this file between the PR's merge-base and the release tip). Fixes combo cooldown-wait state recording so a bogus 503 is no longer crystallized when the cooldown-wait vars reset every setTry, adds an OpenAI-format SSE error frame path for combo-exhausted rejections (capturing request body + attempted models), and gives an abandoned per-target dispatch its own timeout instead of leaking a permanent 'pending' dashboard entry. Irreducible additions at the existing handleComboChat dispatch/retry chokepoint (mirrors the prior quota-share/headroom/task-aware strategy-branch precedents already frozen in this file). Covered by the PR's own combo-config + Gemini TPM-ceiling benchmark test additions.",
|
||||
"_rebaseline_2026_07_22_8213_gemini_tpm_quota_cooldown_wait": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: open-sse/services/accountFallback.ts 1857->1932 on the merged tip (release 1892 incl #8050 +35, plus this PR own growth +40); measured against the PR own merge-base was 1857->1898 (+41 — the release tip separately carries an unrelated +34 from #8050's antigravity 404 model-not-found lockout scoping, which this PR's branch does not include and this entry does not cover). Own growth is the Gemini TPM-ceiling classification + cooldown-wait wiring feeding into the combo cooldown-wait state machine (rate-limit wedge recovery) introduced by this PR's commit series. Irreducible additions at the existing account-fallback/model-lockout chokepoint. Covered by the PR's own gemini-rate-limit-tracker and TPM-ceiling benchmark test additions.",
|
||||
"_rebaseline_2026_07_22_8213_health_unblock_model_cooldowns": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: src/app/(dashboard)/dashboard/health/page.tsx 1094->1165 (+71, entirely this PR's diff — no other commit touched this file between the PR's merge-base and the release tip). Adds handleUnblockAll/handleUnblockOne dashboard actions (DELETE /api/resilience/model-cooldowns) so an operator can manually clear a Gemini TPM-wedge model lockout surfaced by this PR's cooldown-wait fixes, instead of waiting out the ceiling. Irreducible UI wiring at the existing health-page action chokepoint. Covered by the PR's own dashboard/resilience test additions.",
|
||||
"_rebaseline_2026_07_22_8213_requestloggerdetail_unblock_ui": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: src/shared/components/RequestLoggerDetail.tsx 799->941 (+142, entirely this PR's diff — no other commit touched this file between the PR's merge-base and the release tip; crosses the general 800-line new-file cap so is frozen here for the first time). Adds a collapsible section header (open/expand-less toggle) plus per-log-entry unblock (`unblocking`/`cleared` state, isCombo503 detection) so the request-logger detail panel surfaces the same Gemini TPM cooldown-wait / model-lockout unblock action introduced by this PR at the individual-request level (mirrors the health-page bulk unblock action added in the same PR). Covered by the PR's own dashboard/resilience test additions.",
|
||||
"_rebaseline_2026_07_22_fusion_8013_8098_antigravity": "Fusion of #8013 (backryun, catalog/IDE-CLI-split rewrite) + #8098 (nguyenha935, protocol-fidelity/fail-closed/credits/tool-cloaking): open-sse/services/usage/antigravity.ts NEW 802 (>cap 800, +2 — #8098 credits/tier usage service on #8013's profile-aware headers). Test growth (models-catalog-route 1605->1608, provider-models-route 1752->1757 from #8013 Gemini 3.6 catalog) tracked in testFrozen.",
|
||||
"_rebaseline_2026_07_23_8127_grok_weekly_quota": "#8127 (@apoapostolov) own growth: src/sse/handlers/chat.ts 1861->1865 (+4) — weekly quota tracking for grok-web wires a quota-fetch hook at the existing dispatch chokepoint. Thin wiring mirroring adjacent provider-quota branches; not extractable. Covered by tests/unit/grok-quota-fetcher.test.ts.",
|
||||
"_rebaseline_2026_07_23_8143_empty_catch_logging": "#8143 (@chirag127) own growth: open-sse/utils/stream.ts 2869->2887 (+18) — replacing empty catch blocks in the SSE stream subsystem with console.debug logging (Rule #6 silent-swallow fix, issues #8138-#8142). Cohesive logging additions at the existing catch chokepoints, not extractable; frozen at new size. Covered by tests/unit/stream-handler-catch-logging-8143.test.ts.",
|
||||
"_rebaseline_2026_07_23_8219_cache_ttl_settings_sidebar": "#8219 (@oyi77) own growth: sections.ts 806->813 (+7) — configurable model-catalog cache-TTL settings adds a new sidebar nav entry + its visibility wiring. Sidebar item array is data, not extractable logic; frozen at new size.",
|
||||
"_rebaseline_2026_07_23_8247_8248_model_unhealthy": "#8247+#8248 own growth: accountFallback.ts 1940->1941 (+1, irreducible import statement only — the substantive #8248 DEGRADED-pattern classifier was extracted into open-sse/config/errorConfig.ts, which has ample headroom, instead of growing this frozen file; #8247's fix is a single existing-line condition change, net zero lines). Scoping the credits-exhausted 403/429 branch to isCompatibleProvider() (per-model-quota openai/anthropic-compatible-* nicknames) so it stays model-scoped instead of terminalling the whole connection, and classifying NVIDIA NIM 'Function ... DEGRADED' 400 bodies as model-access-denied instead of a raw passthrough 400. Covered by tests/unit/8247-accountfallback-model-unhealthy.test.ts and tests/unit/8248-accountfallback-nvidia-degraded.test.ts.",
|
||||
"_rebaseline_2026_07_23_8252_combo_400_advance": "#8252 (@RaviTharuma) own growth: accountFallback.ts 1932->1940 (+8) + combo.ts 3604->3630 (+26) — advance combo on model-scoped 400s wrapped as invalid/Bad-Request. Irreducible wiring at existing account-fallback + combo dispatch chokepoints. Covered by combo-model-scoped-400-advance.test.ts.",
|
||||
"_rebaseline_2026_07_23_8266_alibaba_media": "#8266 (@backryun) own growth: imageRegistry.ts 821->979 (+158) — Alibaba-family media models (Qwen image/video, Bailian, Wan) added to the image/video registry. Registry model data, not extractable logic; frozen at new size.",
|
||||
"_rebaseline_2026_07_24_8388_compression_detail_persist": "#8388 (compression engine DETAIL settings — Headroom/session-dedup/CCR — dropped on save) own growth: src/lib/db/compression.ts 866->872 (+6 = irreducible call-site wiring at the existing getCompressionSettings/updateCompressionSettings chokepoint: one import line, one `...buildDetailConfigDefaults()` spread in the seed config, and one `case \"sessionDedup\": case \"ccr\": applyDetailConfigUpdate(config, key, parsed); break;` load-switch case, mirroring the existing headroom/#8056 case immediately above it). The actual normalizer logic (normalizeSessionDedupConfig/normalizeCcrConfig, matching SESSION_DEDUP_SCHEMA/CCR_SCHEMA bounds) was EXTRACTED into a new leaf src/lib/db/compressionDetailNormalizers.ts (well under cap) so this frozen file only carries the minimal dispatch wiring. Covered by tests/unit/8388-compression-detail-persist.test.ts (schema-accept + full DB save->reload round-trip for both new sub-objects, plus a no-regression assertion on the existing headroom round-trip).",
|
||||
"_rebaseline_2026_07_24_responses_toolcalls_log_summary": "hartmark, fix/responses-tool-calls-log-summary own growth: open-sse/translator/response/openai-responses.ts 1163->1174 (+11). closeToolCall() now also writes the completed tool call into the shared state.toolCalls Map (already populated by the openai-to-claude / claude-to-openai / gemini-to-openai response translators) so stream.ts's completion-log summary builder (which reads state.toolCalls, not this translator's own funcCallIds/funcNames/funcArgsBuf bookkeeping) reports finish_reason \"tool_calls\" and message.tool_calls for openai->openai-responses translated streams instead of always logging \"stop\" with no tool_calls — the actual client-facing SSE events were already correct; only the persisted call-log summary was wrong. Irreducible call-site addition at the existing tool-call-close chokepoint. Covered by the new regression test in tests/unit/translator-resp-openai-responses.test.ts.",
|
||||
"_rebaseline_2026_07_25_8476_combo_input_bound_homogeneous_scope": "PR #8476 (herjarsa, fix/8375-8459-combo-image-fixes, #8375) own growth: open-sse/services/combo.ts 3642->3679 (+37 net: +29 the PR's own isInputBoundFailure short-circuit for deterministic context_length_exceeded/context_window_exceeded failures, +8 a /green-prs pre-merge fix scoping that short-circuit to homogeneous remainders only — the shipped code fired unconditionally on ANY target, regressing the intentional heterogeneous-combo fallback #6637/isContextOverflow400 protects, exactly as flagged by this PR's own review evidence but never actually implemented in the branch). The fix compares orderedTargets[i+1..] modelStr against the failing target's modelStr at the existing executeTarget dispatch chokepoint (mirrors the sameProviderNext precedent a few lines below) — irreducible call-site wiring, not extractable without hiding the dispatch boundary. Covered by tests/unit/combo-input-bound-failure-8375.test.ts (homogeneous pool still short-circuits) and the new tests/unit/combo-input-bound-heterogeneous-8375.test.ts (heterogeneous combo now correctly falls through to the larger-context target).",
|
||||
"_rebaseline_2026_07_25_adobe_firefly_reference_images": "Follow-up to #8006: storage upload + referenceBlobs for image/video and /v1/images/edits dispatch. adobeFireflyClient.ts 1958->2317 (+upload helpers, extract sources, resolve blob ids). Note: 2317 not 2316 — check-file-size.mjs counts LOC via split(\"\\n\").length (counts the trailing-newline empty element), which is 1 higher than `wc -l` on a file ending in \\n; the PR's original entry (2316) was measured with wc -l and undercounted by 1 against the actual gate.",
|
||||
"_rebaseline_pr1043_minimax_tts": "Upstream port decolua/9router#1043 (toanalien) own growth: audioSpeech.ts 965->1061 (+96). Adds MiniMax T2A v2 TTS dispatch (handleMinimaxSpeech + hexToBytes helper) — provider entry was already in audioRegistry (format: minimax-tts) but no handler existed, falling through to the OpenAI-compatible default that fails (T2A has custom shape + hex-encoded audio + base_resp envelope). New branch sits next to the other inline provider branches (xiaomi-mimo, coqui, tortoise, aws-polly) — extracting would just create indirection. Covered by tests/unit/minimax-tts-1043.test.ts (3 tests, GREEN: success, base_resp error, invalid-hex).",
|
||||
"_rebaseline_pr4592_exclude_exhausted_auto": "Reconcile #4592 already-merged growth: combo.ts 2991->3036 (+45, terminal-status quota-cutoff exclusion in buildAutoCandidates + opt-in gate). Fast-gate PR->release does not run check:file-size.",
|
||||
"open-sse/executors/antigravity.ts": 1528,
|
||||
"open-sse/executors/base.ts": 1640,
|
||||
"open-sse/executors/chatgpt-web.ts": 3241,
|
||||
"open-sse/executors/codex.ts": 1562,
|
||||
"open-sse/executors/cursor.ts": 1563,
|
||||
"open-sse/executors/deepseek-web.ts": 1148,
|
||||
"open-sse/executors/grok-web.ts": 1044,
|
||||
"open-sse/executors/muse-spark-web.ts": 1405,
|
||||
"open-sse/handlers/chatCore.ts": 5034,
|
||||
"open-sse/handlers/imageGeneration.ts": 3101,
|
||||
"open-sse/handlers/responseSanitizer.ts": 1128,
|
||||
"open-sse/handlers/search.ts": 1536,
|
||||
"open-sse/handlers/videoGeneration.ts": 1063,
|
||||
"open-sse/mcp-server/schemas/tools.ts": 1553,
|
||||
"open-sse/mcp-server/server.ts": 1448,
|
||||
"open-sse/mcp-server/tools/advancedTools.ts": 1120,
|
||||
"open-sse/services/accountFallback.ts": 1978,
|
||||
"open-sse/services/adobeFireflyClient.ts": 2385,
|
||||
"open-sse/services/claudeCodeCompatible.ts": 1202,
|
||||
"open-sse/services/combo.ts": 3648,
|
||||
"open-sse/services/compression/strategySelector.ts": 1060,
|
||||
"open-sse/services/rateLimitManager.ts": 1167,
|
||||
"open-sse/translator/response/openai-responses.ts": 1204,
|
||||
"open-sse/utils/cursorAgentProtobuf.ts": 1505,
|
||||
"open-sse/utils/stream.ts": 2889,
|
||||
"src/app/(dashboard)/dashboard/HomePageClient.tsx": 1388,
|
||||
"src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx": 1031,
|
||||
"src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx": 3117,
|
||||
"src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx": 1067,
|
||||
"src/app/(dashboard)/dashboard/combos/page.tsx": 4703,
|
||||
"src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx": 1283,
|
||||
"src/app/(dashboard)/dashboard/costs/quota-share/components/PoolWizard.tsx": 1022,
|
||||
"src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": 2615,
|
||||
"src/app/(dashboard)/dashboard/health/page.tsx": 1165,
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": 1324,
|
||||
"src/app/(dashboard)/dashboard/providers/page.tsx": 1944,
|
||||
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1201,
|
||||
"src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": 1019,
|
||||
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1470,
|
||||
"src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": 1123,
|
||||
"src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1629,
|
||||
"src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1573,
|
||||
"src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx": 1028,
|
||||
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2148,
|
||||
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1119,
|
||||
"src/app/api/providers/[id]/models/route.ts": 2361,
|
||||
"src/app/api/v1/models/catalog.ts": 1590,
|
||||
"src/lib/db/apiKeys.ts": 1529,
|
||||
"src/lib/db/core.ts": 1639,
|
||||
"src/lib/db/migrationRunner.ts": 1094,
|
||||
"src/lib/db/models.ts": 1097,
|
||||
"src/lib/db/providers.ts": 1034,
|
||||
"src/lib/memory/retrieval.ts": 1073,
|
||||
"src/lib/tailscaleTunnel.ts": 1202,
|
||||
"src/lib/usage/providerLimits.ts": 1013,
|
||||
"src/shared/components/OAuthModal.tsx": 1134,
|
||||
"src/shared/components/RequestLoggerV2.tsx": 1629,
|
||||
"src/shared/components/analytics/charts.tsx": 1035,
|
||||
"src/shared/services/cliRuntime.ts": 1122,
|
||||
"src/sse/handlers/chat.ts": 1904,
|
||||
"src/sse/services/auth.ts": 2520,
|
||||
"tests/unit/account-fallback-service.test.ts": 1572,
|
||||
"tests/unit/provider-validation-specialty.test.ts": 2985,
|
||||
"open-sse/executors/hyperagent.ts": 1026,
|
||||
"src/lib/tokenHealthCheck.ts": 1053,
|
||||
"open-sse/executors/default.ts": 1042,
|
||||
"open-sse/executors/kiro.ts": 1069,
|
||||
"open-sse/translator/request/openai-to-kiro.ts": 1057,
|
||||
"open-sse/utils/sseHeartbeat.ts": 149
|
||||
},
|
||||
"_rebaseline_2026_08_02_9242_token_health_transient": "PR #9242 (fix/refresh-circuit-transient): src/lib/tokenHealthCheck.ts 1021 (new file, above cap 1000). The file consolidates token-refresh health checking logic that was previously scattered across auth.ts and tokenRefresh.ts. Cohesive single-responsibility module for refresh circuit state management; not extractable without splitting the refresh state machine. Covered by tests/unit/tokenHealthCheck-transient.test.ts.",
|
||||
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).",
|
||||
"_rebaseline_2026_07_27_v3849_train3": "Merge-train 3 (13 PRs) — owner-approved 2026-07-27. Both entries are genuine irreducible growth at existing chokepoints, not new branches: src/lib/db/apiKeys.ts 1518->1529 (#8805 cx/* ≡ codex/* API-key model permissions); open-sse/handlers/chatCore.ts 5006->5020 (#8806 real response payload into plugin onResponse hooks). Covered by tests/unit/db-apiKeys-crud.test.ts (4 new cases) and the two plugin-hook test files updated in #8806 respectively.",
|
||||
"_rebaseline_2026_07_28_8842_antigravity_projectid_refresh": "PR #8842 (fix/antigravity-projectid-refresh) own growth: open-sse/executors/antigravity.ts 1493->1528 (+35 = projectId discovery in refreshCredentials: import ensureAntigravityProjectAssigned + trim projectId + call ensureAntigravityProjectAssigned with 8s timeout + persistDiscoveredAntigravityProjectId + log success/failure). Irreducible wiring at the existing credential-refresh chokepoint. Covered by tests/unit/executor-antigravity.test.ts (4 new test cases).",
|
||||
@@ -418,146 +421,15 @@
|
||||
"_rebaseline_2026_07_28_8860_tokenrefresh_projectid": "PR #8860 (fix/antigravity-projectid-centralized) own test growth: tests/unit/token-refresh-service.test.ts 1311->1378 (+67 = 4 cases covering projectId discovery on the tokenRefresh.ts path — the Dashboard/health-check refresh route, which #8842 did not reach since that fixed the executor path). Covered by the same file.",
|
||||
"_rebaseline_2026_07_28_8861_xiaomi_token_plan": "PR #8861 (feat/xiaomi-token-plan-protocol-selector) own growth: EditConnectionModal.tsx 1283->1316 (+33 = the per-connection API-protocol selector field) and open-sse/executors/base.ts 1540->1562 (+22 = alternate-format resolution at the existing buildUrl/headers chokepoint). Both are irreducible wiring at existing call sites.",
|
||||
"_rebaseline_2026_07_28_8863_firefly_detail_level": "PR #8863 (fix/adobe-firefly-gpt-detail-level-max) own growth: adobeFireflyClient.ts 2317->2322 (+5 = gpt-image detailLevel defaulting to maximal at the existing payload-build site). Covered by tests/unit/adobe-firefly.test.ts.",
|
||||
"_rebaseline_2026_07_29_8281_home_quickstart_prefetch": "Release v3.8.49 base-red fix (no PR — captain sweep): src/app/(dashboard)/dashboard/HomePageClient.tsx 1377->1381 (+4). #8292 added prefetch={false} to the sidebar but left /home's five quick-start Links prefetching, so first paint still fired 12 speculative RSC requests — caught by navigation.spec.ts only after the e2e helper bug (APP_ROUTE_PATTERN missing /home) was repaired in the same cycle. Growth is the five prefetch attributes; it was offset first by extracting the repeated className literals (INLINE_LINK x4, DOCS_LINK x1), which collapsed five wrapped <Link> blocks back to one line each — a naive fix measured 1391. Guard: tests/unit/sidebar-prefetch-policy-8281.test.ts.",
|
||||
"_rebaseline_2026_08_02_v3850_agentrouter_responses": "Release v3.8.50 AgentRouter/Codex compatibility reconciliation. open-sse/executors/base.ts 1562->1578: #9190 wires AgentRouter's selected Claude/OpenAI/Responses protocol through the existing executor URL, auth, identity-header and fingerprint chokepoints; the reusable alternate resolver remains outside base.ts. open-sse/utils/stream.ts 2887->2889: #9213 evaluates Responses ID and usage normalization independently so response.completed always receives finite usage.total_tokens instead of short-circuiting after an ID rewrite. tests/unit/chatcore-translation-paths.test.ts 2769->2776: #9191 updates the existing Claude-Code bridge assertions for the dynamic AgentRouter wire image. PR #9224 offsets its own chatCore growth by extracting the AgentRouter protocol decisions into chatCore/agentRouterProtocol.ts, leaving chatCore below its frozen ceiling. Covered by agentrouter executor/chatCore protocol tests, chatcore translation-path tests, and responses-commentary-passthrough tests.",
|
||||
"_rebaseline_2026_08_08_v3850_base_drift_batch_9757": "Base drift on release/v3.8.50, not own growth: the 08-06..08-08 merge batches grew 12 already-frozen (or newly-landed) files without carrying their rebaselines — the dedicated rebaseline PR #9616 was closed as 'superseded' but its file-size entries never actually reached the base, and later merges (#8894 combos page, #9539 EditConnectionModal, #8895 models route, #9294/#9293 catalog, #9541 db/core, #8970 tokenHealthCheck, #8925 mcp schemas+server, #8890 accountFallback, #9467 chat.ts, #8931 openai-to-kiro, ProxyRegistryManager) kept growing them. All 12 values re-measured on THIS branch's tree (= pure tip + this PR's 1-line chat.ts fix, which adds zero lines). This PR's own source changes (chat.ts identifier restore, stream.ts format carve-out) do not grow any frozen file past these values.",
|
||||
"_rebaseline_2026_08_08_migration_135_collision": "fix(db): resolve migration version 135 numbering collision — #9449's 135_connection_runtime_state.sql and #8908's 135_migrate_model_capability_max_token.sql both claimed version 135 (#9449 branched before #8908 merged and never got renumbered before landing on release/v3.8.50), which threw 'Migration version collision detected' the moment ANY code touched the database — a fresh install/deploy from this tip cannot even boot. Renumbered the later-landing file to 140 (next free slot) and added the matching isSchemaAlreadyApplied('140') retroactive guard, matching the established pattern already used for the prior 135/136 -> 137/138 renumber in the same file. Own growth: src/lib/db/migrationRunner.ts 1084->1094 (+10, the new case block) — irreducible, matches the existing per-case guard pattern exactly. Covered by tests/unit/migration-135-numbering-collision.test.ts (2/2), confirmed failing (reproducing the exact live crash) against the pre-fix colliding filenames, passing after.",
|
||||
"_rebaseline_2026_08_02_9259_rolling_rpm": "PR #9259 (issue #8733) own growth: open-sse/services/rateLimitManager.ts baseline 1060->1167 (+107; final source 1153). The existing withRateLimit chokepoint now composes process-local rolling RPM leases with Bottleneck admission, releases pre-dispatch leases on queue timeout/abort/connection disable, preserves caller abort reasons, and wires 429/header state into the extracted rollingRpmGate.ts. The remaining growth is irreducible lifecycle wiring at the dispatch boundary plus the real watchdog test hooks needed to verify queued-wedge recovery; moving it further would obscure lease ownership and Bottleneck cleanup. Covered by the focused rate-limit manager/sliding-window suite (33/33); distributed multi-instance coordination remains explicitly out of scope.",
|
||||
"_rebaseline_2026_07_25_dario_upstream_proxy_selector": "PR #8523 (Dario embedded service): upstream-proxy mode selector replaces the binary CLIProxyAPI toggle with Native/CLIProxyAPI/Dario/Fallback + a fallback-backend picker. ProviderDetailPageClient.tsx 798->804 (+6, new hook fields threaded through to ConnectionsListPanel), ConnectionRow.tsx 942->958 (+16, the mode <select> + conditional fallback-backend <select> replacing a single pill button), useProviderConnections.ts 954->986 (+32, upstreamProxyMode/upstreamProxyFallbackBackend state + handleSetUpstreamProxyMode, handleToggleCliproxyapiMode kept as a thin backward-compat wrapper for the existing hook-shape test). All additive UI/state for the new modes — no unrelated refactor.",
|
||||
"_rebaseline_2026_08_02_9242_token_health_transient": "PR #9242 (fix/refresh-circuit-transient): src/lib/tokenHealthCheck.ts 1021 (new file, above cap 1000). The file consolidates token-refresh health checking logic that was previously scattered across auth.ts and tokenRefresh.ts. Cohesive single-responsibility module for refresh circuit state management; not extractable without splitting the refresh state machine. Covered by tests/unit/tokenHealthCheck-transient.test.ts.",
|
||||
"_rebaseline_2026_07_28_8870_firefly_ref_cap_timeout": "PR #8870 (fix/adobe-firefly-gpt-ref-cap-timeout) own growth: adobeFireflyClient.ts 2322->2385 (+63 = gpt-image subject-ref hard cap at 2 + adaptive poll timeout budget (base 300s + 60s/ref, max 600s) + defensive .slice on referenceBlobs for gpt/nano/generic families). Fixes live 504s on multi-screenshot listing jobs (Featured Promo / Box Art) where 3–4+ subject refs stall colligo until the old 180s poll budget expires. Helpers adobeFireflyMaxImageRefs/adobeFireflyImageTimeoutMs live next to the existing payload/poll chokepoint (not extractable without splitting the wire recipe mid-PR). Covered by tests/unit/adobe-firefly.test.ts (ref-cap + timeout cases). Structural shrink tracked in #3501.",
|
||||
"_rebaseline_2026_07_29_8281_home_quickstart_prefetch": "Release v3.8.49 base-red fix (no PR — captain sweep): src/app/(dashboard)/dashboard/HomePageClient.tsx 1377->1381 (+4). #8292 added prefetch={false} to the sidebar but left /home's five quick-start Links prefetching, so first paint still fired 12 speculative RSC requests — caught by navigation.spec.ts only after the e2e helper bug (APP_ROUTE_PATTERN missing /home) was repaired in the same cycle. Growth is the five prefetch attributes; it was offset first by extracting the repeated className literals (INLINE_LINK x4, DOCS_LINK x1), which collapsed five wrapped <Link> blocks back to one line each — a naive fix measured 1391. Guard: tests/unit/sidebar-prefetch-policy-8281.test.ts.",
|
||||
"_rebaseline_2026_08_01_8964_xai_agent_tools": "PR #8964 own growth: chatCore.ts 5020->5034 at the existing native-passthrough chokepoint. Adds xAI Agent Tools passthrough for /v1/responses (xai/xai-oauth/xao): resolve nativeXaiResponsesPassthrough, force openai-responses targetFormat, stamp body marker, and OR into the existing nativeCodexPassthrough sites (web-search bypass + requestEndpointPath). Leaf logic in passthroughHelpers, responsesEndpoint, targetFormat, xai executor, responseSanitizer, usageTracking. Cohesive wiring at the Codex passthrough boundary.",
|
||||
"_rebaseline_2026_08_01_8964_response_sanitizer": "PR #8964 own growth: responseSanitizer.ts 1115->1128. Keep cost_in_usd_ticks / server_side_tool_usage(_details) through sanitizeResponsesApiResponse allowlists so native xAI tool responses retain usage.",
|
||||
"_rebaseline_2026_08_02_v3850_agentrouter_responses": "Release v3.8.50 AgentRouter/Codex compatibility reconciliation. open-sse/executors/base.ts 1562->1578: #9190 wires AgentRouter's selected Claude/OpenAI/Responses protocol through the existing executor URL, auth, identity-header and fingerprint chokepoints; the reusable alternate resolver remains outside base.ts. open-sse/utils/stream.ts 2887->2889: #9213 evaluates Responses ID and usage normalization independently so response.completed always receives finite usage.total_tokens instead of short-circuiting after an ID rewrite. tests/unit/chatcore-translation-paths.test.ts 2769->2776: #9191 updates the existing Claude-Code bridge assertions for the dynamic AgentRouter wire image. PR #9224 offsets its own chatCore growth by extracting the AgentRouter protocol decisions into chatCore/agentRouterProtocol.ts, leaving chatCore below its frozen ceiling. Covered by agentrouter executor/chatCore protocol tests, chatcore translation-path tests, and responses-commentary-passthrough tests.",
|
||||
"_rebaseline_2026_08_05_9323_agentrouter_waf_retry": "PR #9323 (fix(agentrouter): retry on 400 content-blocked + burst guard) own growth: open-sse/executors/base.ts 1578->1623 (check-file-size.mjs conta via split(\"\\n\").length; wc -l ve 1622). As +45 linhas sao o WAF_RETRY_CONFIG + o burst guard via gateOutboundRequest() para o WAF do agentrouter.org, com comentarios explicando o porque de cada mitigacao e cobertos por tests/unit/base-executor-waf-retry.test.ts e tests/unit/wafRateLimit.test.ts. Crescimento funcional legitimo, nao inchaco.",
|
||||
"_rebaseline_2026_08_05_9529_own_growth": "PR #9529 own growth (base release/v3.8.50 medida EXATAMENTE nos frozen antigos, entao o modo base-relative #8522 nao cobre): open-sse/services/rateLimitManager.ts 1060->1105 (+45: helper applyLimiterSettings() que re-arma o heartbeat do reservoir apos updateSettings — fix do bug Bottleneck 2.19.5 que congelava a fila weighted; TDD em tests/unit/ratelimit-reservoir-refresh.test.ts); tests/integration/chat-pipeline.test.ts 1592->1598 (+6: User-Agent do codex derivado de getCodexClientVersion() em vez de literal pinado — teste-irmao alinhado ao contrato); tests/unit/provider-validation-specialty.test.ts 2980->2985 (+5: cobertura NOVA claude-web 429 -> valid:false, alinhamento #9406); open-sse/translator/response/openai-responses.ts 1174->1204 (+30: buildResponsesReasoningSummaryDelta MOVIDA do leaf pureHelpers.ts para o host — a funcao do #9500 muta stream state e violava o contrato do leaf puro; o LOC total do par host+leaf nao cresceu, o pureHelpers encolheu o mesmo tanto). Crescimento por fix de producao + cobertura adicional + realocacao arquitetural, nao inchaco.",
|
||||
"_rebaseline_2026_08_06_v3850_inherited_drift_reconcile": "Reconciliacao 2026-08-06 do drift ACUMULADO da release/v3.8.50 apos o lote de merges de 08-05/06: 13 arquivos acima do frozen no tip puro 8180b49ce1 (medidos pelo proprio gate). O modo PR base-relative (#8522) deixa PRs inocentes passarem, e os rebaselines individuais dos PRs se perderam nas resolucoes sucessivas de conflito deste hot-file — o drift so aparece no modo absoluto (nightly/local). Crescimentos funcionais dos PRs mergeados: #9024 topology click-nav src/app/(dashboard)/dashboard/HomePageClient.tsx; #9324 OpenRouter enrich src/app/(dashboard)/dashboard/providers/page.tsx; #9329 quota card ordering src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx; #9193 context-window suffixes src/sse/handlers/chat.ts; #9332 nested Claude server tool ids open-sse/executors/base.ts; #9228 strip orphaned tool outputs open-sse/executors/codex.ts; #9236 nvidia tool-name normalize open-sse/executors/default.ts; #9314 nested tool_call validation open-sse/executors/kiro.ts; #9260 caller identity REST hops open-sse/mcp-server/server.ts; #8934 cache breakpoints tests tests/unit/chatcore-translation-paths.test.ts; #9193 suffix tests tests/unit/combo-routing-engine.test.ts; #9196 reasoning-on-tool-finish tests tests/unit/sse-auth.test.ts; #9163 GPT-5.6 Max reasoning tests tests/unit/translator-openai-to-kiro.test.ts. default.ts e kiro.ts entram no frozen (estavam sem entrada, acima do cap 1000). Atualizacao pos-medicao (a base avancou durante o ciclo do PR): src/sse/handlers/chat.ts 1857->1877 (#9184 affinity EOF evict) e open-sse/executors/default.ts 1027->1042 (#9005 Kimi K3 tool-name backfill).",
|
||||
"_rebaseline_2026_08_06b_v3850_sweepreds_drift": "Segunda reconciliacao de 2026-08-06 (/sweep-reds sobre o tip puro 2ddbbc61a6): 3 arquivos voltaram a passar do frozen apos os merges do mesmo dia, com atribuicao 1:1 por commit. (1) src/app/(dashboard)/dashboard/providers/page.tsx 1928->1944 e (2) open-sse/executors/base.ts 1635->1640, ambos do #9515 (feat(radar): flag-gated signed free-model catalog overlay, commit e7f6b1d130) — o overlay do Radar entra por wiring nos chokepoints ja existentes (a resolucao/verificacao do catalogo assinado mora fora destes dois arquivos); +16 e +5 linhas liquidas nao sao extraiveis sem inventar um leaf por callsite. (3) open-sse/services/accountFallback.ts 1966->1972 do #8704 (commit c4527f97bd), +6 linhas de dados em CREDITS_EXHAUSTED_SIGNALS ('has been exhausted', fixes #8631). src/sse/handlers/chat.ts 1880>1877 tambem estava violando e NAO entra aqui de proposito: e drenado por encolhimento na PR #9598, sem rebaseline. Crescimento proprio DESTA PR: src/lib/db/migrationRunner.ts 1077->1084 (+7) — o guard retroativo em isSchemaAlreadyApplied para os arquivos renumerados 137/138, exigido pela propria mensagem de erro de colisao do runner (ambas as migracoes sao ALTER TABLE ADD COLUMN puro, nao idempotente). Dois `case` + dois `return hasColumn(...)` + 3 linhas de comentario dentro do switch existente; nao extraivel.",
|
||||
"_rebaseline_2026_08_06c_v3850_sweepreds_pr2": "Segunda PR do /sweep-reds (fix/release-v3.8.50-basereds-0806b): tests/unit/provider-models-route.test.ts 1784->1787 (medido pelo gate, que conta split(\"\\n\").length) (+2 apos compressao de comentarios) — alinhamento de contrato forcado por dois merges do dia: #9106 tornou gemini-3.1-pro-high user-callable (a entry do alias entra na lista esperada do teste de discovery-retry, +1 linha de dado + 1 de comentario) e ff012ff420 adicionou onboardUser como bootstrap hop (exclusao no mock, ja comprimida a 1 linha). Nao ha o que encolher sem apagar o comentario que explica o porque.",
|
||||
"_rebaseline_2026_08_07_v3850_sweepreds_pr2_toolnamemap": "tests/unit/translator-openai-to-gemini.test.ts 1616->1619 (+3). O frozen estava EXATAMENTE no tamanho da base, entao qualquer linha nova viola. #9568 (c9a3361e5a) fez buildChangedToolNameMap emitir entradas IDENTIDADE (o Gemini minusculiza nomes de tool nas respostas, entao o tradutor de resposta precisa da chave para mapear de volta), o que passou a incluir `_toolNameMap` no envelope Antigravity de qualquer request com tools. As 3 linhas sao: a chave nova na lista esperada de Object.keys, 1 comentario explicando POR QUE ela aparece (sem ele o proximo leitor tenta remove-la de novo) e 1 assert do CONTEUDO do map — presenca de chave sozinha nao provaria a entrada identidade, que e justamente o comportamento novo. Nao ha o que extrair: e alinhamento de contrato dentro de um teste existente.",
|
||||
"_rebaseline_2026_06_22_4644_deepseek_web_tools": "PR #4644 (BugsBag/robust deepseek-web tool-call parsing): open-sse/executors/deepseek-web.ts 1117->1125 (+8). The new agentic tool-call path emits surrounding text + reasoning before tool_calls and swaps to the dedicated deepseekWebTools.ts parser; the +8 lines are cohesive wiring at the existing transformSSE chokepoint (the parser itself lives in the new deepseekWebTools.ts file, already under cap). The PR's own fast-gate (PR->release) does not run check:file-size, so this surfaced only at release reconcile. Covered by tests/unit/deepseek-web-tools-variants.test.ts + deepseek-web-tools-execute.test.ts.",
|
||||
"_rebaseline_2026_06_23_4712_deepseek_web_tool_results": "PR for #4712 (deepseek-web drops role:tool): open-sse/executors/deepseek-web.ts 1125->1148 (+23). messagesToPrompt() now folds role:\\\"tool\\\" results into the single-prompt transcript (recovering the tool name from the preceding assistant tool_calls by tool_call_id) instead of silently dropping them; the lines are cohesive wiring inside the existing function. Covered by tests/unit/deepseek-web-tool-result-prompt-4712.test.ts.",
|
||||
"_rebaseline_2026_06_24_headroom_strategy": "Headroom-aware connection selection (dario technique): combo.ts 3168->3180 (+12 = a new `else if (strategy === \\\"headroom\\\")` dispatch branch in handleComboChat that delegates to orderTargetsByHeadroom + its log line, plus the import). The actual logic lives OUT of the god-file: the pure ranker rankByHeadroom/computeHeadroom is the new leaf open-sse/services/combo/headroomRanking.ts (91 LOC, <cap) and the async orderer orderTargetsByHeadroom is appended to the existing open-sse/services/combo/quotaStrategies.ts (<cap) next to its sibling reset-aware/reset-window orderers (reuses their connection-expansion machinery). headroom = 1 - max(util_5h, util_7d) from getSaturation (src/lib/quota/saturationSignals.ts), prefers the connection with the most free capacity. Only the dispatch wiring is irreducible at the existing combo strategy chokepoint (mirrors the reset-aware/reset-window/context-optimized branches); not extractable without hiding the call site. fill-first stays default; all existing strategies untouched. Covered by tests/unit/combo-headroom-ranking.test.ts (pure helper) + tests/unit/combo-headroom-strategy.test.ts (orderer, saturation injected). Structural shrink of combo.ts tracked in #3501.",
|
||||
"_rebaseline_2026_06_24_quota_share_strategy": "Dedicated quota-share strategy (Phase 3 #9): combo.ts 3180->3190 (+10 = one new `else if (strategy === \\\"quota-share\\\")` dispatch branch in handleComboChat that delegates 100% to selectQuotaShareTarget + its log line, plus the import). All the new logic lives OUT of the god-file in two new leaves under open-sse/services/combo/: quotaShareInflight.ts (in-flight counter with TTL/lease, ~150 LOC <cap) and quotaShareStrategy.ts (per-model bucket gating via isBucketSaturated + DRR proportional to weight + P2C over in-flight, ~240 LOC <cap). Only the dispatch wiring is irreducible at the existing combo strategy chokepoint (mirrors the headroom/reset-aware/reset-window/context-optimized branches); not extractable without hiding the call site. ZERO existing strategy cases were modified — only this branch was added, and the qtSd/ combos switched from fill-first to quota-share in src/lib/quota/quotaCombos.ts. Covered by tests/unit/quota-share-strategy.test.ts (gating, DRR fairness, P2C in-flight, fail-open, activation). Structural shrink of combo.ts tracked in #3501.",
|
||||
"_rebaseline_2026_06_24_task_aware_routing": "Task-aware routing strategy (port PR #2045, OmniRoute #4945): combo.ts 3190->3225 (+35) = one new `else if (strategy === \\\"task-aware\\\")` dispatch branch delegating 100% to selectTaskAwareTarget + its imports/log lines. All scoring/classification logic lives OUT of the god-file in the new leaf open-sse/services/taskAwareRouting.ts (553 LOC <cap). Only the dispatch wiring is irreducible at the existing combo strategy chokepoint (mirrors quota-share/headroom/reset-aware branches). ZERO existing strategy cases modified. Covered by tests/unit/combo-task-aware.test.ts (35 tests). Structural shrink of combo.ts tracked in #3501.",
|
||||
"_rebaseline_2026_06_26_fidelity_gate_extraction": "Milestone-B fidelity-gate wiring residual: bodyToText+gateAdvance extracted to fidelityGateStep.ts (889->854, -35), but the StackOptions.fidelityGate field, the `const fidelityGate` reads at the two stacked-loop dispatch chokepoints, and the import of FidelityGateConfig are irreducible wiring that cannot leave strategySelector without an architectural refactor of the pre-existing stacked pipeline. Net: 889->854 (+6 vs the pre-Milestone-B frozen 848). Covered by tests/unit/compression/*.test.ts (940 pass).",
|
||||
"_rebaseline_2026_06_27_5193_5203_antigravity_oauthmodal": "Antigravity remote-login own growth: OAuthModal.tsx 960->969 (gate units). #5193 (+~4: remote paste instruction shown for all remote incl. Google + its rationale comment) and #5203 (+~5: handleManualSubmit credential-blob branch + button guard; submit logic extracted to oauthBlobSubmit.ts to minimize). Frozen set to the SUM so either merge order passes. Cohesive at the existing manual-submit chokepoint.",
|
||||
"_rebaseline_2026_06_27_5193_antigravity_basered": "Base-red (pre-existing release drift, fast-gate PR->release skips check:file-size): accountFallback.ts 1773->1777 and src/app/api/providers/[id]/test/route.ts 924->940 were already over their frozen caps on release/v3.8.39 independent of any antigravity change. Owner chose to rebaseline (keep the documented issue-reference comments #1846/#1449/#347 etc.) rather than accept the contributor comment-stripping in #5200/#5198. Reverted #5200 to restore the comments; bumped these two frozen caps to the actual base sizes. No logic change.",
|
||||
"_rebaseline_2026_06_28_5237_impersonation_ua_refresh": "PR #5237 (refresh impersonation UAs): grok-web.ts 1871->1873 (+2), muse-spark-web.ts 1284->1302 (+18), perplexity-web.ts 1013->1032 (+19). Net semantic change in each file is a single User-Agent constant (Chrome 147->149 for grok/muse; perplexity kept at Firefox 148 to stay matched with the firefox_148 TLS profile — the contributor's 152 bump was reverted to avoid a UA-vs-JA3 mismatch, #2459). The growth is Prettier reflow that lint-staged unavoidably applies to these grandfathered long-line files the moment they are touched; not extractable. src/sse/services/auth.ts 2336->2401 in the same reconcile is #5222's antigravity-LRU-retry growth that merged via --admin without a baseline bump.",
|
||||
"_rebaseline_2026_06_28_5243_risk_gate_prepass": "PR #5243 (compression risk-gate pre-pass) own growth: open-sse/services/compression/strategySelector.ts 854->899 (+45). The three exported entry points (applyCompression/applyStackedCompression/applyStackedCompressionAsync) become thin wrappers over pure-extracted private bodies (runCompression/runStackedCompression/runStackedCompressionAsync) so the risk-gate mask->run->restore wrapper sits strictly OUTSIDE the per-step loop — a single universal integration point. The wrapper logic itself (resolveRiskGate/withRiskGate) lives in the new riskGate/strategyWrap.ts (<cap); the residual growth is the duplicated thin-wrapper signatures + the extracted bodies' dispatch boundary, guarded by a byte-identical parity test (riskGateIntegration). Default off (DEFAULT_COMPRESSION_CONFIG unchanged). Not extractable without hiding the dispatch boundary, mirroring prior compression rebaselines. Structural shrink tracked in #3501.",
|
||||
"_rebaseline_2026_06_28_5275_correlation_id_extract": "Extraction of the safe CorrelationId subset of #5275 (hartmark) — request correlation id stored in call_logs (migration 109) and returned via the X-Correlation-Id response header, WITHOUT the combo/resilience or build/lazy-loading changes (those stay in #5275). Own growth: callLogs.ts 975->985 (correlation_id column on CallLogSummaryRow + read/map), usageHistory.ts 983->988 (correlationId metadata normalize), chat.ts 1575->1632 (withCorrelationId response wiring + combo-failure log carrying correlationId), chatHelpers.ts new 811 (withCorrelationId helper + reqId threading; was 791<cap pre-feature). Cohesive request/logging chokepoint wiring; structural shrink of chat.ts tracked in #3501.",
|
||||
"_rebaseline_2026_06_29_4038_cas_guard": "PR (#4038) own growth: tokenRefresh.ts 2103->2181 (+78 = the compare-and-swap guard on the refresh persist — runWithCasGuard/getActiveCasGuard AsyncLocalStorage pair mirroring runWithOnPersist, casGuardShouldSkipPersist that rereads the row right before persisting and skips the write when a concurrent writer already rotated the refresh_token past the one presented, plus getCasGuardStats counters). Fixes the sibling-rotation-revert → token-family-revocation storm. Gated behind an active guard (opt-in; no guard => byte-identical). Wiring lives at the two persist chokepoints inside getAccessToken; the comparison reuses wasRefreshTokenRotated from refreshSerializer. Not extractable without splitting the refresh hot path.",
|
||||
"_rebaseline_2026_06_29_5286_memoization": "PR #5286 own growth: strategySelector.ts 899->960 (+61 = the opt-in result-memoization branches in applyCompression/applyCompressionAsync — principal+determinism gate, makeMemoKey lookup/store with model+supportsVision folded into the key, recompute-with-memo-off). Default off (memoizeCompressionResults), so zero behavior change. The memo helpers live in the leaf resultMemo.ts (<cap); the chokepoint wiring here is not extractable. Structural shrink of this hot-path file tracked in #3501.",
|
||||
"_rebaseline_2026_07_01_v3843_release_5609": "Rebaseline v3.8.43 (PR #5609 release reconciliation). DRIFT dos 109 commits do ciclo: 8 god-files existentes cresceram (ApiManagerPageClient 2983->3017, combos/page 4594->4608, AddApiKeyModal 868->869, providerPageHelpers 974->996, chat.ts 1635->1647, auth.ts 2401->2403, batchProcessor 828->915, combo.ts 3368->3387) + 2 novos acima do cap (huggingchat.ts 813, tests web-cookie-providers-new 827) + 4 test files cresceram. Modularizacao deferida (blast-radius mid-release); congelado no estado atual p/ o proximo ciclo ratchetar daqui.",
|
||||
"_rebaseline_2026_07_02_5816_qoder": "PR #5816 (@AgentKiller45, qoder PAT via qodercli): qoderCli.ts 666->989, new-above-cap frozen (owner-approved baseline freeze). The growth is the legitimate PAT job-token exchange + quota parsing CLI transport (the pure-JS Cosy path 500'd on every PAT request); extracting the spawn/parse helpers now would just add indirection to a contributor PR mid-merge. Test frozen also raised for this PR's coverage growth: providers-page-utils.test.ts 1052->1092. Additionally clears an inherited base-red from the already-merged #5933 (codex json_schema->text.format): translator-openai-responses-req.test.ts 1097->1172 (+75 regression tests, no offending branch left). All remain frozen (cannot grow further); release captain's rebaseline-at-release supersedes.",
|
||||
"_rebaseline_2026_07_09_6126_clinepass_dual_auth": "PR #6126 (@hajilok, dual-auth ClinePass) own growth: tokenRefresh.ts 2181->2182 (+1 = a single `case \\\"clinepass\\\":` fallthrough label added to the existing `case \\\"cline\\\":` in _getAccessTokenInternal's provider switch, so clinepass token refresh dispatches to the already-shared refreshClineToken() instead of silently falling through to the generic OAuth refresh). Irreducible 1-line switch-case wiring at the existing chokepoint; the header-building logic for the same feature was extracted to a new leaf src/shared/utils/clineAuth.ts::buildClinepassHeaders() (well under cap) to avoid growing open-sse/executors/default.ts. Covered by tests/unit/clinepass-provider.test.ts.",
|
||||
"_rebaseline_2026_07_09_6363_kiro_external_idp": "PR #6363 (@artickc, Kiro external IdP) own growth: tokenRefresh.ts 2182->2249 (+67 = the external_idp refresh branch inside refreshKiroToken — standard public-client OAuth2 refresh_token grant against the org IdP tokenEndpoint via buildExternalIdpRefreshParams/isExternalIdpAuthMethod from the new leaf open-sse/services/kiroExternalIdp.ts, with invalid_grant/invalid_client -> unrecoverable_refresh_error mapping). Cohesive addition at the existing refreshKiroToken chokepoint. Covered by tests/unit/kiro-external-idp.test.ts.",
|
||||
"_rebaseline_2026_07_09_6587_kiro_api_key_auth": "PR #6587 (@strangersp) own growth for Kiro long-lived API-key auth, merged onto v3.8.47 tip: openai-to-kiro.ts 890->912 (+22, auth-header selection for API-key-vs-OAuth-token connections), providerLimits.ts 998->1000 (+2, API-key auth-type branch), translator-openai-to-kiro.test.ts 1234->1257 (+23), providers-page-utils.test.ts 1109->1107 (net -2 after merging with parallel release drift; connectionMatchesProviderCard api_key coverage added), provider-validation-specialty.test.ts 2856->2980 (+124 net after merge with parallel release drift; this PR also removed the file's `@typescript-eslint/no-explicit-any` eslint-suppression entry by fixing all `any` usages, adding typed replacements). Cohesive additive feature growth, well tested; not extractable without splitting the existing chokepoints mid-merge.",
|
||||
"_rebaseline_2026_07_09_6678_routing_strategy_9router": "#6678 (SeaXen) — 9router-parity Routing Strategy settings card + per-provider/combo sticky-round-robin override. Own growth: ProviderDetailPageClient.tsx 784->786 (single ProviderAccountRoutingCard mount + import), auth.ts 2448->2458 (providerStrategies override resolution: fallbackStrategy/stickyRoundRobinLimit per-provider cascade in getProviderCredentials). Both additive, zero unrelated refactor; new UI/logic lives in new files (ProviderAccountRoutingCard.tsx, RoutingStrategyCard.tsx, rrState.ts::resolveComboStickyRoundRobinLimit). chat.ts value below reflects the current release tip (grown by other concurrent PRs, e.g. #6640), not this PR own change.",
|
||||
"_rebaseline_2026_07_10_6318_omp_letta": "PR #6318 (@hamsa0x7, omp+letta CLI integrations) own growth: cliTools.ts (+53 = 2 registry entries incl. omp docsUrl) and cliRuntime.ts (+18 = runtime-detection wiring for the 2 new tools). Cohesive registry/wiring growth at the existing chokepoints; scope reduced from the original 5 tools (pi/codewhale/jcode shipped separately).",
|
||||
"_rebaseline_2026_07_10_gcf_v3_2_decode": "PR #6838 own growth: new vendored file open-sse/services/compression/engines/headroom/gcf/decode_generic.ts frozen at 880 (> 800 cap). It is the vendored GCF generic-profile decoder (spec v3.2 nested flattening plus the prototype-pollution / hasOwnProperty hardening added in this PR's Gemini review). Kept as one file faithful to upstream gcf-typescript so re-vendoring stays a clean copy rather than a re-split each cycle (sibling generic.ts/scalar.ts stay < cap; extraction would also fragment the file's frozen eslint no-explicit-any suppressions). Round-trip + prototype-pollution regression coverage in tests/unit/compression/headroom-smartcrusher.test.ts. Frozen: only shrinks from here.",
|
||||
"_rebaseline_2026_07_12_v3847_mergeprs_tail": "v3.8.47 /merge-prs tail (owner-approved): src/lib/localDb.ts NEW>800 (799->805, +6 re-exports countFreeProxies + recordFreeProxySyncErrors/clearFreeProxySyncErrors/getFreeProxySyncErrors + FreeProxySyncErrors type for #6909 free-pool relay-repair; re-export-only per Hard Rule #2, not extractable).",
|
||||
"_rebaseline_2026_07_15_7070_combos_memo": "PR #7070 (perf/p1-memo) own growth: src/app/(dashboard)/dashboard/combos/page.tsx 4655->4656 (+1 = React.memo wrapping of ComboCard). Covered by tests/unit/ui/combos-page-smoke.test.tsx.",
|
||||
"_rebaseline_2026_07_18_7399_xai_oauth_modal": "PR #7399 (xAI OAuth PKCE) own growth: OAuthModal.tsx 993->998 (+5 = provider entry + PKCE flow branch wiring at the existing provider-switch chokepoint; the provider logic itself lives in src/lib/oauth/providers/xai-oauth.ts, new leaf). Third irreducible wiring bump on this modal (969->989->993->998); structural shrink tracked in #3501.",
|
||||
"_rebaseline_2026_07_19_6636_codex_session_json": "#6636 own growth: OAuthModal.tsx 998->1030 (gate units, split(\\\"\\\\n\\\").length incl. trailing newline; +32 = session-JSON paste branch for handleManualSubmit plus a shared submitCodexAccessToken() helper extracted from the pre-existing bare-JWT branch, mirroring the #5203 oauthBlobSubmit.ts extraction precedent; the normalizer logic itself lives in the new src/lib/oauth/utils/codexSessionImport.ts leaf module, not here). Fourth irreducible wiring bump on this modal (969->989->993->998->1030); structural shrink tracked in #3501.",
|
||||
"_rebaseline_2026_07_19_7546_ghe_copilot_modal": "PR #7546 (GHE Copilot OAuth provider) own growth: OAuthModal.tsx 1030->1056 (gate units). Adds a gheUrl input state, routes ghe-copilot through the existing device-code branch, and threads gheUrl into the device-code request/poll extraData at the existing provider-switch chokepoints (+~24 lines, cohesive with the same pattern as #7399/#6636). The standalone GHE enterprise-URL config step JSX (originally +31 lines inline) was extracted to the new src/shared/components/oauthModal/GheConfigStep.tsx leaf component to minimize the bump; what remains is the irreducible provider-branch wiring. Fifth bump on this modal (969->989->993->998->1030->1056); structural shrink tracked in #3501.",
|
||||
"_rebaseline_2026_07_19_7787_ic2_localdb_reexports": "PR #7787 (IC2 raw connections cache + lazy-decrypt) own growth: localDb.ts 805->807 (gate units, +2). localDb.ts is the re-export-only layer (hard rule #2 — no logic); the PR adds 4 new db/readCache re-exports (touchConnectionLastUsed, getCachedRawProviderConnections, getCachedProviderConnectionById, getCachedProviderNodes) required by existing barrel importers. Irreducible for a re-export list; frozen so it can only shrink.",
|
||||
"_rebaseline_2026_07_20_7779_routingcombo_thread": "PR #7779 own growth: chatHelpers.ts 876->877 (+1, thread routingComboId into executeChatWithBreaker for compression-combo assignment). Frozen so it can only shrink.",
|
||||
"_rebaseline_2026_07_20_7819_autocandidateoverrides_reexport": "PR for #7819 (Level 1+2: read-only auto/* candidate transparency + per-API-key exclusions) own growth: localDb.ts 807->808 (+1). Adds a single `export * from \\\"./db/autoCandidateOverrides\\\"` barrel re-export (hard rule #2 — no logic) for the new DB module backing per-apiKey candidate exclusions. Irreducible for a re-export list; frozen so it can only shrink.",
|
||||
"_rebaseline_2026_07_21_8027_grok_cli_auth_json_paste": "PR #8027 (RaviTharuma, fix(grok-cli) #7610) own growth: OAuthModal.tsx 1080->1100 (gate units). Requires the full ~/.grok/auth.json (with refresh_token) on the paste-import path instead of a bare JWT, at the existing paste-token chokepoint (renamed tab label, updated instructions/placeholder, textarea for the auth.json blob, inline error surface). The validation logic itself (parseGrokCliPasteToken, previously an inline ~75-line function) was extracted to the new src/lib/oauth/utils/grokCliAuthJson.ts leaf module — mirroring the #6636/#7546 extraction precedent — so only the irreducible UI wiring remains here. Sixth bump on this modal (969->989->993->998->1030->1056->1100); structural shrink tracked in #3501.",
|
||||
"_rebaseline_2026_07_21_8034_compression_exclusions_sidebar": "#8034 (compression exclusions dashboard tab) own growth: sections.ts 796->806 (+10, one new COMPRESSION_CONTEXT_GROUP sidebar item linking /dashboard/compression/exclusions). The file was already 796/800 before this PR (organic growth from prior sidebar entries), so a single new nav item pushed it 6 lines over cap. Freezing at 806 (cannot grow further); the sidebar item array is data, not extractable logic.",
|
||||
"_rebaseline_2026_07_22_7936_namespace_roundtrip": "#7936 (@RCrushMe, Responses-Chat namespace round-trip identity seam) own growth: open-sse/translator/response/openai-responses.ts 1092->1125 (+33) and open-sse/utils/stream.ts 2814->2869 (+55) — threading the namespace-identity seam through the Responses↔Chat translation + stream paths so tool-call namespaces survive the round-trip. Cohesive translation/stream wiring at existing chokepoints, frozen at new size.",
|
||||
"_rebaseline_2026_07_22_8010_codex_responses_engine": "PR #8010 (@JxnLexn) own growth: open-sse/mcp-server/schemas/tools.ts 1497->1505 (+8 = threading the new \\\"codex-responses\\\" literal into the compressionConfigureInput strategy/autoTriggerMode Zod enums and setCompressionEngineInput engine enum, mirroring the existing rtk/omniglyph enum entries; no new tool). open-sse/services/compression/strategySelector.ts 1043->1054 (+11 = one new `if (mode === \\\"codex-responses\\\")` dispatch branch in runCompression that delegates 100% to the new codexResponsesEngine.apply, mirroring the existing rtk single-mode dispatch, plus threading config.codexResponsesConfig.preserveToolNames into the shared adaptBodyForCompression call at the 3 existing call sites). src/lib/db/compression.ts (untracked, new-file cap 800) 794->845 (+51 = normalizeCodexResponsesConfig, mirroring the existing normalizeRtkConfig normalizer, plus registering \\\"codex-responses\\\" in the COMPRESSION_MODES/STACKED_PIPELINE_ENGINE_IDS/SINGLE_MODE_ENGINE sets and the getCompressionSettings load/save switch) — added to the baseline at its current size. All three are cohesive dispatch/normalizer wiring at existing chokepoints (mirroring the prior compression-mode rebaselines #6534/#6556), not extractable without hiding the mode-dispatch boundary. Covered by tests/unit/compression/codex-responses.test.ts (6) + omniglyph-registries.test.ts/types.test.ts (22, updated for the new mode).",
|
||||
"_rebaseline_2026_07_22_8034_compression_exclusions_persistence": "#8034 (compression exclusions) own growth: src/lib/db/compression.ts 845->850 (+5 = threading the new compressionExclusions field through the existing getCompressionSettings/saveCompressionSettings load/save switch over the shared key_value compression namespace — no new table, no raw SQL). Mirrors the prior compression-field rebaselines (#8010 codex-responses normalizer at the same chokepoint); the load/save switch is a single dispatch boundary, not extractable without hiding it. Covered by the PR's 8 node:test + 3 vitest cases.",
|
||||
"_rebaseline_2026_07_22_8050_model_lockout_exact_family": "#8050 (@AndrianBalanescu) own growth: accountFallback.ts 1864->1892 (+28) — exact-vs-family model-lockout scoping (getModelLockKey/isModelLocked/clearModelLock/getModelLockoutInfo) so an Antigravity 404 for one bare model no longer hijacks the whole family cooldown. Cohesive lockout logic; frozen at new size.",
|
||||
"_rebaseline_2026_07_22_8081_reasoning_placeholder_guard": "#8081 (@Dingding-leo) own growth: openai-responses.ts 1125->1137 (+12) restructuring the reasoning-placeholder guard so it skips only the empty content block and still emits finish_reason/tool_calls in the same chunk. Cohesive translator wiring; frozen at new size.",
|
||||
"_rebaseline_2026_07_22_8210_openrouter_midstream_error": "PR #8210 (hartmark, fix/openrouter-midstream-error-surfacing) own growth: open-sse/translator/response/openai-responses.ts 1137->1163 (+26) measured on the merged tip (release 1137 + this PR own growth). Adds a single new branch inside openaiToOpenAIResponsesResponse() that detects an OpenRouter-style mid-stream aggregator error (HTTP 200 SSE chunk with empty choices + a top-level error object) and surfaces it as state.upstreamError instead of silently falling through to the no-op/awaitingTrailingUsage path, which previously masked the failure as a false empty-success completion and skipped combo fallback. Irreducible call-site addition at the existing chunk-dispatch chokepoint (mirrors the Gemini-to-OpenAI translator's #4177 precedent for the same class of upstream error surfacing). Note: this baseline entry does NOT cover the separate pre-existing +11 drift already on the release tip from #8081/#8162 (1125->1136, unrelated reasoning-placeholder-stripping fix merged after this PR branched) — that drift belongs to the maintainer's rebaseline, not this PR.",
|
||||
"_rebaseline_2026_07_22_8211_gemini_malformed_tool_choice": "PR #8211 (hartmark, fix/gemini-malformed-function-call-tool-choice) own growth: open-sse/translator/response/gemini-to-openai.ts 771->821 (+50, entirely this PR's diff — no other commit touched this file between the PR's merge-base and the release tip). Adds MALFORMED_FUNCTION_CALL/UNEXPECTED_TOOL_CALL handling inside geminiToOpenAIResponse(): synthesizes a `malformed_tool_call` tool_calls entry so finish_reason normalizes to the standard \\\"tool_calls\\\" instead of an unrecognized raw enum value that OpenAI-compatible clients (e.g. OpenClaw) silently ignore, and always synthesizes (rather than skipping when a real tool call already exists) so a malformed attempt alongside a real one in the same turn is not silently discarded. Irreducible cohesive addition at the existing candidate/finishReason translation chokepoint (mirrors the 9router#2462 raw-finish-reason precedent immediately below it in the same function). Covered by the PR's own tests/unit test additions for both the malformed-only and malformed-plus-real-call cases.",
|
||||
"_rebaseline_2026_07_22_8213_chat_abandoned_target_abort": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: src/sse/handlers/chat.ts 1794->1860 (+66, measured against the PR's own merge-base — the release tip separately carries an unrelated -5 net shrink from #8013's antigravity callable-catalog alignment, which this PR's branch does not include and this entry does not cover). Adds resolveDispatchClientRawRequest(): merges a per-target modelAbortSignal into clientRawRequest.signal (via mergeAbortSignals) so a combo target abandoned by comboTargetTimeoutMs actually observes its own abort and reaches its cleanup path, instead of hanging forever inside withRateLimit/acquireAccountSemaphore and leaking a permanent 'pending' dashboard entry (live incident, log id 1784418258231-14961a). Also wires combo-exhausted rejection logging to capture request body + attempted models via the new rejectedRequestUsage helper. Irreducible additions at the existing chat dispatch chokepoint. Covered by the PR's own combo-config + integration test additions.",
|
||||
"_rebaseline_2026_07_22_8213_combo_cooldown_wait_recording": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: open-sse/services/combo.ts 3548->3604 (+56, entirely this PR's diff — no other commit touched this file between the PR's merge-base and the release tip). Fixes combo cooldown-wait state recording so a bogus 503 is no longer crystallized when the cooldown-wait vars reset every setTry, adds an OpenAI-format SSE error frame path for combo-exhausted rejections (capturing request body + attempted models), and gives an abandoned per-target dispatch its own timeout instead of leaking a permanent 'pending' dashboard entry. Irreducible additions at the existing handleComboChat dispatch/retry chokepoint (mirrors the prior quota-share/headroom/task-aware strategy-branch precedents already frozen in this file). Covered by the PR's own combo-config + Gemini TPM-ceiling benchmark test additions.",
|
||||
"_rebaseline_2026_07_22_8213_gemini_tpm_quota_cooldown_wait": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: open-sse/services/accountFallback.ts 1857->1932 on the merged tip (release 1892 incl #8050 +35, plus this PR own growth +40); measured against the PR own merge-base was 1857->1898 (+41 — the release tip separately carries an unrelated +34 from #8050's antigravity 404 model-not-found lockout scoping, which this PR's branch does not include and this entry does not cover). Own growth is the Gemini TPM-ceiling classification + cooldown-wait wiring feeding into the combo cooldown-wait state machine (rate-limit wedge recovery) introduced by this PR's commit series. Irreducible additions at the existing account-fallback/model-lockout chokepoint. Covered by the PR's own gemini-rate-limit-tracker and TPM-ceiling benchmark test additions.",
|
||||
"_rebaseline_2026_07_22_8213_health_unblock_model_cooldowns": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: src/app/(dashboard)/dashboard/health/page.tsx 1094->1165 (+71, entirely this PR's diff — no other commit touched this file between the PR's merge-base and the release tip). Adds handleUnblockAll/handleUnblockOne dashboard actions (DELETE /api/resilience/model-cooldowns) so an operator can manually clear a Gemini TPM-wedge model lockout surfaced by this PR's cooldown-wait fixes, instead of waiting out the ceiling. Irreducible UI wiring at the existing health-page action chokepoint. Covered by the PR's own dashboard/resilience test additions.",
|
||||
"_rebaseline_2026_07_22_8213_requestloggerdetail_unblock_ui": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: src/shared/components/RequestLoggerDetail.tsx 799->941 (+142, entirely this PR's diff — no other commit touched this file between the PR's merge-base and the release tip; crosses the general 800-line new-file cap so is frozen here for the first time). Adds a collapsible section header (open/expand-less toggle) plus per-log-entry unblock (`unblocking`/`cleared` state, isCombo503 detection) so the request-logger detail panel surfaces the same Gemini TPM cooldown-wait / model-lockout unblock action introduced by this PR at the individual-request level (mirrors the health-page bulk unblock action added in the same PR). Covered by the PR's own dashboard/resilience test additions.",
|
||||
"_rebaseline_2026_07_22_fusion_8013_8098_antigravity": "Fusion of #8013 (backryun, catalog/IDE-CLI-split rewrite) + #8098 (nguyenha935, protocol-fidelity/fail-closed/credits/tool-cloaking): open-sse/services/usage/antigravity.ts NEW 802 (>cap 800, +2 — #8098 credits/tier usage service on #8013's profile-aware headers). Test growth (models-catalog-route 1605->1608, provider-models-route 1752->1757 from #8013 Gemini 3.6 catalog) tracked in testFrozen.",
|
||||
"_rebaseline_2026_07_23_8127_grok_weekly_quota": "#8127 (@apoapostolov) own growth: src/sse/handlers/chat.ts 1861->1865 (+4) — weekly quota tracking for grok-web wires a quota-fetch hook at the existing dispatch chokepoint. Thin wiring mirroring adjacent provider-quota branches; not extractable. Covered by tests/unit/grok-quota-fetcher.test.ts.",
|
||||
"_rebaseline_2026_07_23_8143_empty_catch_logging": "#8143 (@chirag127) own growth: open-sse/utils/stream.ts 2869->2887 (+18) — replacing empty catch blocks in the SSE stream subsystem with console.debug logging (Rule #6 silent-swallow fix, issues #8138-#8142). Cohesive logging additions at the existing catch chokepoints, not extractable; frozen at new size. Covered by tests/unit/stream-handler-catch-logging-8143.test.ts.",
|
||||
"_rebaseline_2026_07_23_8219_cache_ttl_settings_sidebar": "#8219 (@oyi77) own growth: sections.ts 806->813 (+7) — configurable model-catalog cache-TTL settings adds a new sidebar nav entry + its visibility wiring. Sidebar item array is data, not extractable logic; frozen at new size.",
|
||||
"_rebaseline_2026_07_23_8247_8248_model_unhealthy": "#8247+#8248 own growth: accountFallback.ts 1940->1941 (+1, irreducible import statement only — the substantive #8248 DEGRADED-pattern classifier was extracted into open-sse/config/errorConfig.ts, which has ample headroom, instead of growing this frozen file; #8247's fix is a single existing-line condition change, net zero lines). Scoping the credits-exhausted 403/429 branch to isCompatibleProvider() (per-model-quota openai/anthropic-compatible-* nicknames) so it stays model-scoped instead of terminalling the whole connection, and classifying NVIDIA NIM 'Function ... DEGRADED' 400 bodies as model-access-denied instead of a raw passthrough 400. Covered by tests/unit/8247-accountfallback-model-unhealthy.test.ts and tests/unit/8248-accountfallback-nvidia-degraded.test.ts.",
|
||||
"_rebaseline_2026_07_23_8252_combo_400_advance": "#8252 (@RaviTharuma) own growth: accountFallback.ts 1932->1940 (+8) + combo.ts 3604->3630 (+26) — advance combo on model-scoped 400s wrapped as invalid/Bad-Request. Irreducible wiring at existing account-fallback + combo dispatch chokepoints. Covered by combo-model-scoped-400-advance.test.ts.",
|
||||
"_rebaseline_2026_07_23_8266_alibaba_media": "#8266 (@backryun) own growth: imageRegistry.ts 821->979 (+158) — Alibaba-family media models (Qwen image/video, Bailian, Wan) added to the image/video registry. Registry model data, not extractable logic; frozen at new size.",
|
||||
"_rebaseline_2026_07_24_8388_compression_detail_persist": "#8388 (compression engine DETAIL settings — Headroom/session-dedup/CCR — dropped on save) own growth: src/lib/db/compression.ts 866->872 (+6 = irreducible call-site wiring at the existing getCompressionSettings/updateCompressionSettings chokepoint: one import line, one `...buildDetailConfigDefaults()` spread in the seed config, and one `case \\\"sessionDedup\\\": case \\\"ccr\\\": applyDetailConfigUpdate(config, key, parsed); break;` load-switch case, mirroring the existing headroom/#8056 case immediately above it). The actual normalizer logic (normalizeSessionDedupConfig/normalizeCcrConfig, matching SESSION_DEDUP_SCHEMA/CCR_SCHEMA bounds) was EXTRACTED into a new leaf src/lib/db/compressionDetailNormalizers.ts (well under cap) so this frozen file only carries the minimal dispatch wiring. Covered by tests/unit/8388-compression-detail-persist.test.ts (schema-accept + full DB save->reload round-trip for both new sub-objects, plus a no-regression assertion on the existing headroom round-trip).",
|
||||
"_rebaseline_2026_07_24_responses_toolcalls_log_summary": "hartmark, fix/responses-tool-calls-log-summary own growth: open-sse/translator/response/openai-responses.ts 1163->1174 (+11). closeToolCall() now also writes the completed tool call into the shared state.toolCalls Map (already populated by the openai-to-claude / claude-to-openai / gemini-to-openai response translators) so stream.ts's completion-log summary builder (which reads state.toolCalls, not this translator's own funcCallIds/funcNames/funcArgsBuf bookkeeping) reports finish_reason \\\"tool_calls\\\" and message.tool_calls for openai->openai-responses translated streams instead of always logging \\\"stop\\\" with no tool_calls — the actual client-facing SSE events were already correct; only the persisted call-log summary was wrong. Irreducible call-site addition at the existing tool-call-close chokepoint. Covered by the new regression test in tests/unit/translator-resp-openai-responses.test.ts.",
|
||||
"_rebaseline_2026_07_25_8476_combo_input_bound_homogeneous_scope": "PR #8476 (herjarsa, fix/8375-8459-combo-image-fixes, #8375) own growth: open-sse/services/combo.ts 3642->3679 (+37 net: +29 the PR's own isInputBoundFailure short-circuit for deterministic context_length_exceeded/context_window_exceeded failures, +8 a /green-prs pre-merge fix scoping that short-circuit to homogeneous remainders only — the shipped code fired unconditionally on ANY target, regressing the intentional heterogeneous-combo fallback #6637/isContextOverflow400 protects, exactly as flagged by this PR's own review evidence but never actually implemented in the branch). The fix compares orderedTargets[i+1..] modelStr against the failing target's modelStr at the existing executeTarget dispatch chokepoint (mirrors the sameProviderNext precedent a few lines below) — irreducible call-site wiring, not extractable without hiding the dispatch boundary. Covered by tests/unit/combo-input-bound-failure-8375.test.ts (homogeneous pool still short-circuits) and the new tests/unit/combo-input-bound-heterogeneous-8375.test.ts (heterogeneous combo now correctly falls through to the larger-context target).",
|
||||
"_rebaseline_2026_07_25_adobe_firefly_reference_images": "Follow-up to #8006: storage upload + referenceBlobs for image/video and /v1/images/edits dispatch. adobeFireflyClient.ts 1958->2317 (+upload helpers, extract sources, resolve blob ids). Note: 2317 not 2316 — check-file-size.mjs counts LOC via split(\\\"\\\\n\\\").length (counts the trailing-newline empty element), which is 1 higher than `wc -l` on a file ending in \\\\n; the PR's original entry (2316) was measured with wc -l and undercounted by 1 against the actual gate.",
|
||||
"_rebaseline_pr1043_minimax_tts": "Upstream port decolua/9router#1043 (toanalien) own growth: audioSpeech.ts 965->1061 (+96). Adds MiniMax T2A v2 TTS dispatch (handleMinimaxSpeech + hexToBytes helper) — provider entry was already in audioRegistry (format: minimax-tts) but no handler existed, falling through to the OpenAI-compatible default that fails (T2A has custom shape + hex-encoded audio + base_resp envelope). New branch sits next to the other inline provider branches (xiaomi-mimo, coqui, tortoise, aws-polly) — extracting would just create indirection. Covered by tests/unit/minimax-tts-1043.test.ts (3 tests, GREEN: success, base_resp error, invalid-hex).",
|
||||
"_rebaseline_pr4592_exclude_exhausted_auto": "Reconcile #4592 already-merged growth: combo.ts 2991->3036 (+45, terminal-status quota-cutoff exclusion in buildAutoCandidates + opt-in gate). Fast-gate PR->release does not run check:file-size.",
|
||||
"open-sse/executors/antigravity.ts": "1528",
|
||||
"open-sse/executors/base.ts": "1640",
|
||||
"open-sse/executors/chatgpt-web.ts": "3241",
|
||||
"open-sse/executors/codex.ts": "1562",
|
||||
"open-sse/executors/cursor.ts": "1563",
|
||||
"open-sse/executors/deepseek-web.ts": "1148",
|
||||
"open-sse/executors/grok-web.ts": "1044",
|
||||
"open-sse/executors/muse-spark-web.ts": "1405",
|
||||
"open-sse/handlers/chatCore.ts": "5034",
|
||||
"open-sse/handlers/imageGeneration.ts": "3101",
|
||||
"open-sse/handlers/responseSanitizer.ts": "1128",
|
||||
"open-sse/handlers/search.ts": "1536",
|
||||
"open-sse/handlers/videoGeneration.ts": "1063",
|
||||
"open-sse/mcp-server/schemas/tools.ts": "1553",
|
||||
"open-sse/mcp-server/server.ts": "1448",
|
||||
"open-sse/mcp-server/tools/advancedTools.ts": "1120",
|
||||
"open-sse/services/accountFallback.ts": "1978",
|
||||
"open-sse/services/adobeFireflyClient.ts": "2385",
|
||||
"open-sse/services/claudeCodeCompatible.ts": "1202",
|
||||
"open-sse/services/combo.ts": "3648",
|
||||
"open-sse/services/compression/strategySelector.ts": "1060",
|
||||
"open-sse/services/rateLimitManager.ts": "1167",
|
||||
"open-sse/translator/response/openai-responses.ts": "1204",
|
||||
"open-sse/utils/cursorAgentProtobuf.ts": "1505",
|
||||
"open-sse/utils/stream.ts": "2889",
|
||||
"src/app/(dashboard)/dashboard/HomePageClient.tsx": "1388",
|
||||
"src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx": "1031",
|
||||
"src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx": "3117",
|
||||
"src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx": "1067",
|
||||
"src/app/(dashboard)/dashboard/combos/page.tsx": "4703",
|
||||
"src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx": "1283",
|
||||
"src/app/(dashboard)/dashboard/costs/quota-share/components/PoolWizard.tsx": "1022",
|
||||
"src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": "2615",
|
||||
"src/app/(dashboard)/dashboard/health/page.tsx": "1165",
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": "1324",
|
||||
"src/app/(dashboard)/dashboard/providers/page.tsx": "1944",
|
||||
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": "1201",
|
||||
"src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": "1019",
|
||||
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": "1470",
|
||||
"src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": "1123",
|
||||
"src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": "1629",
|
||||
"src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": "1573",
|
||||
"src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx": "1028",
|
||||
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": "2148",
|
||||
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": "1119",
|
||||
"src/app/api/providers/[id]/models/route.ts": "2361",
|
||||
"src/app/api/v1/models/catalog.ts": "1590",
|
||||
"src/lib/tokenHealthCheck.ts": "1053",
|
||||
"src/lib/db/apiKeys.ts": "1529",
|
||||
"src/lib/db/core.ts": "1639",
|
||||
"src/lib/db/migrationRunner.ts": "1094",
|
||||
"src/lib/db/models.ts": "1097",
|
||||
"src/lib/db/providers.ts": "1034",
|
||||
"src/lib/memory/retrieval.ts": "1073",
|
||||
"src/lib/tailscaleTunnel.ts": "1202",
|
||||
"src/lib/usage/providerLimits.ts": "1013",
|
||||
"src/shared/components/OAuthModal.tsx": "1134",
|
||||
"src/shared/components/RequestLoggerV2.tsx": "1629",
|
||||
"src/shared/components/analytics/charts.tsx": "1035",
|
||||
"src/shared/services/cliRuntime.ts": "1122",
|
||||
"src/sse/handlers/chat.ts": "1904",
|
||||
"src/sse/services/auth.ts": "2508",
|
||||
"tests/unit/account-fallback-service.test.ts": "1572",
|
||||
"tests/unit/provider-validation-specialty.test.ts": "2985",
|
||||
"open-sse/executors/hyperagent.ts": "1026",
|
||||
"open-sse/executors/default.ts": "1042",
|
||||
"open-sse/executors/kiro.ts": "1069",
|
||||
"open-sse/translator/request/openai-to-kiro.ts": "1057",
|
||||
"open-sse/utils/sseHeartbeat.ts": "142",
|
||||
"_rebaseline_2026_08_04_9305_sse_comments": "#9305 fix: broadened sseCommentsEnabled()"
|
||||
"_rebaseline_2026_08_07_v3850_sweepreds_pr2_toolnamemap": "tests/unit/translator-openai-to-gemini.test.ts 1616->1619 (+3). O frozen estava EXATAMENTE no tamanho da base, entao qualquer linha nova viola. #9568 (c9a3361e5a) fez buildChangedToolNameMap emitir entradas IDENTIDADE (o Gemini minusculiza nomes de tool nas respostas, entao o tradutor de resposta precisa da chave para mapear de volta), o que passou a incluir `_toolNameMap` no envelope Antigravity de qualquer request com tools. As 3 linhas sao: a chave nova na lista esperada de Object.keys, 1 comentario explicando POR QUE ela aparece (sem ele o proximo leitor tenta remove-la de novo) e 1 assert do CONTEUDO do map — presenca de chave sozinha nao provaria a entrada identidade, que e justamente o comportamento novo. Nao ha o que extrair: e alinhamento de contrato dentro de um teste existente."
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: "Radar Free-Model Catalog"
|
||||
version: 3.8.50
|
||||
lastUpdated: 2026-08-07
|
||||
lastUpdated: 2026-08-08
|
||||
---
|
||||
|
||||
# Radar Free-Model Catalog
|
||||
|
||||
> **Source of truth:** `src/lib/radar/`, `src/lib/db/radar.ts`, `src/app/api/radar/`
|
||||
> **Last updated:** 2026-08-07 — v3.8.50
|
||||
> **Last updated:** 2026-08-08 — v3.8.50
|
||||
|
||||
Radar is an **optional add-on** that overlays a signed, freshly-curated free-model
|
||||
catalog on top of the release baseline (`FREE_MODEL_BUDGETS` in
|
||||
@@ -23,6 +23,23 @@ below.
|
||||
|
||||
---
|
||||
|
||||
## Delivery status in v3.8.50
|
||||
|
||||
The following status distinguishes what this OSS release implements from later Radar
|
||||
workstreams. It is a code-level status, not a promise that a particular hosted deployment
|
||||
or external integration is currently available.
|
||||
|
||||
| Area | Status in this release |
|
||||
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Signed catalog client | Implemented behind `RADAR_ENABLED`, with separate opt-in, Ed25519 verification, local encrypted settings/cache, non-destructive overlay, scheduler, and dashboard. |
|
||||
| Contributor activation | The dashboard links to the server-hosted GitHub claim flow and accepts an existing `omr_…` key. Contributor eligibility is resolved by the private service; the OSS client contains no GitHub token or issuance logic. |
|
||||
| Supporter-key activation | Implemented. The raw key is validated, encrypted at rest, masked on reads, and sent only by the server-side sync. Changing or clearing the key invalidates both entitlement-sensitive feed caches. |
|
||||
| Referral links | Implemented as a separately signed, hourly-refreshed feed. Fixed links are available to the community tier immediately; limited campaigns remain live-tier data. |
|
||||
| Payments and transactional email | Not implemented in the OSS client. Purchase, donation, receipt review, and mail delivery belong to the private service and its later operational workstream. |
|
||||
| Research-agent workstream | Not part of this client release. Curated feed contents remain server-side data; no autonomous research agent runs in an OmniRoute installation. |
|
||||
|
||||
---
|
||||
|
||||
## Flag: `RADAR_ENABLED` (default off)
|
||||
|
||||
Radar is gated end-to-end by the `RADAR_ENABLED` feature flag
|
||||
@@ -67,7 +84,8 @@ When both are on, the sync path is:
|
||||
plain, unauthenticated-by-default GET. OmniRoute never posts usage data, provider
|
||||
configuration, or model traffic to the feed service.
|
||||
3. The response is verified, validated, and cached locally (see
|
||||
[Security model](#security-model)). Nothing else touches the network for Radar.
|
||||
[Security model](#security-model)). Radar has exactly two server-side network paths:
|
||||
`syncRadar()` for the catalog and `syncRadarReferrals()` for the standalone referrals feed.
|
||||
|
||||
The **supporter key** is an optional Bearer token (`radar_settings.supporter_key`)
|
||||
that lets the feed service decide which tier to serve (see
|
||||
@@ -77,6 +95,9 @@ that lets the feed service decide which tier to serve (see
|
||||
helpers (`src/lib/db/encryption.ts`) used for provider credentials.
|
||||
- Set via `POST /api/radar/settings` (`{ supporterKey: "omr_" + 40 hex chars }`) and
|
||||
**never echoed back** — the response returns a masked form (`omr_****abcd`).
|
||||
- Changing or clearing it atomically invalidates both the catalog and referrals caches. The
|
||||
next sync/read resolves the new entitlement server-side; saving a key does not itself make
|
||||
a network request or consume a single-use activation key.
|
||||
- Sent to the feed service as a Bearer token on the sync GET — nothing else about the
|
||||
key ever leaves the client.
|
||||
|
||||
@@ -102,10 +123,10 @@ pattern as `RADAR_FEED_URL`) and relayed to the dashboard through the existing
|
||||
`GET /api/radar/settings` response (`contributorClaimUrl`, `supporterPlansUrl`) — the
|
||||
client component never reads `process.env` itself.
|
||||
|
||||
| Var | Purpose |
|
||||
| -------------------------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| `RADAR_CONTRIBUTOR_CLAIM_URL` | Overrides the contributor-claim URL (default `https://radar.omniroute.online/auth/github`). |
|
||||
| `RADAR_SUPPORTER_PLANS_URL` | Overrides the supporter-plans URL (default `https://radar.omniroute.online/planos`). |
|
||||
| Var | Purpose |
|
||||
| ----------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| `RADAR_CONTRIBUTOR_CLAIM_URL` | Overrides the contributor-claim URL (default `https://radar.omniroute.online/auth/github`). |
|
||||
| `RADAR_SUPPORTER_PLANS_URL` | Overrides the supporter-plans URL (default `https://radar.omniroute.online/planos`). |
|
||||
|
||||
Once a visitor has a key (`omr_` + 40 hex chars), the activation screen
|
||||
(`src/app/(dashboard)/dashboard/radar/page.tsx`) has a paste-key input as the primary
|
||||
@@ -117,7 +138,7 @@ as a UX nicety; the server's Zod schema is the authoritative check either way. O
|
||||
key is set, the activation screen shows the masked form (`supporterKeyMasked` from
|
||||
`GET /api/radar/settings`) instead of an empty input, with a "change key" control to
|
||||
paste a new one — the raw key is never redisplayed. The two claim/plans buttons above
|
||||
remain the way to *obtain* a key in the first place; this input is where an operator
|
||||
remain the way to _obtain_ a key in the first place; this input is where an operator
|
||||
who already has one activates it.
|
||||
|
||||
---
|
||||
@@ -207,11 +228,11 @@ handle.
|
||||
### The served tier comes from a response header, not the signed body
|
||||
|
||||
The signed feed **body**'s `tier` field is always `"live"` — the feed service ships
|
||||
**one signed artifact per version**, so the body cannot carry a per-request tier
|
||||
without invalidating the Ed25519 signature (re-signing per request would defeat the
|
||||
point of a pinned, cacheable, verifiable artifact). The tier actually served for a
|
||||
given request is instead carried in the **`x-omniroute-feed-tier` response header**,
|
||||
decided server-side from the request's `Authorization` key.
|
||||
**two signed artifacts per version**: live includes current campaigns and community
|
||||
omits them. Each artifact is signed over its own exact bytes. The body still does not
|
||||
serve as the entitlement decision; the tier actually selected for a request is carried
|
||||
in the **`x-omniroute-feed-tier` response header**, decided server-side from the request's
|
||||
`Authorization` key.
|
||||
|
||||
`syncRadar()` (`src/lib/radar/sync.ts::parseServedTierHeader()`) is the single place
|
||||
that resolves the tier a client should trust:
|
||||
@@ -223,7 +244,7 @@ that resolves the tier a client should trust:
|
||||
2. Fall back to the signed body's `tier` field (always `"live"`) only when step 1
|
||||
yields nothing.
|
||||
3. The resolved tier is what gets cached and returned as `{ status: "updated",
|
||||
version, tier }` — this is the value the dashboard shows, never the raw body
|
||||
version, tier }` — this is the value the dashboard shows, never the raw body
|
||||
field.
|
||||
|
||||
---
|
||||
@@ -265,18 +286,18 @@ Every merged entry carries an `origin` field the UI renders as a badge:
|
||||
|
||||
Five local routes back the UI, all under `src/app/api/radar/`:
|
||||
|
||||
| Route | Method | Purpose |
|
||||
| ----------------------- | ------ | -------------------------------------------------------------------------------------------------- |
|
||||
| `/api/radar/catalog` | GET | Returns the merged catalog (`getRadarCatalog()`) from the local cache. |
|
||||
| `/api/radar/sync` | POST | Triggers `syncRadar()` server-side; returns the resulting status. |
|
||||
| `/api/radar/settings` | GET | Returns `{ optIn, hasSupporterKey, supporterKeyMasked }` — never the raw key. |
|
||||
| `/api/radar/settings` | POST | Sets opt-in and/or the (encrypted) supporter key. |
|
||||
| `/api/radar/referrals` | GET | Returns `{ fixed, campaigns, tier }` from the local cache — see [Referral links](#referral-links-free-credits) below. |
|
||||
| Route | Method | Purpose |
|
||||
| ---------------------- | ------ | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| `/api/radar/catalog` | GET | Returns the merged catalog (`getRadarCatalog()`) from the local cache. |
|
||||
| `/api/radar/sync` | POST | Triggers `syncRadar()` server-side; returns the resulting status. |
|
||||
| `/api/radar/settings` | GET | Returns `{ optIn, hasSupporterKey, supporterKeyMasked }` — never the raw key. |
|
||||
| `/api/radar/settings` | POST | Sets opt-in and/or the (encrypted) supporter key. |
|
||||
| `/api/radar/referrals` | GET | Returns `{ fixed, campaigns, tier }` from the local cache — see [Referral links](#referral-links-free-credits) below. |
|
||||
|
||||
**Hard rule: these routes never proxy the feed service.** The browser only ever talks
|
||||
to the local OmniRoute server; `syncRadar()` is the single module in the whole client
|
||||
that touches the network for Radar (`src/lib/radar/sync.ts`), and it always runs
|
||||
server-side, never client-side. This keeps the feed URL and any supporter key
|
||||
to the local OmniRoute server. The two modules that touch the Radar service are
|
||||
`src/lib/radar/sync.ts` (catalog) and `src/lib/radar/referralsSync.ts` (referrals); both
|
||||
always run server-side, never client-side. This keeps the feed URL and any supporter key
|
||||
out of client-facing network traffic entirely.
|
||||
|
||||
All five routes return `404` when `RADAR_ENABLED` is off (see
|
||||
@@ -348,11 +369,13 @@ touches the network for referrals, mirroring `syncRadar()`'s contract exactly: f
|
||||
Ed25519 signature over the exact response bytes (`verifyFeedBytes`), validates against
|
||||
`RadarReferralsFeedSchema`, and caches into the `radar_referrals_cache` table
|
||||
(migration `142_radar_referrals_cache.sql`) — a table entirely separate from the
|
||||
catalog's `radar_feed_cache`. A 10 MB response cap and a `generatedAt` floor (an
|
||||
incoming feed with a `generatedAt` no newer than the cached one is treated as `stale`
|
||||
and never overwrites the cache — guards against a replay of an older signed artifact)
|
||||
mirror the catalog sync's own `MAX_FEED_BYTES`/version-floor guards. Never throws —
|
||||
always returns a status object; errors never carry a stack trace in `reason`.
|
||||
catalog's `radar_feed_cache`. A 10 MB response cap and a `generatedAt` floor reject an
|
||||
incoming feed older than the cached one, guarding against replay of an older signed
|
||||
artifact. An equal timestamp is accepted: the server intentionally gives the community
|
||||
and live referral variants the same deterministic `generatedAt`, so the signed payload
|
||||
and served tier can change after a supporter-key change without the underlying link set
|
||||
changing. Never throws — always returns a status object; errors never carry a stack trace
|
||||
in `reason`.
|
||||
|
||||
Two triggers keep the referrals cache warm, both independent of the catalog's own
|
||||
24h cadence:
|
||||
|
||||
@@ -603,7 +603,7 @@ export interface ProviderNodeRow {
|
||||
}
|
||||
|
||||
/** Hosts reachable only from the operator's machine/Docker network. */
|
||||
export function isLoopbackNodeHost(baseUrl: string): boolean {
|
||||
function isLoopbackNodeHost(baseUrl: string): boolean {
|
||||
try {
|
||||
const hostname = new URL(baseUrl).hostname;
|
||||
return (
|
||||
|
||||
@@ -7,8 +7,6 @@ import { isCloudflareChallenge } from "../../services/lmarenaTlsClient.ts";
|
||||
import { markLMArenaCatalogModelDead } from "./models.ts";
|
||||
import { parseArenaSSE } from "./stream.ts";
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
export function errorResponse(
|
||||
status: number,
|
||||
message: string,
|
||||
@@ -167,7 +165,7 @@ function baseChunk(model: string) {
|
||||
}
|
||||
|
||||
function enqueueSse(controller: ReadableStreamDefaultController, chunk: Record<string, unknown>) {
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
|
||||
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(chunk)}\n\n`));
|
||||
}
|
||||
|
||||
function emitStopAndDone(controller: ReadableStreamDefaultController, model: string) {
|
||||
@@ -175,8 +173,7 @@ function emitStopAndDone(controller: ReadableStreamDefaultController, model: str
|
||||
...baseChunk(model),
|
||||
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
||||
});
|
||||
|
||||
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
||||
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"));
|
||||
controller.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -724,7 +724,7 @@ export function openaiResponsesToOpenAIRequest(
|
||||
const reasoningRec = toRecord(root.reasoning);
|
||||
const effort = toString(reasoningRec.effort);
|
||||
if (effort && result.reasoning_effort === undefined) {
|
||||
result.reasoning_effort = normalizeResponsesReasoningEffort(effort, model ?? root.model);
|
||||
result.reasoning_effort = normalizeResponsesReasoningEffort(effort, model);
|
||||
}
|
||||
if (
|
||||
credentialRecord._copilotClient === true &&
|
||||
|
||||
@@ -79,8 +79,7 @@ export function sseCommentsEnabled(): boolean {
|
||||
if (typeof process === "undefined") return true;
|
||||
const v = process.env.OMNIROUTE_SSE_COMMENTS;
|
||||
if (v === undefined || v === "") return true;
|
||||
const normalized = v.trim().toLowerCase();
|
||||
return normalized !== "off" && normalized !== "false" && normalized !== "0" && normalized !== "no";
|
||||
return v.trim().toLowerCase() !== "off";
|
||||
}
|
||||
|
||||
export function createSseHeartbeatTransform({
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
} from "./streamHelpers.ts";
|
||||
import { calculateCost } from "@/lib/usage/costCalculator";
|
||||
import { buildOmniRouteSseMetadataComment } from "@/domain/omnirouteResponseMeta";
|
||||
import { sseCommentsEnabled } from "./sseHeartbeat.ts";
|
||||
import {
|
||||
createStructuredSSECollector,
|
||||
buildStreamSummaryFromEvents,
|
||||
@@ -1002,11 +1001,6 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
controller: TransformStreamDefaultController,
|
||||
finalUsage: UsageTokenRecord | Record<string, unknown> | null | undefined
|
||||
) => {
|
||||
// Skip SSE metadata comment lines when OMNIROUTE_SSE_COMMENTS is disabled
|
||||
// (e.g., "off", "false", "0", "no"). Strict OpenAI-compatible clients that
|
||||
// JSON.parse every SSE line will crash on `: x-omniroute-*` comment lines.
|
||||
if (!sseCommentsEnabled()) return;
|
||||
|
||||
const costUsd = finalUsage ? await calculateCost(provider, model, finalUsage) : 0;
|
||||
const comment = buildOmniRouteSseMetadataComment({
|
||||
provider,
|
||||
|
||||
@@ -47,8 +47,7 @@
|
||||
*/
|
||||
|
||||
import { cpSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, join, sep } from "node:path";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
/**
|
||||
* Entry packages of the SLM optional stack (the closure roots). `@huggingface/transformers` is
|
||||
@@ -97,33 +96,6 @@ export function computeDependencyClosure(nodeModulesDir, seeds = SEED_PACKAGES)
|
||||
return closure;
|
||||
}
|
||||
|
||||
/**
|
||||
* A package in the target tree counts as PRESENT only when its entrypoint
|
||||
* resolves from inside that tree — the same contract the Dockerfile's
|
||||
* post-build guard enforces. Next's file tracing can materialize a package
|
||||
* PARTIALLY (the package.json lands, the files its `main` points at do not),
|
||||
* and a directory-level `existsSync` check then skips the package forever
|
||||
* while the runtime dies with "Cannot find module <pkg>/dist/index.js".
|
||||
*
|
||||
* @param {string} targetNodeModulesDir
|
||||
* @param {string} name
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isPackageIntact(targetNodeModulesDir, name) {
|
||||
if (!existsSync(join(targetNodeModulesDir, name))) return false;
|
||||
try {
|
||||
const probe = createRequire(
|
||||
join(targetNodeModulesDir, "__colocate_probe__.js")
|
||||
);
|
||||
const resolved = probe.resolve(name);
|
||||
// A resolution that walked past the target into an ancestor tree does not
|
||||
// prove the target copy is usable.
|
||||
return resolved.startsWith(targetNodeModulesDir + sep);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Co-locate the SLM optional dependency closure from `<rootDir>/node_modules`
|
||||
* into a standalone bundle's `node_modules`.
|
||||
@@ -170,12 +142,11 @@ export function colocateLlmlinguaOptionals({
|
||||
|
||||
const closure = computeDependencyClosure(rootNm, seeds);
|
||||
|
||||
// Check the complete closure rather than only the entry package, and judge
|
||||
// presence by entrypoint integrity — a partially traced directory (see
|
||||
// isPackageIntact) must still receive its missing files.
|
||||
// Check the complete closure rather than only the entry package. A partially
|
||||
// populated bundle must still receive any missing transitive dependencies.
|
||||
if (
|
||||
closure.length > 0 &&
|
||||
closure.every((name) => isPackageIntact(targetNm, name))
|
||||
closure.every((name) => existsSync(join(targetNm, name)))
|
||||
) {
|
||||
return { skipped: true, reason: "already co-located" };
|
||||
}
|
||||
@@ -184,18 +155,11 @@ export function colocateLlmlinguaOptionals({
|
||||
|
||||
for (const name of closure) {
|
||||
const dest = join(targetNm, name);
|
||||
if (isPackageIntact(targetNm, name)) continue;
|
||||
if (existsSync(dest)) continue;
|
||||
|
||||
try {
|
||||
mkdirSync(dirname(dest), { recursive: true });
|
||||
// force:false merges into a partially traced directory: files the trace
|
||||
// already materialized are kept, missing ones (the package payload) are
|
||||
// filled in from the root tree.
|
||||
cpSync(join(rootNm, name), dest, {
|
||||
recursive: true,
|
||||
force: false,
|
||||
errorOnExist: false,
|
||||
});
|
||||
cpSync(join(rootNm, name), dest, { recursive: true });
|
||||
copied++;
|
||||
} catch (err) {
|
||||
log(
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useLocale, useTranslations } from "next-intl";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Card } from "@/shared/components";
|
||||
import {
|
||||
firstProviderConnectionId,
|
||||
providerConnectionsRequestUrl,
|
||||
type RadarSetupConnection,
|
||||
} from "@/lib/radar/setupConnections";
|
||||
import type { RadarLocalizedText } from "@/lib/radar/feedSchema";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -14,17 +20,16 @@ import { Card } from "@/shared/components";
|
||||
* Localized text: either a plain string or an {en, pt?} object.
|
||||
* The renderer resolves the best locale with EN fallback (D25 compat).
|
||||
*/
|
||||
type LocalizedText = string | { en: string; pt?: string };
|
||||
|
||||
interface SetupInfo {
|
||||
keyUrl: string | null;
|
||||
steps: LocalizedText[];
|
||||
steps: RadarLocalizedText[];
|
||||
}
|
||||
|
||||
interface ProviderSetupData {
|
||||
provider: string;
|
||||
setup: SetupInfo | null;
|
||||
configured: boolean;
|
||||
connectionId: string | null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -32,9 +37,9 @@ interface ProviderSetupData {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Resolve a LocalizedText to a display string. */
|
||||
function resolveText(text: LocalizedText, locale: string): string {
|
||||
function resolveText(text: RadarLocalizedText, locale: string): string {
|
||||
if (typeof text === "string") return text;
|
||||
if (locale === "pt" && text.pt) return text.pt;
|
||||
if (locale.toLowerCase().startsWith("pt") && text.pt) return text.pt;
|
||||
return text.en;
|
||||
}
|
||||
|
||||
@@ -44,9 +49,9 @@ function resolveText(text: LocalizedText, locale: string): string {
|
||||
|
||||
export default function RadarSetupPage() {
|
||||
const t = useTranslations("radarSetupPage");
|
||||
const locale = useLocale();
|
||||
const searchParams = useSearchParams();
|
||||
const provider = searchParams.get("provider");
|
||||
const locale = "en"; // Could be derived from next-intl locale later
|
||||
|
||||
const [setupData, setSetupData] = useState<ProviderSetupData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -63,18 +68,25 @@ export default function RadarSetupPage() {
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const res = await fetch("/api/radar/catalog");
|
||||
const [res, connectionsRes] = await Promise.all([
|
||||
fetch("/api/radar/catalog"),
|
||||
fetch(providerConnectionsRequestUrl(provider)),
|
||||
]);
|
||||
if (res.status === 404) {
|
||||
setError(t("flagDisabled"));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
if (!connectionsRes.ok) throw new Error(`HTTP ${connectionsRes.status}`);
|
||||
const data = await res.json();
|
||||
const connectionsData = (await connectionsRes.json()) as {
|
||||
connections?: RadarSetupConnection[];
|
||||
};
|
||||
|
||||
// Find ALL entries for this provider and extract setup from the first one that has it
|
||||
const providerEntries = data.entries.filter(
|
||||
(e: { provider: string }) => e.provider === provider,
|
||||
(e: { provider: string }) => e.provider === provider
|
||||
);
|
||||
|
||||
if (providerEntries.length === 0) {
|
||||
@@ -85,17 +97,19 @@ export default function RadarSetupPage() {
|
||||
|
||||
// Find setup info from feed entries (they carry the setup field)
|
||||
const entryWithSetup = providerEntries.find(
|
||||
(e: { setup?: SetupInfo | null }) => e.setup && (e.setup.steps.length > 0 || e.setup.keyUrl),
|
||||
(e: { setup?: SetupInfo | null }) =>
|
||||
e.setup && (e.setup.steps.length > 0 || e.setup.keyUrl)
|
||||
);
|
||||
|
||||
// Check if provider is configured (has connections)
|
||||
// We infer this from whether the provider exists in the catalog at all
|
||||
// The actual connection check would need a separate API — for now we show
|
||||
// the guide regardless
|
||||
const connectionId = firstProviderConnectionId(
|
||||
Array.isArray(connectionsData.connections) ? connectionsData.connections : [],
|
||||
provider
|
||||
);
|
||||
setSetupData({
|
||||
provider,
|
||||
setup: entryWithSetup?.setup ?? null,
|
||||
configured: false, // Will be enriched when connection-status API is available
|
||||
configured: connectionId !== null,
|
||||
connectionId,
|
||||
});
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t("loadFailed"));
|
||||
@@ -109,15 +123,11 @@ export default function RadarSetupPage() {
|
||||
|
||||
// Test connection — uses the EXISTING connection-test endpoint
|
||||
const handleTestConnection = useCallback(async () => {
|
||||
if (!provider) return;
|
||||
if (!setupData?.connectionId) return;
|
||||
setTesting(true);
|
||||
setTestResult(null);
|
||||
try {
|
||||
// The existing test endpoint is POST /api/providers/[id]/test
|
||||
// We need the connection ID — for now we use the provider ID as a proxy.
|
||||
// In a full implementation, the setup page would list connections for
|
||||
// this provider and test each one. Here we test the first connection.
|
||||
const res = await fetch(`/api/providers/${encodeURIComponent(provider)}/test`, {
|
||||
const res = await fetch(`/api/providers/${encodeURIComponent(setupData.connectionId)}/test`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
@@ -136,7 +146,7 @@ export default function RadarSetupPage() {
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
}, [provider, t]);
|
||||
}, [setupData?.connectionId, t]);
|
||||
|
||||
if (!provider) {
|
||||
return (
|
||||
@@ -165,9 +175,7 @@ export default function RadarSetupPage() {
|
||||
<p className="text-sm text-text-muted mt-1">{t("setupSubtitle")}</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="p-3 rounded-lg bg-red-500/10 text-red-400 text-sm">{error}</div>
|
||||
)}
|
||||
{error && <div className="p-3 rounded-lg bg-red-500/10 text-red-400 text-sm">{error}</div>}
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center min-h-[200px]">
|
||||
@@ -248,15 +256,13 @@ export default function RadarSetupPage() {
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={handleTestConnection}
|
||||
disabled={testing}
|
||||
disabled={testing || !setupData.connectionId}
|
||||
className="px-4 py-2 text-sm font-medium rounded-lg border border-violet-500 text-violet-400 hover:bg-violet-500/10 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{testing ? t("testing") : t("testButton")}
|
||||
</button>
|
||||
{testResult && (
|
||||
<span
|
||||
className={`text-sm ${testResult.ok ? "text-green-400" : "text-red-400"}`}
|
||||
>
|
||||
<span className={`text-sm ${testResult.ok ? "text-green-400" : "text-red-400"}`}>
|
||||
{testResult.message}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
// Import through the module index, NOT "@/lib/memory/manager" directly: the index's
|
||||
// import-time side effect is what calls memoryManager.register(sqliteBackend). Importing
|
||||
// the bare manager gives an EMPTY registry, so every handler here threw
|
||||
// `Primary backend "sqlite" not registered` and returned 500 (#8752).
|
||||
import { memoryManager } from "@/lib/memory";
|
||||
import { memoryManager } from "@/lib/memory/manager";
|
||||
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
|
||||
import { MemoryUpdatePutSchema } from "@/shared/schemas/memory";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors";
|
||||
import { buildErrorBody } from "@omniroute/open-sse/utils/error";
|
||||
import { installMarketplacePlugin } from "@/lib/plugins/marketplace";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
|
||||
const InstallBodySchema = z.object({
|
||||
name: z.string().trim().min(1),
|
||||
});
|
||||
|
||||
export async function OPTIONS() {
|
||||
return handleCorsOptions();
|
||||
}
|
||||
@@ -20,14 +15,15 @@ export async function POST(request: NextRequest) {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
try {
|
||||
const parsed = InstallBodySchema.safeParse(await request.json());
|
||||
if (!parsed.success) {
|
||||
const body = await request.json();
|
||||
const { name } = body as { name?: string };
|
||||
if (!name || typeof name !== "string") {
|
||||
return NextResponse.json(buildErrorBody(400, "Missing or invalid 'name' field"), {
|
||||
status: 400,
|
||||
headers: CORS_HEADERS,
|
||||
});
|
||||
}
|
||||
const result = await installMarketplacePlugin(parsed.data.name);
|
||||
const result = await installMarketplacePlugin(name);
|
||||
return NextResponse.json(result, { status: 201, headers: CORS_HEADERS });
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : "Failed to install marketplace plugin";
|
||||
|
||||
@@ -11,15 +11,9 @@
|
||||
* and never reaches the browser.
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { forwardToDarioAdmin, requireAdminAuth } from "../_lib";
|
||||
import { createErrorResponse } from "@/lib/api/errorResponse";
|
||||
|
||||
const DeleteAccountBodySchema = z.object({
|
||||
alias: z.string().trim().min(1).optional(),
|
||||
});
|
||||
|
||||
export async function GET(request: Request): Promise<Response> {
|
||||
const authResponse = await requireAdminAuth(request);
|
||||
if (authResponse) return authResponse;
|
||||
@@ -35,9 +29,9 @@ export async function DELETE(request: Request): Promise<Response> {
|
||||
|
||||
if (!alias && request.body !== null) {
|
||||
try {
|
||||
const parsed = DeleteAccountBodySchema.safeParse(await request.json());
|
||||
if (parsed.success && parsed.data.alias) {
|
||||
alias = parsed.data.alias;
|
||||
const parsed = await request.json();
|
||||
if (parsed && typeof parsed === "object" && typeof (parsed as { alias?: unknown }).alias === "string") {
|
||||
alias = (parsed as { alias: string }).alias.trim();
|
||||
}
|
||||
} catch {
|
||||
/* fall through to the missing-alias error below */
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
* pickup, rather than relying on any undocumented hot-reload behavior.
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import { NextResponse } from "next/server";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
@@ -40,11 +39,6 @@ import { getDarioHomeDir } from "@/lib/services/installers/dario";
|
||||
import { createErrorResponse } from "@/lib/api/errorResponse";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
|
||||
const ImportBodySchema = z.object({
|
||||
connectionId: z.string().trim().min(1).optional(),
|
||||
alias: z.string().trim().min(1).optional(),
|
||||
});
|
||||
|
||||
const ALIAS_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_\-.]{0,63}$/;
|
||||
|
||||
function safeAliasFromSource(email: string | null | undefined, connectionId: string): string {
|
||||
@@ -88,16 +82,15 @@ export async function POST(request: Request): Promise<Response> {
|
||||
const authResponse = await requireAdminAuth(request);
|
||||
if (authResponse) return authResponse;
|
||||
|
||||
let raw: unknown;
|
||||
let body: unknown;
|
||||
try {
|
||||
raw = await request.json();
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return createErrorResponse({ status: 400, message: "Invalid JSON body" });
|
||||
}
|
||||
|
||||
const parsed = ImportBodySchema.safeParse(raw ?? {});
|
||||
const b = parsed.success ? parsed.data : {};
|
||||
const connectionId = b.connectionId ?? null;
|
||||
const b = (body || {}) as Record<string, unknown>;
|
||||
const connectionId = typeof b.connectionId === "string" ? b.connectionId : null;
|
||||
if (!connectionId) {
|
||||
return createErrorResponse({ status: 400, message: "connectionId is required" });
|
||||
}
|
||||
@@ -119,7 +112,10 @@ export async function POST(request: Request): Promise<Response> {
|
||||
});
|
||||
}
|
||||
|
||||
let alias = b.alias || safeAliasFromSource(conn.email as string | null, connectionId);
|
||||
let alias =
|
||||
typeof b.alias === "string" && b.alias.trim()
|
||||
? b.alias.trim()
|
||||
: safeAliasFromSource(conn.email as string | null, connectionId);
|
||||
if (!ALIAS_PATTERN.test(alias)) {
|
||||
alias = safeAliasFromSource(conn.email as string | null, connectionId);
|
||||
}
|
||||
|
||||
@@ -8,29 +8,23 @@
|
||||
* posts the displayed code to /login-complete.
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import { forwardToDarioAdmin, requireAdminAuth } from "../_lib";
|
||||
import { createErrorResponse } from "@/lib/api/errorResponse";
|
||||
|
||||
const LoginStartBodySchema = z.object({
|
||||
alias: z.string().trim().min(1).optional(),
|
||||
});
|
||||
type LoginStartBody = z.infer<typeof LoginStartBodySchema>;
|
||||
|
||||
export async function POST(request: Request): Promise<Response> {
|
||||
const authResponse = await requireAdminAuth(request);
|
||||
if (authResponse) return authResponse;
|
||||
|
||||
let body: LoginStartBody = {};
|
||||
let body: { alias?: string } = {};
|
||||
try {
|
||||
if (request.body !== null) {
|
||||
const parsed = LoginStartBodySchema.safeParse(await request.json());
|
||||
if (parsed.success) body = parsed.data;
|
||||
const parsed = await request.json();
|
||||
if (parsed && typeof parsed === "object") body = parsed as { alias?: string };
|
||||
}
|
||||
} catch {
|
||||
return createErrorResponse({ status: 400, message: "Invalid JSON body" });
|
||||
}
|
||||
|
||||
const forwardBody = body.alias ? { alias: body.alias } : {};
|
||||
const forwardBody = typeof body.alias === "string" && body.alias.trim() ? { alias: body.alias.trim() } : {};
|
||||
return forwardToDarioAdmin({ method: "POST", path: "/admin/login/start", body: forwardBody });
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import { getCachedProviderNodes } from "@/lib/db/readCache";
|
||||
import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags";
|
||||
import {
|
||||
buildDynamicAudioProvider,
|
||||
isLoopbackNodeHost,
|
||||
type AudioProvider,
|
||||
type ProviderNodeRow,
|
||||
} from "@omniroute/open-sse/config/audioRegistry.ts";
|
||||
@@ -36,7 +35,19 @@ export const AUDIO_REMOTE_NODES_FLAG = "AUDIO_REMOTE_PROVIDER_NODES";
|
||||
* Loopback / private-range hosts that never leave the operator's machine or
|
||||
* Docker network. `::1` stays excluded, matching the previous SSRF hardening.
|
||||
*/
|
||||
export { isLoopbackNodeHost as isLocalAudioNodeHost };
|
||||
export function isLocalAudioNodeHost(baseUrl: string): boolean {
|
||||
try {
|
||||
const hostname = new URL(baseUrl).hostname;
|
||||
return (
|
||||
hostname === "localhost" ||
|
||||
hostname === "127.0.0.1" ||
|
||||
// Strictly 172.16.0.0/12 (Docker/local)
|
||||
/^172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}$/.test(hostname)
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure selection step — no DB, no flag lookup, so the policy is directly testable.
|
||||
@@ -61,7 +72,7 @@ export function selectAudioProviderNodes(
|
||||
return false;
|
||||
}
|
||||
if (!node.baseUrl) return false;
|
||||
return isLoopbackNodeHost(node.baseUrl) || allowRemote;
|
||||
return isLocalAudioNodeHost(node.baseUrl) || allowRemote;
|
||||
});
|
||||
|
||||
const providers: AudioProvider[] = [];
|
||||
|
||||
@@ -1376,7 +1376,7 @@ async function buildUnifiedModelsResponseCore(
|
||||
continue;
|
||||
}
|
||||
|
||||
// #8958/#9034: honor the compatible-provider node prefix (as the synced/custom
|
||||
// #8958: honor the compatible-provider node prefix (as the synced/custom
|
||||
// loops do) so an alias-backed entry publishes `prefix/model` instead of the
|
||||
// raw provider-node UUID. Without the providerIdToPrefix lookup, `alias` fell
|
||||
// through to `providerKey` (the UUID) and the dedupe below — which only checks
|
||||
|
||||
@@ -13,9 +13,6 @@
|
||||
// happen once.
|
||||
|
||||
export type UsableChatModelCandidate = {
|
||||
id?: string;
|
||||
root?: string;
|
||||
name?: string;
|
||||
owned_by?: string;
|
||||
parent?: string | null;
|
||||
type?: string;
|
||||
@@ -47,19 +44,9 @@ function excludesTextOutputModality(model: UsableChatModelCandidate) {
|
||||
);
|
||||
}
|
||||
|
||||
function isBuiltinAutoModel(model: UsableChatModelCandidate): boolean {
|
||||
const id = model.id || model.root || model.name || "";
|
||||
const normalized = id.trim().toLowerCase();
|
||||
return normalized === "auto" || normalized.startsWith("auto/");
|
||||
}
|
||||
|
||||
export function isUsableChatModel(model: UsableChatModelCandidate) {
|
||||
if (typeof model.owned_by === "string" && model.owned_by.trim().toLowerCase() === "combo") {
|
||||
// Allow built-in auto-routing models (e.g. auto, auto/best-coding)
|
||||
// while still excluding operator-created combos.
|
||||
if (!isBuiltinAutoModel(model)) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (typeof model.parent === "string" && model.parent.length > 0) return false;
|
||||
if (typeof model.type === "string" && model.type !== "chat") return false;
|
||||
|
||||
@@ -103,9 +103,7 @@ export function setRadarCache(entry: {
|
||||
export function getRadarSettings(): RadarSettings {
|
||||
const db = getDbInstance();
|
||||
const row = db
|
||||
.prepare(
|
||||
"SELECT opt_in, supporter_key_encrypted, updated_at FROM radar_settings WHERE id = 1"
|
||||
)
|
||||
.prepare("SELECT opt_in, supporter_key_encrypted, updated_at FROM radar_settings WHERE id = 1")
|
||||
.get() as { opt_in: number; supporter_key_encrypted: string | null; updated_at: string };
|
||||
|
||||
return {
|
||||
@@ -120,9 +118,9 @@ export function getRadarSettings(): RadarSettings {
|
||||
*/
|
||||
export function setRadarOptIn(optIn: boolean): void {
|
||||
const db = getDbInstance();
|
||||
db.prepare(
|
||||
"UPDATE radar_settings SET opt_in = ?, updated_at = datetime('now') WHERE id = 1"
|
||||
).run(optIn ? 1 : 0);
|
||||
db.prepare("UPDATE radar_settings SET opt_in = ?, updated_at = datetime('now') WHERE id = 1").run(
|
||||
optIn ? 1 : 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,9 +131,20 @@ export function setRadarOptIn(optIn: boolean): void {
|
||||
export function setRadarKey(key: string | null): void {
|
||||
const db = getDbInstance();
|
||||
const encrypted = key !== null ? encrypt(key) : null;
|
||||
db.prepare(
|
||||
const updateKey = db.prepare(
|
||||
"UPDATE radar_settings SET supporter_key_encrypted = ?, updated_at = datetime('now') WHERE id = 1"
|
||||
).run(encrypted);
|
||||
);
|
||||
const clearCatalogCache = db.prepare("DELETE FROM radar_feed_cache WHERE id = 1");
|
||||
const clearReferralsCache = db.prepare("DELETE FROM radar_referrals_cache WHERE id = 1");
|
||||
|
||||
db.transaction(() => {
|
||||
updateKey.run(encrypted);
|
||||
// Both signed feeds are entitlement-sensitive. Clearing their cached
|
||||
// variants forces the next sync/read to resolve the new key server-side
|
||||
// instead of serving data fetched under the previous entitlement.
|
||||
clearCatalogCache.run();
|
||||
clearReferralsCache.run();
|
||||
})();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -5,12 +5,15 @@
|
||||
* The baseline (`FREE_MODEL_BUDGETS`) is NEVER mutated.
|
||||
*
|
||||
* Merge rules:
|
||||
* 1. Feed never overwrites a local override.
|
||||
* 1. Feed never overwrites a local override, except the safety-critical
|
||||
* `enabled:false` signal for an upstream model confirmed unavailable.
|
||||
* 2. `enabled:false` disables the entry with `disabledBy: "radar"` provenance.
|
||||
* 3. User-added entry NOT in the feed survives untouched.
|
||||
* 4. User deletion tombstone prevents feed from resurrecting the entry.
|
||||
*/
|
||||
|
||||
import type { RadarLocalizedText } from "./feedSchema";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -73,7 +76,7 @@ export interface MergedEntry {
|
||||
/** Setup guide (key URL + steps) reported by the feed. Undefined for baseline-only entries. */
|
||||
setup?: {
|
||||
keyUrl: string | null;
|
||||
steps: string[];
|
||||
steps: RadarLocalizedText[];
|
||||
} | null;
|
||||
}
|
||||
|
||||
@@ -107,7 +110,7 @@ export interface FeedModel {
|
||||
tosRisk: MergedEntry["tos"];
|
||||
setup: {
|
||||
keyUrl: string | null;
|
||||
steps: string[];
|
||||
steps: RadarLocalizedText[];
|
||||
} | null;
|
||||
enabled: boolean;
|
||||
}
|
||||
@@ -127,9 +130,7 @@ function entryKey(provider: string, modelId: string): string {
|
||||
* Convert a FeedModel budget into a `monthlyTokens` number compatible
|
||||
* with the baseline catalog shape.
|
||||
*/
|
||||
function feedBudgetToMonthlyTokens(
|
||||
budget: FeedModel["budget"],
|
||||
): number {
|
||||
function feedBudgetToMonthlyTokens(budget: FeedModel["budget"]): number {
|
||||
if (budget.kind === "per_model") return budget.tokensPerMonth;
|
||||
if (budget.kind === "shared_pool") return budget.tokensPerMonth;
|
||||
return 0; // rate_only
|
||||
@@ -138,9 +139,7 @@ function feedBudgetToMonthlyTokens(
|
||||
/**
|
||||
* Convert a FeedModel budget into a `poolKey` compatible with the baseline.
|
||||
*/
|
||||
function feedBudgetToPoolKey(
|
||||
budget: FeedModel["budget"],
|
||||
): string | null {
|
||||
function feedBudgetToPoolKey(budget: FeedModel["budget"]): string | null {
|
||||
if (budget.kind === "shared_pool") return budget.poolId;
|
||||
return null;
|
||||
}
|
||||
@@ -242,7 +241,7 @@ export function applyFeed(input: ApplyFeedInput): MergedEntry[] {
|
||||
function mergeOne(
|
||||
base: MergedEntry,
|
||||
feed: FeedModel,
|
||||
overrides: Partial<MergedEntry> | undefined,
|
||||
overrides: Partial<MergedEntry> | undefined
|
||||
): MergedEntry {
|
||||
// Start from baseline
|
||||
const result: MergedEntry = { ...base };
|
||||
@@ -307,6 +306,13 @@ function mergeOne(
|
||||
if (overrides.setup !== undefined) result.setup = overrides.setup;
|
||||
}
|
||||
|
||||
// Safety exception to rule 1: a model confirmed unavailable upstream is
|
||||
// never resurrected by a stale local enabled:true override.
|
||||
if (!feed.enabled) {
|
||||
result.enabled = false;
|
||||
result.disabledBy = "radar";
|
||||
}
|
||||
|
||||
// Origin: "local" if user has overrides, else "radar" (feed updated it)
|
||||
result.origin = overriddenKeys.size > 0 ? "local" : "radar";
|
||||
|
||||
@@ -318,7 +324,7 @@ function mergeOne(
|
||||
*/
|
||||
function feedModelToMerged(
|
||||
feed: FeedModel,
|
||||
overrides: Partial<MergedEntry> | undefined,
|
||||
overrides: Partial<MergedEntry> | undefined
|
||||
): MergedEntry {
|
||||
const entry: MergedEntry = {
|
||||
provider: feed.provider,
|
||||
@@ -329,8 +335,8 @@ function feedModelToMerged(
|
||||
freeType: overrides?.freeType ?? feed.freeType,
|
||||
poolKey: overrides?.poolKey ?? feedBudgetToPoolKey(feed.budget),
|
||||
tos: overrides?.tos ?? feed.tosRisk,
|
||||
trainsOnPrompts: overrides?.trainsOnPrompts ?? (feed.trainsOnPrompts ?? undefined),
|
||||
enabled: overrides?.enabled ?? feed.enabled,
|
||||
trainsOnPrompts: overrides?.trainsOnPrompts ?? feed.trainsOnPrompts ?? undefined,
|
||||
enabled: feed.enabled ? (overrides?.enabled ?? true) : false,
|
||||
origin: overrides ? "local" : "radar",
|
||||
contextWindow: overrides?.contextWindow ?? feed.contextWindow,
|
||||
capabilities: overrides?.capabilities ?? feed.capabilities,
|
||||
@@ -338,10 +344,8 @@ function feedModelToMerged(
|
||||
setup: overrides?.setup ?? feed.setup,
|
||||
};
|
||||
|
||||
// Rule 2 (feed disable) — but rule 1 (local override wins) takes precedence,
|
||||
// matching mergeOne(): only force-disable when the user has NOT explicitly
|
||||
// overridden `enabled` locally.
|
||||
if (!feed.enabled && overrides?.enabled === undefined) {
|
||||
// Rule 2 is the safety exception to local override precedence.
|
||||
if (!feed.enabled) {
|
||||
entry.enabled = false;
|
||||
entry.disabledBy = "radar";
|
||||
}
|
||||
|
||||
@@ -43,6 +43,16 @@ export type RadarTier = z.infer<typeof RadarTierSchema>;
|
||||
|
||||
const IntNullable = z.number().int().nullable();
|
||||
|
||||
/** D25: English is canonical; Portuguese is an optional localized companion. */
|
||||
export const RadarLocalizedTextSchema = z.union([
|
||||
z.string(), // compatibility with schema-v1 feeds published before D25
|
||||
z.object({
|
||||
en: z.string().min(1),
|
||||
pt: z.string().min(1).optional(),
|
||||
}),
|
||||
]);
|
||||
export type RadarLocalizedText = z.infer<typeof RadarLocalizedTextSchema>;
|
||||
|
||||
/**
|
||||
* Budget is a discriminated union on `kind`:
|
||||
* - per_model: tokensPerMonth (positive int)
|
||||
@@ -80,7 +90,7 @@ const CapabilitiesSchema = z.object({
|
||||
const SetupSchema = z
|
||||
.object({
|
||||
keyUrl: z.string().url().nullable(),
|
||||
steps: z.array(z.string()),
|
||||
steps: z.array(RadarLocalizedTextSchema),
|
||||
})
|
||||
.nullable();
|
||||
|
||||
@@ -167,8 +177,8 @@ const QuirkTargetSchema = z.object({
|
||||
|
||||
const QuirkSchema = z.object({
|
||||
slug: z.string(),
|
||||
title: z.string(),
|
||||
body: z.string(),
|
||||
title: RadarLocalizedTextSchema,
|
||||
body: RadarLocalizedTextSchema,
|
||||
severity: SeverityEnum,
|
||||
targets: z.array(QuirkTargetSchema),
|
||||
});
|
||||
@@ -182,10 +192,9 @@ export const RadarFeedSchema = z.object({
|
||||
schemaVersion: z.literal(1),
|
||||
version: z.string(),
|
||||
generatedAt: z.string().datetime(),
|
||||
// NOTE: this body field is ALWAYS "live", by design — the community tier
|
||||
// is the exact same signed bytes served from an older snapshot, and there
|
||||
// is only one signed artifact per version (rewriting this field
|
||||
// server-side per request would break the exact-bytes Ed25519 signature).
|
||||
// NOTE: this body field is not the entitlement decision. The server can
|
||||
// publish separate exact-byte live/community artifacts for one version,
|
||||
// while the selected request tier is still communicated by the header.
|
||||
// The tier ACTUALLY served is decided by the server per-request based on
|
||||
// the Authorization key, and is surfaced via the `x-omniroute-feed-tier`
|
||||
// response header instead. NEVER read this field for UI/display — use the
|
||||
|
||||
@@ -18,10 +18,7 @@
|
||||
* Deps are injectable for testing.
|
||||
*/
|
||||
|
||||
import {
|
||||
RadarReferralsFeedSchema,
|
||||
type RadarReferralsFeed,
|
||||
} from "./referralsFeedSchema";
|
||||
import { RadarReferralsFeedSchema, type RadarReferralsFeed } from "./referralsFeedSchema";
|
||||
import { RadarTierSchema, type RadarTier } from "./feedSchema";
|
||||
import { verifyFeedBytes } from "./verify";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
@@ -144,9 +141,10 @@ export function shouldSyncReferralsOnRead(
|
||||
* 4. Verify Ed25519 signature over exact bytes (same pinned key as the
|
||||
* catalog feed — one key pins both artifacts).
|
||||
* 5. Parse+validate with RadarReferralsFeedSchema.
|
||||
* 6. Replay guard: incoming `generatedAt` must be strictly newer than the
|
||||
* cache (a same/older `generatedAt` is a no-op — nothing changed, or a
|
||||
* stale replay — either way the cache is left untouched).
|
||||
* 6. Replay guard: reject an incoming `generatedAt` older than the cache.
|
||||
* Equal timestamps remain valid because the community and live referral
|
||||
* variants deliberately share one deterministic `generatedAt`; the
|
||||
* served tier can still change after the supporter key changes.
|
||||
* 7. Cache the result.
|
||||
*
|
||||
* @param deps - Injectable dependencies for testing.
|
||||
@@ -262,7 +260,8 @@ export async function syncRadarReferrals(
|
||||
return { status: "invalid_schema" };
|
||||
}
|
||||
|
||||
// Step 7: generatedAt floor — replay/no-op guard.
|
||||
// Step 7: generatedAt floor — reject older signed replays. Equal
|
||||
// timestamps are accepted because entitlement variants share generatedAt.
|
||||
let existingCache: RadarReferralsCacheEntry | null = null;
|
||||
if (getCacheFn) {
|
||||
existingCache = getCacheFn();
|
||||
@@ -274,7 +273,7 @@ export async function syncRadarReferrals(
|
||||
if (existingCache) {
|
||||
const existingMs = Date.parse(existingCache.generatedAt);
|
||||
const incomingMs = Date.parse(feed.generatedAt);
|
||||
if (Number.isFinite(existingMs) && Number.isFinite(incomingMs) && incomingMs <= existingMs) {
|
||||
if (Number.isFinite(existingMs) && Number.isFinite(incomingMs) && incomingMs < existingMs) {
|
||||
return { status: "stale" };
|
||||
}
|
||||
}
|
||||
@@ -282,7 +281,8 @@ export async function syncRadarReferrals(
|
||||
// Step 8: Resolve served tier — the body carries no `tier` field at all
|
||||
// for this feed, so the header is the only source; absent/garbage header
|
||||
// degrades to the least-privileged "community" default.
|
||||
const servedTier = parseServedTierHeader(res.headers.get("x-omniroute-feed-tier")) ?? "community";
|
||||
const servedTier =
|
||||
parseServedTierHeader(res.headers.get("x-omniroute-feed-tier")) ?? "community";
|
||||
|
||||
// Step 9: Cache the result
|
||||
const cacheEntry: RadarReferralsCacheEntry = {
|
||||
|
||||
26
src/lib/radar/setupConnections.ts
Normal file
26
src/lib/radar/setupConnections.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export interface RadarSetupConnection {
|
||||
id: string;
|
||||
provider: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
/** Reuse the authenticated provider-connections API with a bounded provider filter. */
|
||||
export function providerConnectionsRequestUrl(provider: string): string {
|
||||
return `/api/providers?provider=${encodeURIComponent(provider)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a concrete connection id for the setup test endpoint. Prefer an active
|
||||
* connection, then fall back to the first valid connection for the provider.
|
||||
*/
|
||||
export function firstProviderConnectionId(
|
||||
connections: readonly RadarSetupConnection[],
|
||||
provider: string
|
||||
): string | null {
|
||||
const matching = connections.filter(
|
||||
(connection) => connection.provider === provider && connection.id.length > 0
|
||||
);
|
||||
return (
|
||||
matching.find((connection) => connection.isActive !== false)?.id ?? matching[0]?.id ?? null
|
||||
);
|
||||
}
|
||||
@@ -266,7 +266,14 @@ export async function syncRadar(deps: SyncDeps = {}): Promise<SyncStatus> {
|
||||
return { status: "invalid_schema" };
|
||||
}
|
||||
|
||||
// Step 7: Version floor
|
||||
// Step 7: Resolve the served tier before the version floor. A single-use
|
||||
// supporter key deliberately transitions from live to community after its
|
||||
// first catalog pull, and the community snapshot can be older.
|
||||
const servedTier = parseServedTierHeader(res.headers.get("x-omniroute-feed-tier")) ?? feed.tier;
|
||||
|
||||
// Step 8: Version floor. Same/older versions are rejected within a tier,
|
||||
// but a verified live -> community transition must replace the privileged
|
||||
// cache even when the community snapshot is older.
|
||||
let existingCache: RadarCacheEntry | null = null;
|
||||
if (getCacheFn) {
|
||||
existingCache = getCacheFn();
|
||||
@@ -275,17 +282,15 @@ export async function syncRadar(deps: SyncDeps = {}): Promise<SyncStatus> {
|
||||
existingCache = mod.getRadarCache();
|
||||
}
|
||||
|
||||
if (existingCache && compareVersions(feed.version, existingCache.version) <= 0) {
|
||||
const isEntitlementDowngrade = existingCache?.tier === "live" && servedTier === "community";
|
||||
if (
|
||||
existingCache &&
|
||||
!isEntitlementDowngrade &&
|
||||
compareVersions(feed.version, existingCache.version) <= 0
|
||||
) {
|
||||
return { status: "stale" };
|
||||
}
|
||||
|
||||
// Step 8: Resolve the served tier.
|
||||
// The `x-omniroute-feed-tier` header reflects the tier ACTUALLY served
|
||||
// (see `parseServedTierHeader`); fall back to the signed body's `tier`
|
||||
// field only when the header is absent or unrecognized — never trust an
|
||||
// arbitrary header value into the cache/UI.
|
||||
const servedTier = parseServedTierHeader(res.headers.get("x-omniroute-feed-tier")) ?? feed.tier;
|
||||
|
||||
// Step 9: Cache the result
|
||||
const cacheEntry: RadarCacheEntry = {
|
||||
version: feed.version,
|
||||
|
||||
@@ -253,39 +253,6 @@ export const managementPolicy: RoutePolicy = {
|
||||
return allow({ kind: "management_key", id: "cli", label: "local-cli-token" });
|
||||
}
|
||||
|
||||
// MCP path carve-out (#9159): accept mcp:connect, manage, or admin
|
||||
// scope for /api/mcp/* from any origin (loopback, private LAN, or remote).
|
||||
// Loopback/LAN requests skip the Tier 1 bypass gate above, so with
|
||||
// requireLogin=true they would fall through to the generic API-key check
|
||||
// which only accepts manage/admin -- rejecting mcp:connect-only keys.
|
||||
// This carve-out mirrors the existing Tier 1 MCP check but without the
|
||||
// locality guard, so it catches the loopback/LAN requests that the Tier 1
|
||||
// gate does not reach.
|
||||
if (path.startsWith("/api/mcp/")) {
|
||||
const apiKey = extractApiKey(ctx.request as unknown as Request, { allowUrl: false });
|
||||
if (apiKey) {
|
||||
try {
|
||||
if (await isValidApiKey(apiKey)) {
|
||||
const meta = await getApiKeyMetadata(apiKey);
|
||||
if (meta && hasMcpConnectOrManageScope(meta.scopes)) {
|
||||
const grantedBy = meta.scopes.includes("admin")
|
||||
? "admin"
|
||||
: meta.scopes.includes("manage")
|
||||
? "manage"
|
||||
: "mcp-connect";
|
||||
return allow({
|
||||
kind: "management_key",
|
||||
id: meta.id,
|
||||
label: `api-key-${grantedBy}-scope-mcp-carve-out`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return reject(503, "AUTH_BACKEND_UNAVAILABLE", "Service temporarily unavailable");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tier 2: always-protected routes skip the requireLogin=false bypass.
|
||||
if (!isAlwaysProtectedPath(path) && !(await isAuthRequired(ctx.request))) {
|
||||
return allow({ kind: "anonymous", id: "anonymous", label: "auth-disabled" });
|
||||
|
||||
@@ -67,14 +67,6 @@ const QUOTA_PATTERNS: ReadonlyArray<RegExp> = [
|
||||
// ~60s against a budget that only resets at UTC midnight.
|
||||
/daily free allocation/i,
|
||||
|
||||
// OmniRoute auth-layer synthetic 429 (Issue #9269).
|
||||
// Body: "All antigravity accounts have exhausted their quota (reset after 5m)"
|
||||
// Produced by auth.ts line 1477 when every account for a provider has
|
||||
// exhausted its quota. Without this pattern, the message is classified as
|
||||
// a transient rate-limit and the combo loop burns retries against the
|
||||
// same provider instead of falling back to a healthy one.
|
||||
/have exhausted their quota/i,
|
||||
|
||||
// Modal-hosted OpenAI-compatible endpoints (e.g. self-hosted Kimi K3).
|
||||
// Body: {"error":"usage limit reached"}, no nested "message"/"quota"/
|
||||
// "daily" wording. Without this pattern the 429 falls through to
|
||||
|
||||
@@ -714,20 +714,6 @@ async function handleChatImplementation(
|
||||
) => {
|
||||
if (isComboLiveTest) return true;
|
||||
|
||||
// #9057: for keys with model restrictions (allowedModels or disableNonPublicModels),
|
||||
// run isModelAllowedForKey even for auto/* models. The API-key policy gate
|
||||
// (validateModelAccess in apiKeyPolicy.ts) treats auto/* as a virtual combo and
|
||||
// skips isModelAllowedForKey, so the per-candidate check here is the only
|
||||
// enforcement point during combo routing. Without it, a key with
|
||||
// disableNonPublicModels=true can reach free/prohibited models through auto/*.
|
||||
const hasModelRestrictions =
|
||||
apiKeyInfo &&
|
||||
(Boolean(apiKeyInfo.allowedModels?.length) || apiKeyInfo.disableNonPublicModels === true);
|
||||
if (hasModelRestrictions && apiKey) {
|
||||
const modelAllowed = await isModelAllowedForKey(apiKey, modelString);
|
||||
if (!modelAllowed) return false;
|
||||
}
|
||||
|
||||
// Use getModelInfo to resolve custom prefixes, but prefer the combo
|
||||
// target's providerId when available — the model string's provider
|
||||
// prefix may differ from the credential provider ID (e.g. model
|
||||
|
||||
@@ -775,15 +775,9 @@ function providerCanUseSyntheticNoAuthFallback(providerId: string): boolean {
|
||||
|
||||
async function maybeSyntheticNoAuthFallback(
|
||||
providerId: string,
|
||||
excludedConnectionIds: Set<string>,
|
||||
allowedConnections: string[] | null = null
|
||||
excludedConnectionIds: Set<string>
|
||||
) {
|
||||
if (!providerCanUseSyntheticNoAuthFallback(providerId)) return null;
|
||||
// #9057: a key pinned to specific connections via allowedConnections must
|
||||
// NOT receive the synthetic "noauth" connection — the synthetic id is
|
||||
// never in an explicit allowlist, so returning it would let a restricted
|
||||
// key reach free providers (felo-chat, etc.) that it should not access.
|
||||
if (Array.isArray(allowedConnections) && allowedConnections.length > 0) return null;
|
||||
if (excludedConnectionIds.has(SYNTHETIC_NOAUTH_CONNECTION_ID)) return null;
|
||||
// #4954: hydrate per-account proxy/rotation config off the connection row so
|
||||
// no-auth executors (opencode, mimocode) actually honor configured proxies.
|
||||
@@ -1012,14 +1006,7 @@ export async function getProviderCredentials(
|
||||
excludeConnectionId,
|
||||
options.excludeConnectionIds
|
||||
);
|
||||
// #9057: when allowedConnections is set, the synthetic "noauth" connection
|
||||
// is never in the explicit allowlist, so we must NOT return it — fall through
|
||||
// to the normal connection-selection path so the connection allowlist is
|
||||
// respected (the no-auth provider will be rejected if it has no real connections
|
||||
// matching the allowlist, or a real connection row will be selected if present).
|
||||
if (!allowedConnections || allowedConnections.length === 0) {
|
||||
return await maybeSyntheticNoAuthFallback(resolvedId, excludedForNoAuth);
|
||||
}
|
||||
return await maybeSyntheticNoAuthFallback(resolvedId, excludedForNoAuth);
|
||||
}
|
||||
|
||||
const allowSuppressedConnections = options.allowSuppressedConnections === true;
|
||||
@@ -1146,8 +1133,7 @@ export async function getProviderCredentials(
|
||||
if (terminalConnections.length === allConnections.length) {
|
||||
const syntheticFallback = await maybeSyntheticNoAuthFallback(
|
||||
resolvedId,
|
||||
excludedConnectionIds,
|
||||
allowedConnections
|
||||
excludedConnectionIds
|
||||
);
|
||||
if (syntheticFallback) return syntheticFallback;
|
||||
|
||||
@@ -1167,8 +1153,7 @@ export async function getProviderCredentials(
|
||||
}
|
||||
const syntheticFallback = await maybeSyntheticNoAuthFallback(
|
||||
resolvedId,
|
||||
excludedConnectionIds,
|
||||
allowedConnections
|
||||
excludedConnectionIds
|
||||
);
|
||||
if (syntheticFallback) return syntheticFallback;
|
||||
log.warn("AUTH", `No credentials for ${provider}`);
|
||||
@@ -1367,8 +1352,7 @@ export async function getProviderCredentials(
|
||||
}
|
||||
const syntheticFallback = await maybeSyntheticNoAuthFallback(
|
||||
resolvedId,
|
||||
excludedConnectionIds,
|
||||
allowedConnections
|
||||
excludedConnectionIds
|
||||
);
|
||||
if (syntheticFallback) return syntheticFallback;
|
||||
log.warn("AUTH", `${provider} | all ${connections.length} accounts unavailable`);
|
||||
|
||||
5
tests/fixtures/radar-feed-canonical.json
vendored
5
tests/fixtures/radar-feed-canonical.json
vendored
@@ -46,7 +46,10 @@
|
||||
"setup": {
|
||||
"keyUrl": "https://console.groq.com/keys",
|
||||
"steps": [
|
||||
"Crie uma conta gratuita no console da Groq",
|
||||
{
|
||||
"en": "Create a free account in the Groq console",
|
||||
"pt": "Crie uma conta gratuita no console da Groq"
|
||||
},
|
||||
"Gere uma API key na página de keys",
|
||||
"Adicione a key no OmniRoute com o provider groq"
|
||||
]
|
||||
|
||||
@@ -62,12 +62,6 @@ const { getHandoff } = await import("../../../src/lib/db/contextHandoffs.ts");
|
||||
// ── Constants ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const CODEX_COMBO_NAME = "m-relay-codex-quota";
|
||||
// The control test needs its OWN combo name. resetStorage() unlinks the DB file
|
||||
// between tests, but the previous better-sqlite3 handle survives the unlink and
|
||||
// keeps writing to the same inode, so reusing the name here failed with
|
||||
// `UNIQUE constraint failed: combos.name` — a test-isolation defect, not a
|
||||
// routing one (the assertion below does not depend on the name).
|
||||
const CONTROL_COMBO_NAME = "m-relay-openai-control";
|
||||
const SESSION_HEADER_VALUE = "relay-codex-quota-001";
|
||||
const SESSION_ID = `ext:${SESSION_HEADER_VALUE}`;
|
||||
|
||||
@@ -77,7 +71,8 @@ const CODEX_RESPONSES_HOST = "chatgpt.com/backend-api/codex/responses";
|
||||
|
||||
// Summary JSON that parseHandoffJSON will successfully parse.
|
||||
const CODEX_SUMMARY_JSON = JSON.stringify({
|
||||
summary: "User is implementing a TypeScript context-relay codex quota-handoff test using TDD.",
|
||||
summary:
|
||||
"User is implementing a TypeScript context-relay codex quota-handoff test using TDD.",
|
||||
keyDecisions: ["codex provider selected", "quota threshold at 90%"],
|
||||
taskProgress: "writing deterministic integration test for codex handoff",
|
||||
activeEntities: ["combo.ts", "codexQuotaFetcher.ts", "contextHandoff.ts"],
|
||||
@@ -147,11 +142,11 @@ function buildCodexUsageBody(
|
||||
|
||||
// ── Request builder ───────────────────────────────────────────────────────────
|
||||
|
||||
function codexRequest(withSessionId = true, comboName = CODEX_COMBO_NAME) {
|
||||
function codexRequest(withSessionId = true) {
|
||||
return buildRequest({
|
||||
headers: withSessionId ? { "x-session-id": SESSION_HEADER_VALUE } : {},
|
||||
body: {
|
||||
model: comboName,
|
||||
model: CODEX_COMBO_NAME,
|
||||
stream: false,
|
||||
messages: [{ role: "user", content: "Write a TypeScript hello world." }],
|
||||
},
|
||||
@@ -264,7 +259,9 @@ test("context-relay codex quota handoff: fires and expiresAt matches session-win
|
||||
name: CODEX_COMBO_NAME,
|
||||
strategy: "context-relay",
|
||||
config: { maxRetries: 0, retryDelayMs: 0, stickyRoundRobinLimit: 1 },
|
||||
models: [{ id: "rc-codex-1", kind: "model", providerId: "codex", model: "gpt-5.3-codex" }],
|
||||
models: [
|
||||
{ id: "rc-codex-1", kind: "model", providerId: "codex", model: "gpt-5.3-codex" },
|
||||
],
|
||||
});
|
||||
|
||||
// 4. Compute quota reset times (future timestamps).
|
||||
@@ -331,10 +328,12 @@ test("context-relay codex quota handoff: does NOT fire when provider is openai (
|
||||
await seedConnection("openai", { apiKey: "sk-openai-control-no-codex-block" });
|
||||
|
||||
await combosDb.createCombo({
|
||||
name: CONTROL_COMBO_NAME,
|
||||
name: CODEX_COMBO_NAME,
|
||||
strategy: "context-relay",
|
||||
config: { maxRetries: 0, retryDelayMs: 0, stickyRoundRobinLimit: 1 },
|
||||
models: [{ id: "rc-openai-ctrl", kind: "model", providerId: "openai", model: "gpt-4o-mini" }],
|
||||
models: [
|
||||
{ id: "rc-openai-ctrl", kind: "model", providerId: "openai", model: "gpt-4o-mini" },
|
||||
],
|
||||
});
|
||||
|
||||
const seenUrls: string[] = [];
|
||||
@@ -343,7 +342,7 @@ test("context-relay codex quota handoff: does NOT fire when provider is openai (
|
||||
return buildOpenAIResponse("assistant reply ok");
|
||||
};
|
||||
|
||||
const r = await handleChat(codexRequest(true, CONTROL_COMBO_NAME));
|
||||
const r = await handleChat(codexRequest(true));
|
||||
assert.equal(r.status, 200, "openai request must return 200");
|
||||
|
||||
// Give setImmediate time to fire if the block were incorrectly entered.
|
||||
@@ -359,7 +358,7 @@ test("context-relay codex quota handoff: does NOT fire when provider is openai (
|
||||
// No codex quota handoff record in DB.
|
||||
// (The universal handoff also does not fire because no prior model is seeded,
|
||||
// so getLastSessionModel returns null → no model switch detected.)
|
||||
const handoff = getHandoff(SESSION_ID, CONTROL_COMBO_NAME);
|
||||
const handoff = getHandoff(SESSION_ID, CODEX_COMBO_NAME);
|
||||
assert.equal(
|
||||
handoff,
|
||||
null,
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { GithubExecutor } from "../../open-sse/executors/github.ts";
|
||||
|
||||
test("#8951 GitHub GPT-5.6 models must use the Responses endpoint", () => {
|
||||
const executor = new GithubExecutor();
|
||||
const urls = Object.fromEntries(
|
||||
["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"].map((model) => [
|
||||
model,
|
||||
executor.buildUrl(model, false),
|
||||
])
|
||||
);
|
||||
assert.deepEqual(urls, {
|
||||
"gpt-5.6-sol": "https://api.githubcopilot.com/responses",
|
||||
"gpt-5.6-terra": "https://api.githubcopilot.com/responses",
|
||||
"gpt-5.6-luna": "https://api.githubcopilot.com/responses",
|
||||
});
|
||||
});
|
||||
@@ -1,120 +0,0 @@
|
||||
/**
|
||||
* Regression test for #9034 — /v1/models alias-backed emission block (catalog.ts ~:1298)
|
||||
* leaked the raw provider-node UUID as the published model `id` for alias-backed models
|
||||
* (synced via `syncManagedAvailableModelAliases`), instead of the operator-configured
|
||||
* prefix. The #8327 fix only covered `owned_by`; the routable model `id` was missed.
|
||||
*
|
||||
* Root cause: the alias-backed block builds `alias` as
|
||||
* `providerIdToAlias[canonicalProviderId] || providerKey` and never consults
|
||||
* `providerIdToPrefix`. For a compatible provider node, the storage prefix is the raw
|
||||
* node UUID (managedAvailableModels.getProviderStoragePrefix()), so `providerKey` is the
|
||||
* node UUID and the catalog re-publishes it as the public model `id` (e.g.
|
||||
* `openai-compatible-chat-550e8400-.../kimi-k2`) instead of the configured prefix.
|
||||
*
|
||||
* Fix: resolve `const prefix = providerIdToPrefix[providerKey] ?? providerIdToPrefix[canonicalProviderId]`
|
||||
* and `const alias = prefix || providerIdToAlias[canonicalProviderId] || providerKey`,
|
||||
* plus add `!prefix` to the includeCanonical guard (mirroring synced :896 / custom :1245).
|
||||
*/
|
||||
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-9034-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
// Type-only imports for the module shape
|
||||
type CoreModule = typeof import("../../src/lib/db/core.ts");
|
||||
type ProvidersDbModule = typeof import("../../src/lib/db/providers.ts");
|
||||
type ModelsDbModule = typeof import("../../src/lib/db/models.ts");
|
||||
type CatalogModule = typeof import("../../src/app/api/v1/models/catalog.ts");
|
||||
type ManagedAvailableModelsModule = typeof import("../../src/lib/providerModels/managedAvailableModels.ts");
|
||||
|
||||
let core: CoreModule;
|
||||
let providersDb: ProvidersDbModule;
|
||||
let modelsDb: ModelsDbModule;
|
||||
let v1ModelsCatalog: CatalogModule;
|
||||
let managedAvailableModels: ManagedAvailableModelsModule;
|
||||
|
||||
// A realistic provider-node id shape, matching `openai-compatible-chat-<uuid>`
|
||||
const NODE_ID = "openai-compatible-chat-550e8400-e29b-41d4-a716-446655440000";
|
||||
const UUID_SHAPE_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
||||
const CONFIGURED_PREFIX = "myprefix";
|
||||
const MODEL_NAME = "kimi-k2";
|
||||
|
||||
async function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
v1ModelsCatalog.__resetCatalogBuilderRunsForTest();
|
||||
}
|
||||
|
||||
test.before(async () => {
|
||||
core = await import("../../src/lib/db/core.ts");
|
||||
providersDb = await import("../../src/lib/db/providers.ts");
|
||||
modelsDb = await import("../../src/lib/db/models.ts");
|
||||
v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts");
|
||||
managedAvailableModels = await import("../../src/lib/providerModels/managedAvailableModels.ts");
|
||||
await resetStorage();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
if (core) core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("#9034: alias-backed model id must use the configured prefix, not the raw provider-node UUID", async () => {
|
||||
// Create an openai-compatible provider node with a configured prefix
|
||||
await providersDb.createProviderNode({
|
||||
id: NODE_ID,
|
||||
type: "openai-compatible",
|
||||
name: "test node (probe)",
|
||||
prefix: CONFIGURED_PREFIX,
|
||||
baseUrl: "https://proxy.example.com",
|
||||
chatPath: "/v1/chat/completions",
|
||||
modelsPath: "/v1/models",
|
||||
});
|
||||
await providersDb.createProviderConnection({
|
||||
provider: NODE_ID,
|
||||
authType: "apikey",
|
||||
name: "test-conn",
|
||||
apiKey: "sk-test",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
providerSpecificData: {
|
||||
baseUrl: "https://proxy.example.com",
|
||||
chatPath: "/v1/chat/completions",
|
||||
modelsPath: "/v1/models",
|
||||
},
|
||||
});
|
||||
|
||||
// The real producer path: syncManagedAvailableModelAliases stores aliases as
|
||||
// `<UUID>/<modelId>` (getProviderStoragePrefix returns raw node id for compatible providers).
|
||||
// This creates a key_value alias entry that the alias-backed block in catalog.ts reads.
|
||||
await managedAvailableModels.syncManagedAvailableModelAliases(NODE_ID, [MODEL_NAME]);
|
||||
|
||||
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
|
||||
new Request("http://localhost/api/v1/models")
|
||||
);
|
||||
const body = (await response.json()) as { data: Array<Record<string, unknown>> };
|
||||
const ids = body.data.map((m) => m.id as string);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
|
||||
// (a) The configured prefix must be used in the model id
|
||||
const expectedId = `${CONFIGURED_PREFIX}/${MODEL_NAME}`;
|
||||
assert.ok(
|
||||
ids.includes(expectedId),
|
||||
`expected model id "${expectedId}" to exist in /v1/models — got: ${JSON.stringify(ids)}`
|
||||
);
|
||||
|
||||
// (b) No entry id should start with the raw node UUID when a prefix is configured
|
||||
for (const id of ids) {
|
||||
assert.equal(
|
||||
id.startsWith(NODE_ID),
|
||||
false,
|
||||
`entry id "${id}" must not start with the raw provider-node UUID "${NODE_ID}" when a prefix ("${CONFIGURED_PREFIX}") is configured`
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -1,95 +0,0 @@
|
||||
// Repro test for #9134 — /v1/audio/transcriptions rejects combo names.
|
||||
//
|
||||
// Run: node --import tsx/esm --test tests/unit/9134-repro-audio-combo-rejection.test.ts
|
||||
// Expected to PASS once the fix is applied, RED before.
|
||||
|
||||
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-9134-repro-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const { createCombo } = await import("../../src/lib/db/combos.ts");
|
||||
const { createProviderNode } = await import("../../src/lib/db/providers.ts");
|
||||
const route = await import("../../src/app/api/v1/audio/transcriptions/route.ts");
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
test.after(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
/** Minimal but structurally valid WAV so nothing rejects the upload shape. */
|
||||
function makeWav(): Blob {
|
||||
const dataLen = 1600;
|
||||
const b = Buffer.alloc(44 + dataLen);
|
||||
b.write("RIFF", 0, "ascii");
|
||||
b.writeUInt32LE(36 + dataLen, 4);
|
||||
b.write("WAVE", 8, "ascii");
|
||||
b.write("fmt ", 12, "ascii");
|
||||
b.writeUInt32LE(16, 16);
|
||||
b.writeUInt16LE(1, 20);
|
||||
b.writeUInt16LE(1, 22);
|
||||
b.writeUInt32LE(16000, 24);
|
||||
b.writeUInt32LE(32000, 28);
|
||||
b.writeUInt16LE(2, 32);
|
||||
b.writeUInt16LE(16, 34);
|
||||
b.write("data", 36, "ascii");
|
||||
b.writeUInt32LE(dataLen, 40);
|
||||
return new Blob([b], { type: "audio/wav" });
|
||||
}
|
||||
|
||||
function transcriptionRequest(model: string) {
|
||||
const fd = new FormData();
|
||||
fd.set("model", model);
|
||||
fd.set("file", makeWav(), "t.wav");
|
||||
return new Request("http://localhost/v1/audio/transcriptions", { method: "POST", body: fd });
|
||||
}
|
||||
|
||||
test("#9134 combo name is rejected instead of resolved", async () => {
|
||||
await createProviderNode({
|
||||
id: "openai-compatible-audio-transcriptions-test",
|
||||
type: "openai-compatible",
|
||||
name: "Local STT",
|
||||
prefix: "localstt",
|
||||
apiType: "audio-transcriptions",
|
||||
baseUrl: "http://localhost:9000/v1",
|
||||
} as Parameters<typeof createProviderNode>[0]);
|
||||
|
||||
await createCombo({
|
||||
name: "transcricao",
|
||||
strategy: "priority",
|
||||
models: [{ provider: "localstt", model: "whisper-1" }],
|
||||
} as Parameters<typeof createCombo>[0]);
|
||||
|
||||
globalThis.fetch = (async () =>
|
||||
new Response(JSON.stringify({ text: "ok" }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
) as typeof fetch;
|
||||
|
||||
const res = await route.POST(transcriptionRequest("transcricao"));
|
||||
const body = await res.text();
|
||||
|
||||
// The bug: the combo name "transcricao" is NOT resolved. The route returns 400
|
||||
// with "Invalid transcription model: transcricao. Use format: provider/model"
|
||||
// even though /v1/models advertises this combo and chat/embeddings resolve it.
|
||||
// Regression guard: combo names must be resolved before model parsing. This
|
||||
// was failing as `400 Invalid transcription model: transcricao` before the fix.
|
||||
assert.notEqual(
|
||||
res.status,
|
||||
400,
|
||||
`BUG #9134: combo name "transcricao" was rejected as invalid model — got status ${res.status}: ${body}`
|
||||
);
|
||||
assert.ok(
|
||||
!body.includes("Invalid transcription model"),
|
||||
`BUG #9134: combo name was not resolved — got: ${body}`
|
||||
);
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* #9057 — API key `allowedConnections` MUST gate no-auth synthetic credentials
|
||||
*
|
||||
* TDD regression test: an API key pinned via `allowedConnections` to a specific
|
||||
* connection must NOT receive synthetic no-auth credentials for free providers
|
||||
* (e.g. felo-chat).
|
||||
*/
|
||||
|
||||
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-9057-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "9057-test-secret";
|
||||
|
||||
const coreDb = await import("../../src/lib/db/core.ts");
|
||||
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
|
||||
const { getProviderCredentials } = await import("../../src/sse/services/auth.ts");
|
||||
const { isModelAllowedForKey } = await import("../../src/lib/db/apiKeys.ts");
|
||||
|
||||
const RESTRICTED_CONNECTION_UUID = "00000000-0000-4000-8000-000000000001";
|
||||
|
||||
test.after(() => {
|
||||
coreDb.resetDbInstance();
|
||||
try { fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); } catch {}
|
||||
});
|
||||
|
||||
test("#9057 LAYER1: restricted key gets NO synthetic credentials for noauth provider felo", async () => {
|
||||
// LAYER1: getProviderCredentials() with explicit allowedConnections
|
||||
// must NOT return synthetic noauth credentials because the synthetic
|
||||
// "noauth" connection is never in an explicit allowed-connections list.
|
||||
const creds = await getProviderCredentials(
|
||||
"felo",
|
||||
null,
|
||||
[RESTRICTED_CONNECTION_UUID], // allowedConnections restricts to a real UUID
|
||||
"felo-chat"
|
||||
);
|
||||
assert.equal(creds, null,
|
||||
"noauth provider felo must not leak synthetic credentials for a connection-restricted key");
|
||||
});
|
||||
|
||||
test("#9057 LAYER1: unrestricted key still gets synthetic credentials for felo", async () => {
|
||||
const creds = await getProviderCredentials(
|
||||
"felo",
|
||||
null,
|
||||
null, // allowedConnections=null means unrestricted
|
||||
"felo-chat"
|
||||
);
|
||||
assert(creds, "unrestricted key must receive synthetic credentials for felo");
|
||||
assert.equal(
|
||||
(creds as Record<string, unknown>)?.connectionId,
|
||||
"noauth",
|
||||
"synthetic noauth connection"
|
||||
);
|
||||
});
|
||||
|
||||
test("#9057 LAYER2: isModelAllowedForKey rejects felo-chat for disableNonPublicModels key", async () => {
|
||||
// Create a key with disableNonPublicModels=true
|
||||
const created = await apiKeysDb.createApiKey("dnp-9057", "machine-dnp");
|
||||
assert(created, "key must be created");
|
||||
const key = created.key;
|
||||
await apiKeysDb.updateApiKeyPermissions(created.id, {
|
||||
disableNonPublicModels: true,
|
||||
});
|
||||
|
||||
const allowed = await isModelAllowedForKey(key, "felo-chat");
|
||||
assert.equal(allowed, false, "disableNonPublicModels key must reject felo-chat");
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { parseSpeechModel, parseTranscriptionModel, parseTranslationModel } = await import("../../open-sse/config/audioRegistry.ts");
|
||||
|
||||
test("parseSpeechModel resolves dynamic provider (audio-speech apiType) by prefix", () => {
|
||||
const dynamicProviders = [
|
||||
{
|
||||
id: "mytest9096",
|
||||
baseUrl: "http://localhost:9999/v1/audio/speech",
|
||||
authType: "none" as const,
|
||||
authHeader: "none" as const,
|
||||
models: [],
|
||||
},
|
||||
];
|
||||
|
||||
const result = parseSpeechModel("mytest9096/tts-1", dynamicProviders);
|
||||
assert.equal(result.provider, "mytest9096");
|
||||
assert.equal(result.model, "tts-1");
|
||||
});
|
||||
|
||||
test("parseSpeechModel returns null for unknown dynamic provider prefix", () => {
|
||||
const dynamicProviders = [
|
||||
{
|
||||
id: "mytest9096",
|
||||
baseUrl: "http://localhost:9999/v1/audio/speech",
|
||||
authType: "none" as const,
|
||||
authHeader: "none" as const,
|
||||
models: [],
|
||||
},
|
||||
];
|
||||
|
||||
const result = parseSpeechModel("nonexistent/tts-1", dynamicProviders);
|
||||
assert.equal(result.provider, null);
|
||||
});
|
||||
|
||||
test("parseTranscriptionModel resolves dynamic provider (audio-transcriptions apiType) by prefix", () => {
|
||||
const dynamicProviders = [
|
||||
{
|
||||
id: "mytest9096",
|
||||
baseUrl: "http://localhost:9999/v1/audio/transcriptions",
|
||||
authType: "none" as const,
|
||||
authHeader: "none" as const,
|
||||
models: [],
|
||||
},
|
||||
];
|
||||
|
||||
const result = parseTranscriptionModel("mytest9096/whisper-1", dynamicProviders);
|
||||
assert.equal(result.provider, "mytest9096");
|
||||
assert.equal(result.model, "whisper-1");
|
||||
});
|
||||
|
||||
test("parseTranslationModel resolves dynamic provider (audio-transcriptions apiType) by prefix", () => {
|
||||
const dynamicProviders = [
|
||||
{
|
||||
id: "mytest9096",
|
||||
baseUrl: "http://localhost:9999/v1/audio/translations",
|
||||
authType: "none" as const,
|
||||
authHeader: "none" as const,
|
||||
models: [],
|
||||
},
|
||||
];
|
||||
|
||||
const result = parseTranslationModel("mytest9096/whisper-1", dynamicProviders);
|
||||
assert.equal(result.provider, "mytest9096");
|
||||
assert.equal(result.model, "whisper-1");
|
||||
});
|
||||
@@ -45,13 +45,6 @@ test("classify429: Antigravity 'Individual quota reached' body returns 'quota_ex
|
||||
assert.equal(classify429({ status: 429, body: { error: { message: body } } }), "quota_exhausted");
|
||||
});
|
||||
|
||||
test("classify429: auth-layer synthetic 'have exhausted their quota' returns 'quota_exhausted' (#9269)", () => {
|
||||
const body = "All antigravity accounts have exhausted their quota (reset after 5m)";
|
||||
assert.equal(looksLikeQuotaExhausted(body), true);
|
||||
assert.equal(classify429({ status: 429, body }), "quota_exhausted");
|
||||
assert.equal(classify429({ status: 429, body: { error: { message: body } } }), "quota_exhausted");
|
||||
});
|
||||
|
||||
test("classify429: Google RESOURCE_EXHAUSTED with a billing-period reset is quota exhausted", () => {
|
||||
const body = "Resource has been exhausted (e.g. check quota). (reset after 24h)";
|
||||
assert.equal(looksLikeQuotaExhausted(body), true);
|
||||
|
||||
@@ -33,11 +33,6 @@ function mkPkg(
|
||||
* @tensorflow/tfjs → dep @tensorflow/tfjs-core → dep long
|
||||
* js-tiktoken → dep base64-js
|
||||
* @huggingface/transformers present at root as a (stale) 4.2.0
|
||||
*
|
||||
* Each mock package gets a resolvable entrypoint so that isPackageIntact (which
|
||||
* checks entrypoint integrity via require.resolve) can validate the co-located
|
||||
* copy. The `main` field and corresponding index.js mirror what real npm
|
||||
* packages ship.
|
||||
*/
|
||||
function buildRoot(rootDir: string): void {
|
||||
const rootNm = join(rootDir, "node_modules");
|
||||
@@ -45,7 +40,6 @@ function buildRoot(rootDir: string): void {
|
||||
rootNm,
|
||||
"@atjsh/llmlingua-2",
|
||||
{
|
||||
main: "dist/index.js",
|
||||
dependencies: { "es-toolkit": "^1.38.0" },
|
||||
peerDependencies: {
|
||||
"@huggingface/transformers": "*",
|
||||
@@ -55,27 +49,12 @@ function buildRoot(rootDir: string): void {
|
||||
},
|
||||
{ "dist/index.js": "export const llmlingua = true;\n" }
|
||||
);
|
||||
mkPkg(rootNm, "es-toolkit", { main: "index.js" }, { "index.js": "export const esToolkit = true;\n" });
|
||||
mkPkg(
|
||||
rootNm,
|
||||
"@tensorflow/tfjs",
|
||||
{ main: "index.js", dependencies: { "@tensorflow/tfjs-core": "4.22.0" } },
|
||||
{ "index.js": "export const tfjs = true;\n" }
|
||||
);
|
||||
mkPkg(
|
||||
rootNm,
|
||||
"@tensorflow/tfjs-core",
|
||||
{ main: "index.js", dependencies: { long: "^5.0.0" } },
|
||||
{ "index.js": "export const tfjsCore = true;\n" }
|
||||
);
|
||||
mkPkg(rootNm, "long", { main: "index.js" }, { "index.js": "export const long = true;\n" });
|
||||
mkPkg(
|
||||
rootNm,
|
||||
"js-tiktoken",
|
||||
{ main: "index.js", dependencies: { "base64-js": "^1.5.1" } },
|
||||
{ "index.js": "export const tiktoken = true;\n" }
|
||||
);
|
||||
mkPkg(rootNm, "base64-js", { main: "index.js" }, { "index.js": "export const base64 = true;\n" });
|
||||
mkPkg(rootNm, "es-toolkit", {});
|
||||
mkPkg(rootNm, "@tensorflow/tfjs", { dependencies: { "@tensorflow/tfjs-core": "4.22.0" } });
|
||||
mkPkg(rootNm, "@tensorflow/tfjs-core", { dependencies: { long: "^5.0.0" } });
|
||||
mkPkg(rootNm, "long", {});
|
||||
mkPkg(rootNm, "js-tiktoken", { dependencies: { "base64-js": "^1.5.1" } });
|
||||
mkPkg(rootNm, "base64-js", {});
|
||||
// Root transformers is the STALE 4.x line — the bug we must not propagate into dist.
|
||||
mkPkg(rootNm, "@huggingface/transformers", { version: "4.2.0" });
|
||||
}
|
||||
|
||||
@@ -55,7 +55,6 @@ function buildLlmlinguaRoot(
|
||||
rootNm,
|
||||
"@atjsh/llmlingua-2",
|
||||
{
|
||||
main: "dist/index.js",
|
||||
dependencies: {
|
||||
"es-toolkit": "^1.38.0",
|
||||
},
|
||||
@@ -228,103 +227,6 @@ test("#9166 standalone assembly never overwrites an already pinned transformers
|
||||
}
|
||||
});
|
||||
|
||||
test("#9166 co-location completes a partially traced package (package.json without its main)", () => {
|
||||
const root = mkdtempSync(
|
||||
join(tmpdir(), "omniroute-docker-llmlingua-partial-9166-")
|
||||
);
|
||||
|
||||
try {
|
||||
buildLlmlinguaRoot(root);
|
||||
const { distDir, standaloneDir } = createStandalone(root);
|
||||
|
||||
// Next's file tracing materializes @atjsh/llmlingua-2 PARTIALLY in the
|
||||
// standalone: the package.json lands (its "main" points at dist/index.js)
|
||||
// but the dist/ payload does not — the exact state the Docker guard hits
|
||||
// ("Cannot find module .../dist/index.js"). A directory-level no-clobber
|
||||
// sees the dir and skips the package forever.
|
||||
mkPkg(join(standaloneDir, "node_modules"), "@atjsh/llmlingua-2", {
|
||||
main: "dist/index.js",
|
||||
});
|
||||
|
||||
assembleStandalone({
|
||||
distDir,
|
||||
outDir: standaloneDir,
|
||||
projectRoot: root,
|
||||
copyNatives: true,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
existsSync(
|
||||
join(
|
||||
standaloneDir,
|
||||
"node_modules",
|
||||
"@atjsh",
|
||||
"llmlingua-2",
|
||||
"dist",
|
||||
"index.js"
|
||||
)
|
||||
),
|
||||
"a partially traced package must be completed, not skipped as already present"
|
||||
);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("#9166 co-location is not skipped when every closure dir exists but one is partial", () => {
|
||||
const root = mkdtempSync(
|
||||
join(tmpdir(), "omniroute-docker-llmlingua-partial-all-9166-")
|
||||
);
|
||||
|
||||
try {
|
||||
buildLlmlinguaRoot(root);
|
||||
const { distDir, standaloneDir } = createStandalone(root);
|
||||
const standaloneNm = join(standaloneDir, "node_modules");
|
||||
|
||||
// Every closure package already has a directory in the standalone (so a
|
||||
// directory-level "already co-located" early-exit would fire), but the
|
||||
// llmlingua-2 one is the partial NFT-trace shell without its main.
|
||||
for (const packageName of [
|
||||
"es-toolkit",
|
||||
"@tensorflow/tfjs",
|
||||
"@tensorflow/tfjs-core",
|
||||
"long",
|
||||
"js-tiktoken",
|
||||
"base64-js",
|
||||
"@huggingface/transformers",
|
||||
"onnxruntime-node",
|
||||
]) {
|
||||
mkPkg(standaloneNm, packageName, { main: "index.js" }, {
|
||||
"index.js": "export {};\n",
|
||||
});
|
||||
}
|
||||
mkPkg(standaloneNm, "@atjsh/llmlingua-2", { main: "dist/index.js" });
|
||||
|
||||
assembleStandalone({
|
||||
distDir,
|
||||
outDir: standaloneDir,
|
||||
projectRoot: root,
|
||||
copyNatives: true,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
existsSync(
|
||||
join(
|
||||
standaloneDir,
|
||||
"node_modules",
|
||||
"@atjsh",
|
||||
"llmlingua-2",
|
||||
"dist",
|
||||
"index.js"
|
||||
)
|
||||
),
|
||||
"the closure-wide early-exit must not fire while any member is partial"
|
||||
);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("#9166 Docker explicitly installs and validates LLMLingua optionals", () => {
|
||||
const dockerfile = readFileSync(
|
||||
new URL("../../Dockerfile", import.meta.url),
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
/**
|
||||
* Regression test for #9306 — Arena AI streaming response must produce
|
||||
* Uint8Array chunks (not raw strings) so downstream consumers like
|
||||
* ensureStreamReadiness / TextDecoder.decode() do not throw TypeError.
|
||||
*
|
||||
* Run: node --import tsx/esm --test tests/unit/lmarena-stream-readiness-repro-9306.test.ts
|
||||
*/
|
||||
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { ensureStreamReadiness } from "../../open-sse/utils/streamReadiness.ts";
|
||||
import { createOpenAIArenaStream } from "../../open-sse/executors/lmarena/response.ts";
|
||||
|
||||
describe("Arena AI stream readiness (#9306)", () => {
|
||||
it("produces Uint8Array chunks consumable by ensureStreamReadiness", async () => {
|
||||
// Simulate the upstream Arena TLS reader returning Uint8Array chunks
|
||||
const upstreamReader = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode('a0:{"text":"Hello"}\n'));
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode('a0:{"text":", world!"}\nad:{}\n')
|
||||
);
|
||||
controller.close();
|
||||
},
|
||||
}).getReader();
|
||||
|
||||
const stream = createOpenAIArenaStream({
|
||||
reader: upstreamReader,
|
||||
model: "test-model",
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
|
||||
// Wrap in a Response so ensureStreamReadiness can consume it
|
||||
const response = new Response(stream, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
|
||||
// This should not throw TypeError
|
||||
const result = await ensureStreamReadiness(response, {
|
||||
timeoutMs: 5000,
|
||||
provider: "lmarena",
|
||||
model: "test-model",
|
||||
});
|
||||
|
||||
assert.ok(result.ok, "Stream readiness should succeed, not throw ERR_INVALID_ARG_TYPE");
|
||||
if (result.ok) {
|
||||
// Verify the stream body can be read
|
||||
const reader = result.response.body?.getReader();
|
||||
assert.ok(reader, "Should have a readable body");
|
||||
const decoder = new TextDecoder();
|
||||
let fullText = "";
|
||||
while (true) {
|
||||
const { done, value } = await reader!.read();
|
||||
if (done) break;
|
||||
fullText += decoder.decode(value, { stream: true });
|
||||
}
|
||||
fullText += decoder.decode();
|
||||
// Should contain the SSE data we sent
|
||||
assert.ok(fullText.includes("Hello"), "Stream should contain the SSE text");
|
||||
assert.ok(fullText.includes("world"), "Stream should contain the SSE text");
|
||||
assert.ok(fullText.includes("[DONE]"), "Stream should end with [DONE] marker");
|
||||
}
|
||||
});
|
||||
|
||||
it("does not throw TypeError when reading chunks directly", async () => {
|
||||
const upstreamReader = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode('a0:{"text":"Hello"}\nad:{}\n'));
|
||||
controller.close();
|
||||
},
|
||||
}).getReader();
|
||||
|
||||
const stream = createOpenAIArenaStream({
|
||||
reader: upstreamReader,
|
||||
model: "test-model",
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
|
||||
const reader = stream.getReader();
|
||||
const chunks: Uint8Array[] = [];
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
// If value is a string, this would throw
|
||||
chunks.push(value);
|
||||
}
|
||||
// All chunks should be Uint8Array, not string
|
||||
assert.ok(chunks.length > 0, "Should have produced at least one chunk");
|
||||
for (const chunk of chunks) {
|
||||
assert.ok(chunk instanceof Uint8Array, "Each chunk must be Uint8Array, not string");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -56,14 +56,13 @@ test.after(() => {
|
||||
else process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL;
|
||||
});
|
||||
|
||||
function mgmtCtx(headers: Headers, method = "GET", pathname = "/api/keys", peerAddress?: string) {
|
||||
function mgmtCtx(headers: Headers, method = "GET", pathname = "/api/keys") {
|
||||
return {
|
||||
request: {
|
||||
method,
|
||||
headers,
|
||||
url: `http://localhost${pathname}`,
|
||||
nextUrl: { pathname },
|
||||
socket: peerAddress ? { remoteAddress: peerAddress } : undefined,
|
||||
},
|
||||
classification: {
|
||||
routeClass: "MANAGEMENT" as const,
|
||||
@@ -269,37 +268,3 @@ test("mcp:connect key is still rejected for the non-bypassable /api/cli-tools/ru
|
||||
assert.equal(out.code, "LOCAL_ONLY");
|
||||
}
|
||||
});
|
||||
|
||||
// ─── 7. #9159 — loopback/LAN mcp:connect with requireLogin ──────────────────
|
||||
|
||||
test("#9159 mcp:connect-only key must pass /api/mcp/ from loopback when login is required", async () => {
|
||||
await seedAuthRequired();
|
||||
const created = await apiKeysDb.createApiKey("mcp-loopback-only", "machine-mcp-loopback", [
|
||||
MCP_CONNECT_SCOPE,
|
||||
]);
|
||||
const out = await managementPolicy.evaluate(
|
||||
mgmtCtx(
|
||||
new Headers({ authorization: `Bearer ${created.key}` }),
|
||||
"POST",
|
||||
"/api/mcp/stream",
|
||||
"127.0.0.1"
|
||||
)
|
||||
);
|
||||
assert.equal(out.allow, true, JSON.stringify(out));
|
||||
});
|
||||
|
||||
test("#9159 mcp:connect-only key must pass /api/mcp/ from private LAN when login is required", async () => {
|
||||
await seedAuthRequired();
|
||||
const created = await apiKeysDb.createApiKey("mcp-lan-only", "machine-mcp-lan", [
|
||||
MCP_CONNECT_SCOPE,
|
||||
]);
|
||||
const out = await managementPolicy.evaluate(
|
||||
mgmtCtx(
|
||||
new Headers({ authorization: `Bearer ${created.key}` }),
|
||||
"POST",
|
||||
"/api/mcp/stream",
|
||||
"192.168.1.20"
|
||||
)
|
||||
);
|
||||
assert.equal(out.allow, true, JSON.stringify(out));
|
||||
});
|
||||
|
||||
@@ -67,9 +67,6 @@ for (const id of [
|
||||
"gpt-5.4-mini",
|
||||
"gpt-5.4",
|
||||
"gpt-5.5",
|
||||
"gpt-5.6-sol",
|
||||
"gpt-5.6-terra",
|
||||
"gpt-5.6-luna",
|
||||
"mai-code-1-flash",
|
||||
"gpt-5-mini",
|
||||
"oswe-vscode-prime",
|
||||
|
||||
@@ -48,15 +48,19 @@ test("lint-guard carries the quality-ratchet engine (collect → ratchet → req
|
||||
|
||||
test("fast-gates carries the deterministic ratchets and security scanners from the main rail", () => {
|
||||
const block = jobBlock("fast-gates");
|
||||
// #8542: all gates run inside a single aggregation step's bash loop.
|
||||
// Check that the gate names appear in the arrays or the loop body.
|
||||
for (const needle of [
|
||||
"cycles lockfile duplication dead-code type-coverage compression-budget",
|
||||
"secrets vuln-ratchet workflows openapi-breaking",
|
||||
"typecheck:core",
|
||||
"check:dashboard-typecheck",
|
||||
"npm run check:cycles",
|
||||
"npm run check:lockfile",
|
||||
"npm run check:duplication",
|
||||
"npm run check:dead-code",
|
||||
"npm run check:type-coverage",
|
||||
"npm run check:compression-budget",
|
||||
"npm run check:secrets -- --ratchet",
|
||||
"npm run check:vuln-ratchet -- --ratchet",
|
||||
"npm run check:workflows -- --ratchet",
|
||||
"npm run check:openapi-breaking -- --ratchet",
|
||||
]) {
|
||||
assert.ok(block.includes(needle), `fast-gates must contain "${needle}"`);
|
||||
assert.ok(block.includes(needle), `fast-gates must run "${needle}"`);
|
||||
}
|
||||
assert.ok(
|
||||
block.includes(
|
||||
@@ -82,7 +86,7 @@ test("fast-gates carries the deterministic ratchets and security scanners from t
|
||||
|
||||
test("the complexity ratchet stays on the release rail (G0's written validation criterion)", () => {
|
||||
assert.ok(
|
||||
jobBlock("fast-gates").includes("complexity-ratchets"),
|
||||
jobBlock("fast-gates").includes("npm run check:complexity-ratchets"),
|
||||
"a complexity regression in a PR→release/** must be blocked by fast-gates"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -15,11 +15,7 @@
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
applyFeed,
|
||||
type MergedEntry,
|
||||
type FeedModel,
|
||||
} from "../../src/lib/radar/applyFeed.ts";
|
||||
import { applyFeed, type MergedEntry, type FeedModel } from "../../src/lib/radar/applyFeed.ts";
|
||||
import {
|
||||
getRadarCatalog,
|
||||
baselineToMergedEntries,
|
||||
@@ -70,7 +66,9 @@ function makeBaseline(): MergedEntry[] {
|
||||
];
|
||||
}
|
||||
|
||||
function makeFeedModel(overrides: Partial<FeedModel> & { provider: string; modelId: string }): FeedModel {
|
||||
function makeFeedModel(
|
||||
overrides: Partial<FeedModel> & { provider: string; modelId: string }
|
||||
): FeedModel {
|
||||
return {
|
||||
displayName: overrides.displayName ?? overrides.modelId,
|
||||
familyId: null,
|
||||
@@ -116,7 +114,7 @@ test("rule 1: feed does NOT overwrite a local override field", () => {
|
||||
});
|
||||
|
||||
const groq = result.find(
|
||||
(e) => e.provider === "groq" && e.modelId === "llama-3.3-70b-versatile",
|
||||
(e) => e.provider === "groq" && e.modelId === "llama-3.3-70b-versatile"
|
||||
)!;
|
||||
|
||||
// Local override fields must survive
|
||||
@@ -150,7 +148,7 @@ test("rule 2: feed enabled:false disables entry and carries disabledBy provenanc
|
||||
});
|
||||
|
||||
const groq = result.find(
|
||||
(e) => e.provider === "groq" && e.modelId === "llama-3.3-70b-versatile",
|
||||
(e) => e.provider === "groq" && e.modelId === "llama-3.3-70b-versatile"
|
||||
)!;
|
||||
|
||||
assert.equal(groq.enabled, false);
|
||||
@@ -191,9 +189,7 @@ test("rule 3: user-added entry not in feed survives untouched", () => {
|
||||
tombstones: new Set(),
|
||||
});
|
||||
|
||||
const custom = result.find(
|
||||
(e) => e.provider === "custom" && e.modelId === "my-local-model",
|
||||
)!;
|
||||
const custom = result.find((e) => e.provider === "custom" && e.modelId === "my-local-model")!;
|
||||
|
||||
assert.equal(custom.displayName, "My Local Model");
|
||||
assert.equal(custom.monthlyTokens, 50_000);
|
||||
@@ -242,7 +238,7 @@ test("rule 3b: user-added entry that IS in the feed merges with rule 1", () => {
|
||||
});
|
||||
|
||||
const groq = result.filter(
|
||||
(e) => e.provider === "groq" && e.modelId === "llama-3.3-70b-versatile",
|
||||
(e) => e.provider === "groq" && e.modelId === "llama-3.3-70b-versatile"
|
||||
);
|
||||
|
||||
// Should be deduplicated to ONE entry
|
||||
@@ -262,7 +258,7 @@ test("rule 3b: user-added entry that IS in the feed merges with rule 1", () => {
|
||||
test("rule 4: tombstone prevents feed from resurrecting a deleted entry", () => {
|
||||
// Baseline has an entry for gemini, but user deleted it
|
||||
const baseline = makeBaseline().filter(
|
||||
(e) => !(e.provider === "gemini" && e.modelId === "gemini-2.5-flash"),
|
||||
(e) => !(e.provider === "gemini" && e.modelId === "gemini-2.5-flash")
|
||||
);
|
||||
|
||||
const feed: FeedModel[] = [
|
||||
@@ -283,9 +279,7 @@ test("rule 4: tombstone prevents feed from resurrecting a deleted entry", () =>
|
||||
tombstones,
|
||||
});
|
||||
|
||||
const gemini = result.find(
|
||||
(e) => e.provider === "gemini" && e.modelId === "gemini-2.5-flash",
|
||||
);
|
||||
const gemini = result.find((e) => e.provider === "gemini" && e.modelId === "gemini-2.5-flash");
|
||||
|
||||
// Must NOT be resurrected
|
||||
assert.equal(gemini, undefined);
|
||||
@@ -353,9 +347,7 @@ test("applyFeed: feed-only entry is added with origin 'radar'", () => {
|
||||
tombstones: new Set(),
|
||||
});
|
||||
|
||||
const added = result.find(
|
||||
(e) => e.provider === "new-provider" && e.modelId === "new-model",
|
||||
);
|
||||
const added = result.find((e) => e.provider === "new-provider" && e.modelId === "new-model");
|
||||
|
||||
assert.ok(added, "feed-only entry should be present");
|
||||
assert.equal(added.displayName, "Brand New Model");
|
||||
@@ -388,7 +380,7 @@ test("applyFeed: feed fields merge over baseline where no local override", () =>
|
||||
});
|
||||
|
||||
const groq = result.find(
|
||||
(e) => e.provider === "groq" && e.modelId === "llama-3.3-70b-versatile",
|
||||
(e) => e.provider === "groq" && e.modelId === "llama-3.3-70b-versatile"
|
||||
)!;
|
||||
|
||||
// Feed values win when no local override
|
||||
@@ -440,7 +432,7 @@ test("applyFeed: duplicate key (baseline + feed) produces single merged entry",
|
||||
});
|
||||
|
||||
const groqEntries = result.filter(
|
||||
(e) => e.provider === "groq" && e.modelId === "llama-3.3-70b-versatile",
|
||||
(e) => e.provider === "groq" && e.modelId === "llama-3.3-70b-versatile"
|
||||
);
|
||||
|
||||
assert.equal(groqEntries.length, 1, "should be deduplicated to one entry");
|
||||
@@ -465,9 +457,7 @@ test("rule 4b: tombstoned entry removed even when baseline has it", () => {
|
||||
tombstones,
|
||||
});
|
||||
|
||||
const groq = result.find(
|
||||
(e) => e.provider === "groq" && e.modelId === "llama-3.3-70b-versatile",
|
||||
);
|
||||
const groq = result.find((e) => e.provider === "groq" && e.modelId === "llama-3.3-70b-versatile");
|
||||
|
||||
assert.equal(groq, undefined, "tombstoned entry should be excluded");
|
||||
});
|
||||
@@ -494,9 +484,7 @@ test("origin switches to 'radar' when feed updates a baseline entry", () => {
|
||||
tombstones: new Set(),
|
||||
});
|
||||
|
||||
const gemini = result.find(
|
||||
(e) => e.provider === "gemini" && e.modelId === "gemini-2.5-flash",
|
||||
)!;
|
||||
const gemini = result.find((e) => e.provider === "gemini" && e.modelId === "gemini-2.5-flash")!;
|
||||
|
||||
assert.equal(gemini.origin, "radar");
|
||||
assert.equal(gemini.displayName, "Updated Gemini");
|
||||
@@ -612,7 +600,7 @@ test("getRadarCatalog: valid cache returns merged entries with meta", () => {
|
||||
|
||||
// The feed has groq:llama-3.3-70b-versatile, which merges over baseline
|
||||
const groq = result.entries.find(
|
||||
(e) => e.provider === "groq" && e.modelId === "llama-3.3-70b-versatile",
|
||||
(e) => e.provider === "groq" && e.modelId === "llama-3.3-70b-versatile"
|
||||
)!;
|
||||
|
||||
assert.equal(groq.displayName, "Feed Groq Name");
|
||||
@@ -714,7 +702,7 @@ test("FIX2 mergeOne path: contextWindow/capabilities/limits/setup survive merge
|
||||
});
|
||||
|
||||
const groq = result.find(
|
||||
(e) => e.provider === "groq" && e.modelId === "llama-3.3-70b-versatile",
|
||||
(e) => e.provider === "groq" && e.modelId === "llama-3.3-70b-versatile"
|
||||
)!;
|
||||
|
||||
assert.equal(groq.contextWindow, 131072);
|
||||
@@ -758,13 +746,11 @@ test("FIX2 feedModelToMerged path: contextWindow/capabilities/limits/setup survi
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// FIX 4 — feedModelToMerged() must honor an `enabled` local override instead
|
||||
// of unconditionally forcing `enabled:false` when the feed disables the model.
|
||||
// mergeOne() already gets this right (overrides applied AFTER rule 2); this
|
||||
// pins the same semantics on the feed-only path.
|
||||
// Feed `enabled:false` is the safety exception to local override precedence:
|
||||
// a model confirmed dead upstream must not be resurrected locally.
|
||||
// ===========================================================================
|
||||
|
||||
test("FIX4: feed-only entry with local override enabled:true wins over feed enabled:false", () => {
|
||||
test("rule 2: feed-only entry stays disabled even with local enabled:true", () => {
|
||||
const baseline = makeBaseline();
|
||||
const feed: FeedModel[] = [
|
||||
makeFeedModel({
|
||||
@@ -786,11 +772,11 @@ test("FIX4: feed-only entry with local override enabled:true wins over feed enab
|
||||
});
|
||||
|
||||
const entry = result.find(
|
||||
(e) => e.provider === "new-provider" && e.modelId === "disabled-model",
|
||||
(e) => e.provider === "new-provider" && e.modelId === "disabled-model"
|
||||
)!;
|
||||
|
||||
assert.equal(entry.enabled, true, "local override must win over feed disable");
|
||||
assert.equal(entry.disabledBy, undefined, "must not carry radar disabledBy when overridden on");
|
||||
assert.equal(entry.enabled, false, "a local override must not resurrect a dead upstream model");
|
||||
assert.equal(entry.disabledBy, "radar");
|
||||
});
|
||||
|
||||
test("FIX4: feed-only entry with NO override still gets disabled with disabledBy provenance", () => {
|
||||
@@ -811,7 +797,7 @@ test("FIX4: feed-only entry with NO override still gets disabled with disabledBy
|
||||
});
|
||||
|
||||
const entry = result.find(
|
||||
(e) => e.provider === "new-provider" && e.modelId === "disabled-model-2",
|
||||
(e) => e.provider === "new-provider" && e.modelId === "disabled-model-2"
|
||||
)!;
|
||||
|
||||
assert.equal(entry.enabled, false);
|
||||
|
||||
@@ -165,7 +165,9 @@ test("setRadarKey encrypts at rest and getRadarSettings decrypts", () => {
|
||||
assert.equal(settings.supporterKey, clearKey, "getRadarSettings must return the clear key");
|
||||
|
||||
// Direct DB query to prove encryption at rest
|
||||
interface SettingsRow { supporter_key_encrypted: string | null }
|
||||
interface SettingsRow {
|
||||
supporter_key_encrypted: string | null;
|
||||
}
|
||||
const row = db
|
||||
.prepare("SELECT supporter_key_encrypted FROM radar_settings WHERE id = 1")
|
||||
.get() as SettingsRow;
|
||||
@@ -197,6 +199,36 @@ test("setRadarKey(null) clears the key", () => {
|
||||
assert.equal(cleared.supporter_key_encrypted, null, "DB value must be null");
|
||||
});
|
||||
|
||||
test("changing the supporter key invalidates the entitlement-sensitive referrals cache", () => {
|
||||
radar.setRadarCache({
|
||||
version: "2026.08.07.1",
|
||||
tier: "live",
|
||||
payload: '{"models":[]}',
|
||||
signature: "catalog-live-signature",
|
||||
});
|
||||
radar.setRadarReferralsCache({
|
||||
generatedAt: "2026-08-07T12:00:00.000Z",
|
||||
tier: "live",
|
||||
payload: '{"referrals":{"fixed":[],"campaigns":[{"provider":"groq"}]}}',
|
||||
signature: "live-signature",
|
||||
});
|
||||
assert.ok(radar.getRadarReferralsCache(), "precondition: live referrals cache exists");
|
||||
assert.ok(radar.getRadarCache(), "precondition: live catalog cache exists");
|
||||
|
||||
radar.setRadarKey("omr_" + "d".repeat(40));
|
||||
|
||||
assert.equal(
|
||||
radar.getRadarReferralsCache(),
|
||||
null,
|
||||
"a new key must force the next referrals read to resolve entitlement server-side"
|
||||
);
|
||||
assert.equal(
|
||||
radar.getRadarCache(),
|
||||
null,
|
||||
"a new key must force the next catalog sync to resolve entitlement server-side"
|
||||
);
|
||||
});
|
||||
|
||||
test("setRadarKey uses existing AES-256-GCM encryption from encryption.ts", () => {
|
||||
const db = core.getDbInstance();
|
||||
const clearKey = "omr_" + "c".repeat(40);
|
||||
@@ -271,7 +303,9 @@ test("second setRadarReferralsCache REPLACES the row (still single row)", () =>
|
||||
assert.equal(result.tier, "live", "must have the second tier");
|
||||
assert.equal(result.payload, '{"new":true}', "must have the second payload");
|
||||
|
||||
const count = db.prepare("SELECT COUNT(*) AS c FROM radar_referrals_cache").get() as { c: number };
|
||||
const count = db.prepare("SELECT COUNT(*) AS c FROM radar_referrals_cache").get() as {
|
||||
c: number;
|
||||
};
|
||||
assert.equal(count.c, 1, "must have exactly one row");
|
||||
});
|
||||
|
||||
|
||||
45
tests/unit/radar-localized-feed.test.ts
Normal file
45
tests/unit/radar-localized-feed.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { test } from "node:test";
|
||||
|
||||
import { RadarFeedSchema } from "../../src/lib/radar/feedSchema.ts";
|
||||
|
||||
const fixture = JSON.parse(
|
||||
readFileSync(new URL("../fixtures/radar-feed-canonical.json", import.meta.url), "utf8")
|
||||
) as Record<string, unknown>;
|
||||
|
||||
test("canonical fixture carries D25 localized setup and accepts localized quirks", () => {
|
||||
const localized = structuredClone(fixture) as {
|
||||
models: Array<{ setup: { steps: unknown[] } | null }>;
|
||||
quirks: Array<{ title: unknown; body: unknown }>;
|
||||
};
|
||||
localized.quirks = [
|
||||
{
|
||||
slug: "shared-pool",
|
||||
title: { en: "Shared quota", pt: "Cota compartilhada" },
|
||||
body: { en: "Models share one pool." },
|
||||
severity: "info",
|
||||
targets: [{ provider: "groq", modelGlob: null }],
|
||||
},
|
||||
];
|
||||
const parsed = RadarFeedSchema.parse(localized);
|
||||
assert.deepEqual(parsed.models[0]!.setup!.steps[0], {
|
||||
en: "Create a free account in the Groq console",
|
||||
pt: "Crie uma conta gratuita no console da Groq",
|
||||
});
|
||||
});
|
||||
|
||||
test("RadarFeedSchema preserves schema-v1 legacy setup and quirk strings", () => {
|
||||
const legacy = structuredClone(fixture) as {
|
||||
models: Array<{ setup: { steps: unknown[] } | null }>;
|
||||
quirks: Array<{ title: unknown; body: unknown }>;
|
||||
};
|
||||
legacy.models[0]!.setup!.steps[0] = "Create an account";
|
||||
legacy.quirks[0]!.title = "Shared quota";
|
||||
legacy.quirks[0]!.body = "Models share one pool.";
|
||||
|
||||
const parsed = RadarFeedSchema.parse(legacy);
|
||||
assert.equal(parsed.models[0]!.setup!.steps[0], "Create an account");
|
||||
assert.equal(parsed.quirks[0]!.title, "Shared quota");
|
||||
assert.equal(parsed.quirks[0]!.body, "Models share one pool.");
|
||||
});
|
||||
@@ -118,7 +118,11 @@ test("RadarReferralsFeedSchema: rejects missing referrals section", () => {
|
||||
const feed = baseReferralsFeed();
|
||||
delete (feed as Record<string, unknown>).referrals;
|
||||
const result = referralsFeedSchema.RadarReferralsFeedSchema.safeParse(feed);
|
||||
assert.equal(result.success, false, "referrals section is required (no old-feed compat needed here)");
|
||||
assert.equal(
|
||||
result.success,
|
||||
false,
|
||||
"referrals section is required (no old-feed compat needed here)"
|
||||
);
|
||||
});
|
||||
|
||||
test("RadarReferralsFeedSchema: rejects a non-https referral url", () => {
|
||||
@@ -215,7 +219,10 @@ test("syncRadarReferrals: valid signature => cache updated, payload byte-identic
|
||||
},
|
||||
fetch: (() =>
|
||||
Promise.resolve(
|
||||
mockResponse(bytes, { "x-omniroute-feed-signature": sig, "x-omniroute-feed-tier": "community" })
|
||||
mockResponse(bytes, {
|
||||
"x-omniroute-feed-signature": sig,
|
||||
"x-omniroute-feed-tier": "community",
|
||||
})
|
||||
)) as unknown as typeof globalThis.fetch,
|
||||
now: () => new Date("2026-08-07T12:05:00.000Z"),
|
||||
});
|
||||
@@ -296,32 +303,47 @@ test("syncRadarReferrals: valid sig over garbage JSON => invalid_schema, cache u
|
||||
// syncRadarReferrals — generatedAt floor (replay/no-op guard)
|
||||
// ===========================================================================
|
||||
|
||||
test("syncRadarReferrals: same generatedAt as cache => stale, cache untouched", async () => {
|
||||
test("syncRadarReferrals: same generatedAt with a new served tier replaces the cache", async () => {
|
||||
const feed = baseReferralsFeed("2026-08-07T12:00:00.000Z");
|
||||
(feed.referrals as { campaigns: Array<Record<string, unknown>> }).campaigns = [
|
||||
{
|
||||
provider: "groq",
|
||||
url: "https://groq.com/?campaign=live",
|
||||
kind: "campanha",
|
||||
validUntil: null,
|
||||
requiredAction: null,
|
||||
isDefault: false,
|
||||
},
|
||||
];
|
||||
const bytes = feedBytes(feed);
|
||||
const sig = signBytes(bytes);
|
||||
let cacheWritten = false;
|
||||
const cacheStore: referralsSync.RadarReferralsCacheEntry[] = [];
|
||||
|
||||
const result = await referralsSync.syncRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
getSettings: () => ({ optIn: true, supporterKey: "omr_" + "a".repeat(40) }),
|
||||
getCache: () => ({
|
||||
generatedAt: "2026-08-07T12:00:00.000Z",
|
||||
tier: "community",
|
||||
payload: "{}",
|
||||
signature: "old-sig",
|
||||
}),
|
||||
setCache: () => {
|
||||
cacheWritten = true;
|
||||
setCache: (entry) => {
|
||||
cacheStore.push(entry);
|
||||
},
|
||||
fetch: (() =>
|
||||
Promise.resolve(
|
||||
mockResponse(bytes, { "x-omniroute-feed-signature": sig })
|
||||
mockResponse(bytes, {
|
||||
"x-omniroute-feed-signature": sig,
|
||||
"x-omniroute-feed-tier": "live",
|
||||
})
|
||||
)) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.equal(result.status, "stale");
|
||||
assert.equal(cacheWritten, false);
|
||||
assert.equal(result.status, "updated");
|
||||
assert.equal(cacheStore.length, 1);
|
||||
assert.equal(cacheStore[0]!.tier, "live");
|
||||
assert.equal(cacheStore[0]!.payload, bytes.toString("utf-8"));
|
||||
});
|
||||
|
||||
test("syncRadarReferrals: older generatedAt than cache => stale (replay rejected)", async () => {
|
||||
@@ -487,7 +509,9 @@ test("syncRadarReferrals: header absent => falls back to 'community' (no body ti
|
||||
cacheStore.push(entry);
|
||||
},
|
||||
fetch: (() =>
|
||||
Promise.resolve(mockResponse(bytes, { "x-omniroute-feed-signature": sig }))) as unknown as typeof globalThis.fetch,
|
||||
Promise.resolve(
|
||||
mockResponse(bytes, { "x-omniroute-feed-signature": sig })
|
||||
)) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.equal(result.status, "updated");
|
||||
@@ -596,7 +620,9 @@ test("syncRadarReferrals: HTTP non-200 => error mentioning the status code", asy
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
fetch: (() =>
|
||||
Promise.resolve(mockResponse(Buffer.from("Internal Server Error"), {}, 500))) as unknown as typeof globalThis.fetch,
|
||||
Promise.resolve(
|
||||
mockResponse(Buffer.from("Internal Server Error"), {}, 500)
|
||||
)) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.equal(result.status, "error");
|
||||
@@ -646,7 +672,8 @@ test("FIX: oversized body without a trustworthy Content-Length header => too_lar
|
||||
setCache: () => {
|
||||
setCacheCalled = true;
|
||||
},
|
||||
fetch: (() => Promise.resolve(mockResponse(oversized, {}))) as unknown as typeof globalThis.fetch,
|
||||
fetch: (() =>
|
||||
Promise.resolve(mockResponse(oversized, {}))) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.deepEqual(result, { status: "too_large" });
|
||||
|
||||
28
tests/unit/radar-setup-connections.test.ts
Normal file
28
tests/unit/radar-setup-connections.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
|
||||
import {
|
||||
firstProviderConnectionId,
|
||||
providerConnectionsRequestUrl,
|
||||
} from "../../src/lib/radar/setupConnections.ts";
|
||||
|
||||
test("providerConnectionsRequestUrl filters the existing providers API", () => {
|
||||
assert.equal(
|
||||
providerConnectionsRequestUrl("openrouter/custom"),
|
||||
"/api/providers?provider=openrouter%2Fcustom"
|
||||
);
|
||||
});
|
||||
|
||||
test("firstProviderConnectionId selects a real connection id, never the provider slug", () => {
|
||||
assert.equal(
|
||||
firstProviderConnectionId(
|
||||
[
|
||||
{ id: "connection-disabled", provider: "groq", isActive: false },
|
||||
{ id: "connection-active", provider: "groq", isActive: true },
|
||||
],
|
||||
"groq"
|
||||
),
|
||||
"connection-active"
|
||||
);
|
||||
assert.equal(firstProviderConnectionId([], "groq"), null);
|
||||
});
|
||||
@@ -409,6 +409,35 @@ test("syncRadar: version floor — incoming older => stale", async () => {
|
||||
assert.equal(cacheWritten, false, "cache must NOT be overwritten with older version");
|
||||
});
|
||||
|
||||
test("syncRadar: an entitlement downgrade replaces a newer live cache with community", async () => {
|
||||
const sig = signBytes(FIXTURE_BYTES);
|
||||
const cacheStore: syncMod.RadarCacheEntry[] = [];
|
||||
|
||||
const result = await syncMod.syncRadar({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: "omr_" + "a".repeat(40) }),
|
||||
getCache: () => ({
|
||||
version: "2026.08.02.1",
|
||||
tier: "live",
|
||||
payload: "{}",
|
||||
signature: "old-live-sig",
|
||||
}),
|
||||
setCache: (entry) => cacheStore.push(entry),
|
||||
fetch: (() =>
|
||||
Promise.resolve(
|
||||
mockResponse(FIXTURE_BYTES, {
|
||||
"x-omniroute-feed-signature": sig,
|
||||
"x-omniroute-feed-tier": "community",
|
||||
})
|
||||
)) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.equal(result.status, "updated");
|
||||
assert.equal(cacheStore.length, 1);
|
||||
assert.equal(cacheStore[0]!.tier, "community");
|
||||
assert.equal(cacheStore[0]!.version, "2026.08.01.1");
|
||||
});
|
||||
|
||||
test("syncRadar: version floor — incoming newer => updated", async () => {
|
||||
// Modify fixture to have a newer version
|
||||
const fixtureObj = JSON.parse(FIXTURE_STRING);
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { parse } from "yaml";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(__dirname, "../..");
|
||||
const WORKFLOW = resolve(repoRoot, ".github/workflows/quality.yml");
|
||||
|
||||
function loadWorkflow(): any {
|
||||
return parse(readFileSync(WORKFLOW, "utf8"));
|
||||
}
|
||||
|
||||
function invokesGate(run: string): boolean {
|
||||
if (!run) return false;
|
||||
return /npm run (check:|typecheck:)/.test(run) || /npm run "check/.test(run) || /npm run \\"check/.test(run);
|
||||
}
|
||||
function stepCanFail(step: any): boolean {
|
||||
return step?.["continue-on-error"] !== true;
|
||||
}
|
||||
|
||||
test("repro #8542: fast-gates must not fail-fast into a later gate", () => {
|
||||
const wf = loadWorkflow();
|
||||
const job = wf.jobs?.["fast-gates"];
|
||||
assert.ok(job, "fast-gates job must exist");
|
||||
const steps: any[] = job.steps ?? [];
|
||||
assert.ok(steps.length >= 5, `fast-gates must have >=5 steps, got ${steps.length}`);
|
||||
|
||||
const gateSteps = steps.map((s, i) => ({ s, i })).filter(({ s }) => invokesGate(s?.run ?? ""));
|
||||
assert.ok(gateSteps.length >= 1, `expected >=1 gate step, got ${gateSteps.length}`);
|
||||
|
||||
const maskedPairs: string[] = [];
|
||||
for (let a = 0; a < gateSteps.length; a++) {
|
||||
const stepA = gateSteps[a];
|
||||
if (!stepCanFail(stepA.s)) continue;
|
||||
for (let b = a + 1; b < gateSteps.length; b++) {
|
||||
const stepB = gateSteps[b];
|
||||
maskedPairs.push(
|
||||
`step ${stepA.i + 1} (${stepA.s.name ?? String(stepA.s.run).split("\n")[0].slice(0, 40)})` +
|
||||
` can fail and masks step ${stepB.i + 1} (${stepB.s.name ?? String(stepB.s.run).split("\n")[0].slice(0, 40)})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
assert.deepEqual(
|
||||
maskedPairs,
|
||||
[],
|
||||
`FAIL-FAST MASKING PRESENT (${maskedPairs.length} pair(s)): a failing gate step aborts the job and every later gate reports "skipped". This is the #8542 mechanism.\n` +
|
||||
maskedPairs.slice(0, 12).join("\n") +
|
||||
(maskedPairs.length > 12 ? `\n... (+${maskedPairs.length - 12} more)` : "")
|
||||
);
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* Guard for the t06:route-validation gate (Hard Rule #7 — always validate inputs
|
||||
* with Zod schemas).
|
||||
*
|
||||
* The gate (scripts/check/check-route-validation.mjs) is a source scan: any
|
||||
* `route.ts` under src/app/api that calls `request.json()` must also call
|
||||
* `validateBody()` or `.safeParse()`. It has NO allowlist, so a route that
|
||||
* hand-rolls `typeof x === "string"` checks passes review but fails CI — which
|
||||
* is exactly how four routes (#9445 marketplace install, #8523's three Dario
|
||||
* admin routes) landed on release/v3.8.50 and kept the branch out of
|
||||
* release-green (#9737).
|
||||
*
|
||||
* This test runs the same rule inside the unit suite so the violation surfaces
|
||||
* on the PR that introduces it, instead of on the next base-red sweep.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const REPO_ROOT = path.resolve(import.meta.dirname, "..", "..");
|
||||
const API_ROOT = path.join(REPO_ROOT, "src", "app", "api");
|
||||
|
||||
function collectRouteFiles(dir: string): string[] {
|
||||
const files: string[] = [];
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...collectRouteFiles(full));
|
||||
} else if (entry.isFile() && entry.name === "route.ts") {
|
||||
files.push(full);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
test("every API route reading request.json() validates it with Zod (t06)", () => {
|
||||
const offenders: string[] = [];
|
||||
|
||||
for (const file of collectRouteFiles(API_ROOT)) {
|
||||
const source = fs.readFileSync(file, "utf8");
|
||||
if (!/request\.json\s*\(/.test(source)) continue;
|
||||
if (/\bvalidateBody\s*\(/.test(source) || /\.safeParse\s*\(/.test(source)) continue;
|
||||
offenders.push(path.relative(REPO_ROOT, file));
|
||||
}
|
||||
|
||||
assert.deepEqual(
|
||||
offenders,
|
||||
[],
|
||||
`routes call request.json() without validateBody()/.safeParse() — hand-rolled ` +
|
||||
`typeof checks do not satisfy Hard Rule #7 and fail the t06 CI gate:\n ` +
|
||||
offenders.join("\n ")
|
||||
);
|
||||
});
|
||||
@@ -23,17 +23,11 @@ test("sseCommentsEnabled defaults to true when the env var is unset", () => {
|
||||
withEnv(undefined, () => assert.equal(sseCommentsEnabled(), true));
|
||||
});
|
||||
|
||||
test("sseCommentsEnabled is false for 'off', 'false', '0', 'no' (case-insensitive)", () => {
|
||||
test("sseCommentsEnabled is false only when set to 'off' (case-insensitive)", () => {
|
||||
withEnv("off", () => assert.equal(sseCommentsEnabled(), false));
|
||||
withEnv("OFF", () => assert.equal(sseCommentsEnabled(), false));
|
||||
withEnv("false", () => assert.equal(sseCommentsEnabled(), false));
|
||||
withEnv("FALSE", () => assert.equal(sseCommentsEnabled(), false));
|
||||
withEnv("0", () => assert.equal(sseCommentsEnabled(), false));
|
||||
withEnv("no", () => assert.equal(sseCommentsEnabled(), false));
|
||||
withEnv("NO", () => assert.equal(sseCommentsEnabled(), false));
|
||||
withEnv("on", () => assert.equal(sseCommentsEnabled(), true));
|
||||
withEnv("yes", () => assert.equal(sseCommentsEnabled(), true));
|
||||
withEnv("1", () => assert.equal(sseCommentsEnabled(), true));
|
||||
withEnv("false", () => assert.equal(sseCommentsEnabled(), true));
|
||||
});
|
||||
|
||||
test("shapeForClientFormat maps known client formats", () => {
|
||||
|
||||
@@ -1,61 +1,120 @@
|
||||
/**
|
||||
* #8853 — authenticated HTTP proxy health checks drop credentials
|
||||
*
|
||||
* Root cause: both the auto-test route and the scheduler build proxy URLs
|
||||
* manually as `${proxy.type}://${proxy.host}:${proxy.port}`, dropping
|
||||
* username/password. The `proxyConfigToUrl()` function in proxyDispatcher.ts
|
||||
* already handles URL-encoded credentials correctly.
|
||||
*
|
||||
* We prove the bug by showing that the proxy URL produced by the current
|
||||
* manual construction lacks credentials, and that `proxyConfigToUrl()` with
|
||||
* the same config object includes them — therefore the fix is to reuse it.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { openaiResponsesToOpenAIRequest } from "../../open-sse/translator/request/openai-responses.ts";
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value as Record<string, unknown>;
|
||||
// The function that fixes the bug — we import it here to verify it works
|
||||
import { proxyConfigToUrl } from "@omniroute/open-sse/utils/proxyDispatcher";
|
||||
|
||||
// ── proxyConfigToUrl credential tests ──────────────────────────────────────
|
||||
|
||||
test("#8853 proxyConfigToUrl encodes username and password into proxy URL", () => {
|
||||
const url = proxyConfigToUrl({
|
||||
type: "http",
|
||||
host: "127.0.0.1",
|
||||
port: 3128,
|
||||
username: "alice",
|
||||
password: "s3cret",
|
||||
});
|
||||
assert.ok(url, "proxyConfigToUrl must return a URL");
|
||||
assert.match(url!, /:\/\/alice:s3cret@/, "URL must contain credentials");
|
||||
});
|
||||
|
||||
test("#8853 proxyConfigToUrl encodes special characters in credentials", () => {
|
||||
const url = proxyConfigToUrl({
|
||||
type: "http",
|
||||
host: "proxy.example.com",
|
||||
port: 8080,
|
||||
username: "user@domain",
|
||||
password: "p@ss:w0rd",
|
||||
});
|
||||
assert.ok(url, "proxyConfigToUrl must return a URL");
|
||||
assert.match(url!, /:\/\/user%40domain:p%40ss%3Aw0rd@/, "URL must URL-encode special chars");
|
||||
});
|
||||
|
||||
test("#8853 proxyConfigToUrl omits auth when no username", () => {
|
||||
const url = proxyConfigToUrl({
|
||||
type: "http",
|
||||
host: "127.0.0.1",
|
||||
port: 3128,
|
||||
});
|
||||
assert.ok(url, "proxyConfigToUrl must return a URL");
|
||||
assert.doesNotMatch(url!, /@/, "URL must not contain @ (no auth)");
|
||||
});
|
||||
|
||||
test("#8853 proxyConfigToUrl handles IPv6 host with family", () => {
|
||||
const url = proxyConfigToUrl({
|
||||
type: "http",
|
||||
host: "[::1]",
|
||||
port: 3128,
|
||||
family: "ipv6",
|
||||
});
|
||||
assert.ok(url, "proxyConfigToUrl must return a URL");
|
||||
assert.match(url!, /\[::1\]/, "IPv6 host must be bracketed");
|
||||
});
|
||||
|
||||
// ── Simulate the buggy construction ─────────────────────────────────────────
|
||||
|
||||
function buggyManualUrl(proxy: { type: string; host: string; port: number }) {
|
||||
return `${proxy.type}://${proxy.host}:${proxy.port}`;
|
||||
}
|
||||
|
||||
for (const variant of ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]) {
|
||||
test(`#8997 ${variant} nested reasoning.effort max survives promotion`, () => {
|
||||
const translated = asRecord(
|
||||
openaiResponsesToOpenAIRequest(
|
||||
variant,
|
||||
{ model: variant, input: "hello", reasoning: { effort: "max" } },
|
||||
false,
|
||||
{}
|
||||
)
|
||||
);
|
||||
assert.equal(translated.reasoning_effort, "max");
|
||||
});
|
||||
test("#8853 manual URL construction (current bug) drops credentials", () => {
|
||||
const proxy = {
|
||||
type: "http",
|
||||
host: "127.0.0.1",
|
||||
port: 3128,
|
||||
username: "alice",
|
||||
password: "s3cret",
|
||||
};
|
||||
const manualUrl = buggyManualUrl(proxy);
|
||||
assert.doesNotMatch(manualUrl, /alice/, "Buggy URL must NOT contain username");
|
||||
assert.doesNotMatch(manualUrl, /s3cret/, "Buggy URL must NOT contain password");
|
||||
|
||||
test(`#8997 ${variant} flat reasoning_effort max survives promotion`, () => {
|
||||
const translated = asRecord(
|
||||
openaiResponsesToOpenAIRequest(
|
||||
variant,
|
||||
{ model: variant, input: "hello", reasoning_effort: "max" },
|
||||
false,
|
||||
{}
|
||||
)
|
||||
);
|
||||
assert.equal(translated.reasoning_effort, "max");
|
||||
});
|
||||
}
|
||||
// Compare with proxyConfigToUrl which includes credentials
|
||||
const fixedUrl = proxyConfigToUrl(proxy);
|
||||
assert.ok(fixedUrl);
|
||||
assert.match(fixedUrl!, /alice/, "Fixed URL must contain username");
|
||||
assert.match(fixedUrl!, /s3cret/, "Fixed URL must contain password");
|
||||
});
|
||||
|
||||
test("non-GPT-5.6 models still get max downgraded to xhigh", () => {
|
||||
const translated = asRecord(
|
||||
openaiResponsesToOpenAIRequest(
|
||||
"gpt-4o",
|
||||
{ model: "gpt-4o", input: "hello", reasoning: { effort: "max" } },
|
||||
false,
|
||||
{}
|
||||
)
|
||||
);
|
||||
assert.equal(translated.reasoning_effort, "xhigh");
|
||||
// ── Verify the scheduler and auto-test would use proxyConfigToUrl ──────────
|
||||
|
||||
// #9140 — VS Code routes filter out built-in auto models
|
||||
const { isUsableChatModel } = await import(
|
||||
"../../src/app/api/v1/vscode/[token]/usableChatModel.ts"
|
||||
);
|
||||
test("#8853 proxyConfigToUrl accepts ProxyRegistryRecord-shaped object", () => {
|
||||
// Simulating the shape of a proxy record returned by listProxies({ includeSecrets: true })
|
||||
const proxyRecord = {
|
||||
id: "p1",
|
||||
name: "test",
|
||||
type: "http",
|
||||
host: "10.0.0.1",
|
||||
port: 8888,
|
||||
username: "bob",
|
||||
password: "p4ss",
|
||||
family: "auto",
|
||||
region: null,
|
||||
notes: null,
|
||||
status: "active",
|
||||
source: "manual",
|
||||
subscriptionId: null,
|
||||
createdAt: "2026-01-01",
|
||||
updatedAt: "2026-01-01",
|
||||
};
|
||||
const url = proxyConfigToUrl(proxyRecord);
|
||||
assert.ok(url, "proxyConfigToUrl must accept ProxyRegistryRecord-shaped objects");
|
||||
assert.match(url!, /bob:p4ss/, "URL must include credentials from the record");
|
||||
});
|
||||
|
||||
test("#9140 VS Code listing must accept built-in auto routing entries", () => {
|
||||
assert.equal(
|
||||
isUsableChatModel({ id: "auto/best-coding", owned_by: "combo" }),
|
||||
true,
|
||||
"built-in auto/* model should be accepted"
|
||||
);
|
||||
assert.equal(
|
||||
isUsableChatModel({ id: "operator-combo", owned_by: "combo" }),
|
||||
false,
|
||||
"operator-created combo should still be rejected"
|
||||
);
|
||||
test("#8853 proxyConfigToUrl returns null for partial config (no host)", () => {
|
||||
const url = proxyConfigToUrl({ type: "http", port: 8080 } as Record<string, unknown>);
|
||||
assert.equal(url, null, "proxyConfigToUrl must return null for partial config without host");
|
||||
});
|
||||
Reference in New Issue
Block a user