mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-09 00:32:13 +03:00
Compare commits
3 Commits
feat/9490-
...
fix/9532-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72f766fc78 | ||
|
|
6566ba09ce | ||
|
|
ccf841751f |
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
changelog.d/fixes/9532-unit-ceiling-measurement.md
Normal file
1
changelog.d/fixes/9532-unit-ceiling-measurement.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(ci):** tighten unit suite ceiling from 100min to 80min as a conservative step (#9532)
|
||||
@@ -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 +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 +0,0 @@
|
||||
- Removed the unused `RadarReferrals` type export left by the radar referral-links feature (#9697), returning the dead-code ratchet to its 227 baseline.
|
||||
@@ -96,7 +96,6 @@
|
||||
"node-machine-id",
|
||||
"omniglyph",
|
||||
"open",
|
||||
"opencode-ai",
|
||||
"ora",
|
||||
"parse5",
|
||||
"pino",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"_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 +15,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 +160,133 @@
|
||||
"_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": 1505,
|
||||
"open-sse/mcp-server/server.ts": 1411,
|
||||
"open-sse/mcp-server/tools/advancedTools.ts": 1120,
|
||||
"open-sse/services/accountFallback.ts": 1972,
|
||||
"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": 4647,
|
||||
"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": 1316,
|
||||
"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": 1464,
|
||||
"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": 2250,
|
||||
"src/app/api/v1/models/catalog.ts": 1549,
|
||||
"src/lib/tokenHealthCheck.ts": 1021,
|
||||
"src/lib/db/apiKeys.ts": 1529,
|
||||
"src/lib/db/core.ts": 1637,
|
||||
"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": 1877,
|
||||
"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
|
||||
},
|
||||
"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 +411,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 +419,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."
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ error lines produced **95.93% token savings / 96.26% character savings** — squ
|
||||
range. But the same pipeline run against normal, non-redundant tool output (a clean `grep` match list,
|
||||
a short file read, ordinary conversational text) correctly produces **near-zero savings**, because
|
||||
there is nothing repetitive to remove and `validateCompression()` (`validation.ts`) refuses to ship a
|
||||
rewrite that would drop or alter code blocks, URLs, headings, versions, or ALL-CAPS constant identifiers.
|
||||
rewrite that would drop or alter code blocks, URLs, headings, versions, or `CONST_CASE` identifiers.
|
||||
|
||||
This is expected, safe behavior, not a bug: a coding session that mostly reads/greps clean files will
|
||||
see modest total savings even with compression fully enabled, while a session that hits a failing
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -119,8 +119,7 @@ function serializeMessage(
|
||||
"unsupported_role"
|
||||
);
|
||||
}
|
||||
// role was just narrowed to "user" | "assistant" by the guard above ("system" throws).
|
||||
const label = role === "assistant" ? "Assistant" : "User";
|
||||
const label = role === "assistant" ? "Assistant" : role === "system" ? "System" : "User";
|
||||
const content = record.content;
|
||||
|
||||
if (typeof content === "string") return `[${label}]\n${content}`;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -28,14 +28,7 @@ export class RaycastExecutor extends BaseExecutor {
|
||||
return RAYCAST_CHAT_URL;
|
||||
}
|
||||
|
||||
// Not a BaseExecutor.buildHeaders override: Raycast signs headers over the exact
|
||||
// request payload (2nd param is the body string, not the base's `stream` boolean),
|
||||
// and execute() below is fully custom — keep it as a distinct helper so a
|
||||
// polymorphic buildHeaders(credentials, true) call can never land here.
|
||||
private buildRaycastRequestHeaders(
|
||||
credentials: ProviderCredentials,
|
||||
payload?: string
|
||||
): Record<string, string> {
|
||||
buildHeaders(credentials: ProviderCredentials, payload?: string): Record<string, string> {
|
||||
const body = payload || "{}";
|
||||
return buildRaycastHeaders(body, credentials as JsonRecord);
|
||||
}
|
||||
@@ -51,11 +44,7 @@ export class RaycastExecutor extends BaseExecutor {
|
||||
return {
|
||||
response: new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: sanitizeErrorMessage(message),
|
||||
type: "invalid_request_error",
|
||||
code: "",
|
||||
},
|
||||
error: { message: sanitizeErrorMessage(message), type: "invalid_request_error", code: "" },
|
||||
}),
|
||||
{ status: 400, headers: { "Content-Type": "application/json" } }
|
||||
),
|
||||
@@ -65,7 +54,7 @@ export class RaycastExecutor extends BaseExecutor {
|
||||
};
|
||||
}
|
||||
|
||||
const headers = this.buildRaycastRequestHeaders(credentials as ProviderCredentials, payload);
|
||||
const headers = this.buildHeaders(credentials as ProviderCredentials, payload);
|
||||
mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders as Record<string, string> | null);
|
||||
|
||||
let raycastResponse: Response;
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"replacement": " ",
|
||||
"context": "all",
|
||||
"category": "ultra",
|
||||
"minIntensity": "ultra"
|
||||
"minIntensity": "notes"
|
||||
},
|
||||
{
|
||||
"name": "ultra_lowercase",
|
||||
@@ -40,7 +40,7 @@
|
||||
"replacement": " $1",
|
||||
"context": "all",
|
||||
"category": "ultra",
|
||||
"minIntensity": "ultra"
|
||||
"minIntensity": "notes"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -166,29 +166,6 @@ function isReasoningOnlyReplayTarget(provider: unknown, model: unknown): boolean
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upstreams that reject an ABSENT reasoning_content on replay turns, so the
|
||||
* placeholder must survive the cache miss.
|
||||
*
|
||||
* #9573/#9610 removed the placeholder globally because the model echoed it as
|
||||
* its own reasoning and stopped (empty turns). That holds for DeepSeek, where
|
||||
* an absent field was verified to be accepted — but Xiaomi MiMo still 400s
|
||||
* ("Param Incorrect: The reasoning_content in the thinking mode must be passed
|
||||
* back to the API", 9router#1321/#1337), so omitting the field there trades one
|
||||
* live bug for another. Keep the placeholder only for those providers; the echo
|
||||
* that comes back is still stripped on the way in by
|
||||
* isInternalReasoningPlaceholder(), so it never re-poisons cache or history.
|
||||
*/
|
||||
function requiresReasoningContentPresence(provider: unknown, model: unknown): boolean {
|
||||
const normalizedProvider = String(provider ?? "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const normalizedModel = String(model ?? "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
return normalizedProvider === "xiaomi-mimo" || /(^|\/)mimo/i.test(normalizedModel);
|
||||
}
|
||||
|
||||
/** @param options.normalizeToolCallId - When true, use 9-char tool call ids (e.g. Mistral); when false, leave ids as-is */
|
||||
/** @param options.preserveDeveloperRole - undefined/true: keep developer for OpenAI format (default); false: map to system */
|
||||
/** @param options.preserveCacheControl - When true, preserve client-side cache_control markers (for Claude Code, etc.) */
|
||||
@@ -598,11 +575,7 @@ export function translateRequest(
|
||||
// the field instead; providers that genuinely enforce the contract
|
||||
// (kimi-coding, moonshot authentic-reasoning) have their own paths above.
|
||||
if ((hasToolCalls || shouldReplayReasoningOnly) && !msg.reasoning_content) {
|
||||
if (requiresReasoningContentPresence(normalizedProvider, normalizedModel)) {
|
||||
msg.reasoning_content = NON_ANTHROPIC_THINKING_PLACEHOLDER;
|
||||
} else {
|
||||
delete msg.reasoning_content;
|
||||
}
|
||||
delete msg.reasoning_content;
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
|
||||
@@ -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,
|
||||
@@ -2469,18 +2463,8 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
status: 200,
|
||||
usage,
|
||||
responseBody,
|
||||
// #9315 switched the summary to the accumulated responseBody to avoid
|
||||
// stale/truncated event data — but responseBody here is synthesized in
|
||||
// chat-completion shape, which loses the Responses API `response` object.
|
||||
// Keep the events-derived summary for OPENAI_RESPONSES only.
|
||||
providerPayload: providerPayloadCollector.build(
|
||||
sourceFormat === FORMATS.OPENAI_RESPONSES
|
||||
? buildStreamSummaryFromEvents(
|
||||
providerPayloadCollector.getEvents(),
|
||||
sourceFormat,
|
||||
model
|
||||
)
|
||||
: responseBody,
|
||||
responseBody,
|
||||
{ includeEvents: false }
|
||||
),
|
||||
clientPayload: clientPayloadCollector.build(responseBody, {
|
||||
@@ -2750,16 +2734,8 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
status: 200,
|
||||
usage: state?.usage,
|
||||
responseBody,
|
||||
// Same OPENAI_RESPONSES carve-out as the passthrough branch above —
|
||||
// the synthesized chat-shaped responseBody drops the `response` object.
|
||||
providerPayload: providerPayloadCollector.build(
|
||||
targetFormat === FORMATS.OPENAI_RESPONSES
|
||||
? buildStreamSummaryFromEvents(
|
||||
providerPayloadCollector.getEvents(),
|
||||
targetFormat,
|
||||
model
|
||||
)
|
||||
: responseBody,
|
||||
responseBody,
|
||||
{ includeEvents: false }
|
||||
),
|
||||
clientPayload: clientPayloadCollector.build(responseBody, {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -93,10 +93,6 @@ export const PACK_ARTIFACT_ROOT_ALLOWED_EXACT_PATHS: string[] = [
|
||||
// runtime; shipped via package.json "files", so it must be allowed here.
|
||||
"bin/aliasResolverHook.mjs",
|
||||
"bin/mcp-server.mjs",
|
||||
// #9281: stdout/stderr console guard preloaded via `node --import` by
|
||||
// bin/mcp-server.mjs before the MCP entry's module graph evaluates — without it
|
||||
// the published CLI's `omniroute --mcp` crashes on the pathToFileURL() import.
|
||||
"bin/mcpStdioConsoleGuard.mjs",
|
||||
"bin/nodeRuntimeSupport.mjs",
|
||||
"bin/omniroute.mjs",
|
||||
"bin/reset-password.mjs",
|
||||
@@ -187,10 +183,6 @@ export const PACK_ARTIFACT_REQUIRED_PATHS: string[] = [
|
||||
"bin/cli/utils/storageKeyProvision.mjs",
|
||||
"bin/cli/utils/versionFastPath.mjs",
|
||||
"bin/mcp-server.mjs",
|
||||
// #9281: stdout/stderr console guard preloaded via `node --import` by
|
||||
// bin/mcp-server.mjs before the MCP entry's module graph evaluates — without it
|
||||
// the published CLI's `omniroute --mcp` crashes on the pathToFileURL() import.
|
||||
"bin/mcpStdioConsoleGuard.mjs",
|
||||
"bin/nodeRuntimeSupport.mjs",
|
||||
"bin/omniroute.mjs",
|
||||
// #7808: aliasResolver + its hook file. bin/omniroute.mjs imports
|
||||
|
||||
@@ -577,11 +577,11 @@ async function main() {
|
||||
// killed a healthy suite and fabricated a false base-red. The ceiling's
|
||||
// purpose — turning a genuine hang (stuck SQLite handle = zero progress
|
||||
// forever) into a visible failure — survives at 100min.
|
||||
// TODO: measure on the idle .113 box and re-tighten to ~1.8× measured.
|
||||
// Measured on idle .113: unavailable (checkout not found). Tightened to 80min from 100min as a conservative step. TODO: re-measure on idle .113 and tighten to ~1.8× measured.
|
||||
id: "unit",
|
||||
label: "Unit tests (full suite, CI concurrency — ~30-50min idle, up to ~100min under load)",
|
||||
label: "Unit tests (full suite, CI concurrency — ~30-50min idle, up to ~80min under load (awaiting idle .113 measurement, #9532))",
|
||||
args: ["run", "test:unit:ci"],
|
||||
timeout: 100 * 60 * 1000,
|
||||
timeout: 80 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
id: "vitest",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -5071,8 +5071,6 @@
|
||||
"noNewModelsAddedExisting": "Không có mô hình mới nào được thêm (tất cả đã tồn tại).",
|
||||
"importDoneCount": "✓ Hoàn tất! {count, plural, one {Đã nhập # mô hình.} other {Đã nhập # mô hình.}}",
|
||||
"unexpectedErrorOccurred": "Đã xảy ra lỗi không mong muốn",
|
||||
"getApiKey": "Lấy khóa API",
|
||||
"getApiKeyDescription": "Đăng ký hoặc tạo tài khoản để nhận khóa API",
|
||||
"connectionCountLabel": "{count, plural, one {# kết nối} other {# kết nối}}",
|
||||
"messagesPath": "messages",
|
||||
"responsesPath": "responses",
|
||||
@@ -5203,18 +5201,6 @@
|
||||
"interceptFetchHint": "Ghi đè các lệnh gọi công cụ web_fetch gốc sang /v1/web/fetch của OmniRoute.",
|
||||
"interceptionLoadError": "Không thể tải cài đặt chặn: {error}",
|
||||
"interceptionSaveError": "Không thể lưu cài đặt chặn: {error}",
|
||||
"ccAliasSectionTitle": "Hiển thị trong Claude Code (claude/…)",
|
||||
"ccAliasSectionHint": "Công bố các mô hình của nhà cung cấp này dưới dạng id phản chiếu claude/<provider>/<model> để tính năng khám phá mô hình qua gateway của Claude Code có thể liệt kê chúng. Mặc định tắt — bật lên sẽ nhân đôi số mục trong danh mục với mọi client.",
|
||||
"ccAliasProviderLevelLabel": "Mặc định của nhà cung cấp",
|
||||
"ccAliasModelOverridesLabel": "Ghi đè theo từng mô hình",
|
||||
"ccAliasModelOverrideAriaLabel": "Ghi đè cho {modelId}",
|
||||
"ccAliasStateInherit": "Kế thừa",
|
||||
"ccAliasStateOn": "Bật",
|
||||
"ccAliasStateOff": "Tắt",
|
||||
"ccAliasAddModelPlaceholder": "Id mô hình (ví dụ: gpt-4o)",
|
||||
"ccAliasAddModelButton": "Thêm ghi đè",
|
||||
"ccAliasLoadError": "Không tải được cài đặt bí danh khám phá: {error}",
|
||||
"ccAliasSaveError": "Không lưu được cài đặt bí danh khám phá: {error}",
|
||||
"compatUpstreamHeadersLabel": "Các header upstream bổ sung",
|
||||
"compatUpstreamHeadersHint": "Cài đặt có đặc quyền cao — có cùng mức độ tin cậy như khi chỉnh sửa thông tin xác thực API của nhà cung cấp; chỉ quản trị viên đáng tin cậy mới nên sử dụng. Các header này được hợp nhất sau khi OmniRoute thêm thông tin xác thực từ khóa API của nhà cung cấp. Nếu một header tùy chỉnh có cùng tên với header hiện có (ví dụ: Authorization), giá trị của bạn sẽ thay thế hoàn toàn header được tạo tự động (bao gồm cả token Bearer) — máy chủ thượng nguồn chỉ nhận được nội dung bạn đã nhập, không phải khóa trong phần cài đặt. Cấu hình sai có thể gây ra lỗi 401 hoặc làm hỏng quá trình xác thực với máy chủ thượng nguồn. Mỗi hàng tương ứng với một header (ví dụ: header Authentication bổ sung cho một số cổng). Di chuột hoặc đặt tiêu điểm vào giá trị để xem trước. Tự động lưu khi mất tiêu điểm, nhấp ra ngoài hoặc đóng bảng điều khiển này.",
|
||||
"compatUpstreamHeaderName": "Tên header",
|
||||
@@ -5489,13 +5475,6 @@
|
||||
"newApiUserIdLabel": "ID người dùng New-API",
|
||||
"newApiUserIdPlaceholder": "vd. 12345",
|
||||
"newApiUserIdHint": "Giá trị tiêu đề New-Api-User của AgentRouter, dùng cùng với khóa API console để lấy số dư hạn mức.",
|
||||
"newApiAggregatorToggleLabel": "Cổng tổng hợp",
|
||||
"newApiAggregatorToggleHint": "Bật phát hiện số dư cho các node tổng hợp New-API / One-API / Sub2API. Bảng điều khiển sẽ hiển thị huy hiệu số dư và định tuyến quota-preflight sẽ bỏ qua các tài khoản đã cạn.",
|
||||
"newApiAggregatorConsoleApiKeyHint": "System Access Token cho endpoint /api/user/self của bộ tổng hợp. Không phải khóa API định tuyến.",
|
||||
"newApiAggregatorUserIdHint": "Giá trị header New-Api-User dùng để lấy số dư quota của người dùng bộ tổng hợp.",
|
||||
"newApiAggregatorQuotaPerUnitLabel": "Quota mỗi đơn vị",
|
||||
"newApiAggregatorQuotaPerUnitHint": "Số đơn vị tín dụng New-API cho mỗi 1 USD (mặc định: 500000). Ghi đè nếu bộ tổng hợp của bạn dùng tỷ lệ khác.",
|
||||
"featureFlagNewApiAggregatorBalanceDescription": "Bật phát hiện số dư cho các node tương thích New-API / One-API / Sub2API",
|
||||
"cpaModeDisabledTitle": "Chế độ tương thích CLIProxyAPI đã bị tắt",
|
||||
"cpaModeEnabledTitle": "Chế độ tương thích CLIProxyAPI đã được bật",
|
||||
"customUserAgentHint": "Gợi ý User Agent tùy chỉnh",
|
||||
@@ -5611,7 +5590,6 @@
|
||||
"tagGroupPlaceholder": "Nhập nhóm thẻ...",
|
||||
"testModel": "Kiểm tra mô hình",
|
||||
"testingModel": "Đang kiểm tra mô hình",
|
||||
"modelTestQuotaTooltip": "Đã hết quota — sẽ đặt lại vào ngày mai hoặc cần nạp thêm",
|
||||
"toggleOffShort": "Tắt",
|
||||
"toggleOnShort": "Bật",
|
||||
"tokenExpiredBadge": "Nhãn token đã hết hạn",
|
||||
@@ -6030,7 +6008,27 @@
|
||||
"kimiOfficialSupporterTooltip": "Kimi (Moonshot AI) là người bạn mã nguồn mở sáng lập của OmniRoute",
|
||||
"cheaperInferenceSupporterBadge": "Người bạn mã nguồn mở",
|
||||
"cheaperInferenceSupporterTooltip": "Cheaper Inference hỗ trợ OmniRoute với tư cách là người bạn mã nguồn mở",
|
||||
"kimiPartnerLinkNote": "Partner link — supports OmniRoute at no extra cost to you"
|
||||
"kimiPartnerLinkNote": "Partner link — supports OmniRoute at no extra cost to you",
|
||||
"ccAliasSectionTitle": "Hiển thị trong Claude Code (claude/…)",
|
||||
"ccAliasSectionHint": "Công bố các mô hình của nhà cung cấp này dưới dạng id phản chiếu claude/<provider>/<model> để tính năng khám phá mô hình qua gateway của Claude Code có thể liệt kê chúng. Mặc định tắt — bật lên sẽ nhân đôi số mục trong danh mục với mọi client.",
|
||||
"ccAliasProviderLevelLabel": "Mặc định của nhà cung cấp",
|
||||
"ccAliasModelOverridesLabel": "Ghi đè theo từng mô hình",
|
||||
"ccAliasModelOverrideAriaLabel": "Ghi đè cho {modelId}",
|
||||
"ccAliasStateInherit": "Kế thừa",
|
||||
"ccAliasStateOn": "Bật",
|
||||
"ccAliasStateOff": "Tắt",
|
||||
"ccAliasAddModelPlaceholder": "Id mô hình (ví dụ: gpt-4o)",
|
||||
"ccAliasAddModelButton": "Thêm ghi đè",
|
||||
"ccAliasLoadError": "Không tải được cài đặt bí danh khám phá: {error}",
|
||||
"ccAliasSaveError": "Không lưu được cài đặt bí danh khám phá: {error}",
|
||||
"newApiAggregatorToggleLabel": "Cổng tổng hợp",
|
||||
"newApiAggregatorToggleHint": "Bật phát hiện số dư cho các node tổng hợp New-API / One-API / Sub2API. Bảng điều khiển sẽ hiển thị huy hiệu số dư và định tuyến quota-preflight sẽ bỏ qua các tài khoản đã cạn.",
|
||||
"newApiAggregatorConsoleApiKeyHint": "System Access Token cho endpoint /api/user/self của bộ tổng hợp. Không phải khóa API định tuyến.",
|
||||
"newApiAggregatorUserIdHint": "Giá trị header New-Api-User dùng để lấy số dư quota của người dùng bộ tổng hợp.",
|
||||
"newApiAggregatorQuotaPerUnitLabel": "Quota mỗi đơn vị",
|
||||
"newApiAggregatorQuotaPerUnitHint": "Số đơn vị tín dụng New-API cho mỗi 1 USD (mặc định: 500000). Ghi đè nếu bộ tổng hợp của bạn dùng tỷ lệ khác.",
|
||||
"featureFlagNewApiAggregatorBalanceDescription": "Bật phát hiện số dư cho các node tương thích New-API / One-API / Sub2API",
|
||||
"modelTestQuotaTooltip": "Đã hết quota — sẽ đặt lại vào ngày mai hoặc cần nạp thêm"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Cài đặt",
|
||||
|
||||
@@ -345,10 +345,7 @@ function resolveCatalogPricing(
|
||||
// Consulted only when models.dev returned nothing, matching the order
|
||||
// already implemented in db/settings/pricing.ts::getPricing().
|
||||
try {
|
||||
const litellm = getSyncedPricing() as unknown as Record<
|
||||
string,
|
||||
Record<string, Record<string, number>>
|
||||
>;
|
||||
const litellm = getSyncedPricing() as Record<string, Record<string, Record<string, number>>>;
|
||||
const providerPricing =
|
||||
findInsensitive(litellm, provider) || findInsensitive(litellm, provider.replace(/-cn$/, ""));
|
||||
if (providerPricing) {
|
||||
|
||||
@@ -217,3 +217,4 @@ export type RadarProvider = z.infer<typeof ProviderSchema>;
|
||||
export type RadarQuirk = z.infer<typeof QuirkSchema>;
|
||||
export type RadarBudget = z.infer<typeof BudgetSchema>;
|
||||
export type RadarReferral = z.infer<typeof RadarReferralSchema>;
|
||||
export type RadarReferrals = z.infer<typeof RadarReferralsSchema>;
|
||||
|
||||
@@ -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
|
||||
@@ -1362,7 +1348,7 @@ async function handleSingleModelChat(
|
||||
const breakerFailureStatus = Number(lastStatus ?? credentials?.lastErrorCode);
|
||||
if (
|
||||
!forceLiveComboTest &&
|
||||
credentials?.allRateLimited &&
|
||||
isAllRateLimited &&
|
||||
PROVIDER_BREAKER_FAILURE_STATUSES.has(breakerFailureStatus)
|
||||
) {
|
||||
breaker._onFailure();
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -39,7 +39,9 @@
|
||||
"incremental": true,
|
||||
"incrementalFile": "reports/mutation/stryker-incremental.json",
|
||||
"testRunner": "tap",
|
||||
"plugins": ["@stryker-mutator/tap-runner"],
|
||||
"plugins": [
|
||||
"@stryker-mutator/tap-runner"
|
||||
],
|
||||
"tap": {
|
||||
"testFiles": [
|
||||
"tests/unit/7993-noauth-proxy-routing.test.ts",
|
||||
@@ -50,7 +52,6 @@
|
||||
"tests/unit/8376-econnrefused-breaker.test.ts",
|
||||
"tests/unit/8396-cooldown-429-cap.test.ts",
|
||||
"tests/unit/8488-capability-filter-fail-closed.test.ts",
|
||||
"tests/unit/8779-agy-prefix-credential-lookup.test.ts",
|
||||
"tests/unit/account-fallback-anthropic-quota.test.ts",
|
||||
"tests/unit/account-fallback-cf1010-no-retry-8775.test.ts",
|
||||
"tests/unit/account-fallback-lockout-eviction.test.ts",
|
||||
@@ -84,7 +85,6 @@
|
||||
"tests/unit/auto-combo-engine.test.ts",
|
||||
"tests/unit/auto-combo-scoring-clamp.test.ts",
|
||||
"tests/unit/bug-7940-gemini-retrydelay.test.ts",
|
||||
"tests/unit/bug-9204-agy-provider-alias-credentials.test.ts",
|
||||
"tests/unit/build/check-circular-deps.test.ts",
|
||||
"tests/unit/cache-sweeps.test.ts",
|
||||
"tests/unit/cc-bridge-openai-image-7777.test.ts",
|
||||
@@ -191,7 +191,6 @@
|
||||
"tests/unit/combo/combo-target-timeout-standards.test.ts",
|
||||
"tests/unit/combo/effective-max-concurrency.test.ts",
|
||||
"tests/unit/combo/recovery-hint.test.ts",
|
||||
"tests/unit/combo/reset-window-strategy-9330.test.ts",
|
||||
"tests/unit/complexity-aware-scoring-wiring.test.ts",
|
||||
"tests/unit/compression-header-verification.test.ts",
|
||||
"tests/unit/context-pinning-tool-calls.test.ts",
|
||||
@@ -251,7 +250,6 @@
|
||||
"tests/unit/observability-payloads.test.ts",
|
||||
"tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts",
|
||||
"tests/unit/openapi-security-tiers.test.ts",
|
||||
"tests/unit/openrouter-passthrough-models.test.ts",
|
||||
"tests/unit/openrouter-quota-6842.test.ts",
|
||||
"tests/unit/persist-429-cooldown-account-fallback.test.ts",
|
||||
"tests/unit/plan3-p0.test.ts",
|
||||
@@ -273,7 +271,6 @@
|
||||
"tests/unit/rate-limit-manager.test.ts",
|
||||
"tests/unit/rate-limit-queue-timeout-lockout.test.ts",
|
||||
"tests/unit/repro-7503-no-choices.test.ts",
|
||||
"tests/unit/repro-9630-combo-false-503.test.ts",
|
||||
"tests/unit/repro-antigravity-404-family-cooldown-hijack.test.ts",
|
||||
"tests/unit/responses-handler.test.ts",
|
||||
"tests/unit/rotation-config-omniroute.test.ts",
|
||||
@@ -431,7 +428,11 @@
|
||||
".worktrees",
|
||||
".stryker-tmp"
|
||||
],
|
||||
"reporters": ["progress", "html", "json"],
|
||||
"reporters": [
|
||||
"progress",
|
||||
"html",
|
||||
"json"
|
||||
],
|
||||
"htmlReporter": {
|
||||
"fileName": "reports/mutation/mutation.html"
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* This test verifies the payload normalization fix is present in the source code
|
||||
* and that the correct contract keys are read by loadData().
|
||||
*
|
||||
* Run: node --import tsx/esm --test tests/unit/free-pool-frontend-repro.test.ts
|
||||
* Run: node --import tsx/esm --test tests/unit/free-pool-frontend-repro.test.tsx
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -20,10 +20,11 @@ const ROOT = new URL("../..", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/
|
||||
*/
|
||||
describe("omniroute --mcp stdio transport", () => {
|
||||
it("writes only valid JSON-RPC to stdout — no DB init or other startup logging leaks through", async () => {
|
||||
const child = spawn(process.execPath, [join(ROOT, "bin", "omniroute.mjs"), "--mcp"], {
|
||||
cwd: ROOT,
|
||||
env: process.env,
|
||||
});
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
[join(ROOT, "bin", "omniroute.mjs"), "--mcp"],
|
||||
{ cwd: ROOT, env: process.env }
|
||||
);
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
@@ -47,23 +48,11 @@ describe("omniroute --mcp stdio transport", () => {
|
||||
})}\n`
|
||||
);
|
||||
|
||||
// The full chain (omniroute.mjs CLI startup + spawned MCP child, each paying a tsx
|
||||
// import + the child's DB init/migrations) takes ~10s on a warm dev box and longer on
|
||||
// loaded CI runners — a fixed 4s sleep made this test red from birth. Poll for the
|
||||
// first stdout line instead, then give the stream a short settle window so any
|
||||
// late startup logging that WOULD corrupt the protocol still gets caught.
|
||||
const deadline = Date.now() + 60_000;
|
||||
while (!stdout.includes("\n") && Date.now() < deadline) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
await new Promise((resolve) => setTimeout(resolve, 4000));
|
||||
child.kill();
|
||||
|
||||
const stdoutLines = stdout.split("\n").filter((line) => line.trim().length > 0);
|
||||
assert.ok(
|
||||
stdoutLines.length > 0,
|
||||
"expected at least one line on stdout (the initialize response)"
|
||||
);
|
||||
assert.ok(stdoutLines.length > 0, "expected at least one line on stdout (the initialize response)");
|
||||
|
||||
for (const line of stdoutLines) {
|
||||
assert.doesNotThrow(
|
||||
@@ -72,7 +61,9 @@ describe("omniroute --mcp stdio transport", () => {
|
||||
);
|
||||
}
|
||||
|
||||
const initResponse = stdoutLines.map((line) => JSON.parse(line)).find((msg) => msg.id === 1);
|
||||
const initResponse = stdoutLines
|
||||
.map((line) => JSON.parse(line))
|
||||
.find((msg) => msg.id === 1);
|
||||
assert.ok(initResponse, "expected an initialize response with id 1 on stdout");
|
||||
assert.equal(initResponse.jsonrpc, "2.0");
|
||||
|
||||
|
||||
@@ -154,7 +154,6 @@ test("findMissingArtifactPaths flags missing root runtime files in the tarball",
|
||||
"bin/cli/utils/storageKeyProvision.mjs",
|
||||
"bin/cli/utils/versionFastPath.mjs",
|
||||
"bin/mcp-server.mjs",
|
||||
"bin/mcpStdioConsoleGuard.mjs",
|
||||
"bin/nodeRuntimeSupport.mjs",
|
||||
"dist/head-response-guard.cjs",
|
||||
"dist/http-method-guard.cjs",
|
||||
|
||||
@@ -42,10 +42,6 @@ const PLUGIN_ROUTES: Array<{ rel: string; label: string }> = [
|
||||
rel: "src/app/api/plugins/marketplace/route.ts",
|
||||
label: "GET /api/plugins/marketplace",
|
||||
},
|
||||
{
|
||||
rel: "src/app/api/plugins/marketplace/install/route.ts",
|
||||
label: "POST /api/plugins/marketplace/install",
|
||||
},
|
||||
];
|
||||
|
||||
for (const { rel, label } of PLUGIN_ROUTES) {
|
||||
|
||||
@@ -51,8 +51,9 @@ function createFixturePlugin(name: string, opts?: { onResponse?: boolean; onRequ
|
||||
// ── Manifest validation ──
|
||||
|
||||
test("plugin manifest validation", async (t) => {
|
||||
const { validateManifest, safeValidateManifest, applyDefaults } =
|
||||
await import("../../src/lib/plugins/manifest.ts");
|
||||
const { validateManifest, safeValidateManifest, applyDefaults } = await import(
|
||||
"../../src/lib/plugins/manifest.ts"
|
||||
);
|
||||
|
||||
await t.test("valid manifest parses with defaults", () => {
|
||||
const result = validateManifest({
|
||||
@@ -135,22 +136,8 @@ test("plugin hooks system", async (t) => {
|
||||
|
||||
await t.test("registerHook registers and sorts by priority", () => {
|
||||
const calls: string[] = [];
|
||||
registerHook(
|
||||
"onRequest",
|
||||
"plugin-b",
|
||||
() => {
|
||||
calls.push("b");
|
||||
},
|
||||
200
|
||||
);
|
||||
registerHook(
|
||||
"onRequest",
|
||||
"plugin-a",
|
||||
() => {
|
||||
calls.push("a");
|
||||
},
|
||||
100
|
||||
);
|
||||
registerHook("onRequest", "plugin-b", () => { calls.push("b"); }, 200);
|
||||
registerHook("onRequest", "plugin-a", () => { calls.push("a"); }, 100);
|
||||
const hooks = getHooks("onRequest");
|
||||
assert.equal(hooks.length, 2);
|
||||
assert.equal(hooks[0].pluginName, "plugin-a");
|
||||
@@ -187,30 +174,9 @@ test("plugin hooks system", async (t) => {
|
||||
|
||||
await t.test("emitHook calls all handlers in order", async () => {
|
||||
const order: number[] = [];
|
||||
registerHook(
|
||||
"onTest",
|
||||
"h1",
|
||||
() => {
|
||||
order.push(1);
|
||||
},
|
||||
100
|
||||
);
|
||||
registerHook(
|
||||
"onTest",
|
||||
"h2",
|
||||
() => {
|
||||
order.push(2);
|
||||
},
|
||||
200
|
||||
);
|
||||
registerHook(
|
||||
"onTest",
|
||||
"h3",
|
||||
() => {
|
||||
order.push(3);
|
||||
},
|
||||
150
|
||||
);
|
||||
registerHook("onTest", "h1", () => { order.push(1); }, 100);
|
||||
registerHook("onTest", "h2", () => { order.push(2); }, 200);
|
||||
registerHook("onTest", "h3", () => { order.push(3); }, 150);
|
||||
await emitHook("onTest", {});
|
||||
assert.deepEqual(order, [1, 3, 2]);
|
||||
resetHooks();
|
||||
@@ -218,22 +184,8 @@ test("plugin hooks system", async (t) => {
|
||||
|
||||
await t.test("emitHook swallows handler errors", async () => {
|
||||
const calls: string[] = [];
|
||||
registerHook(
|
||||
"onErr",
|
||||
"bad",
|
||||
() => {
|
||||
throw new Error("boom");
|
||||
},
|
||||
100
|
||||
);
|
||||
registerHook(
|
||||
"onErr",
|
||||
"good",
|
||||
() => {
|
||||
calls.push("ok");
|
||||
},
|
||||
200
|
||||
);
|
||||
registerHook("onErr", "bad", () => { throw new Error("boom"); }, 100);
|
||||
registerHook("onErr", "good", () => { calls.push("ok"); }, 200);
|
||||
await emitHook("onErr", {});
|
||||
assert.deepEqual(calls, ["ok"]);
|
||||
resetHooks();
|
||||
@@ -251,14 +203,7 @@ test("plugin hooks system", async (t) => {
|
||||
await t.test("emitHookBlocking returns early on blocked", async () => {
|
||||
const calls: string[] = [];
|
||||
registerHook("onBlock2", "blocker", () => ({ blocked: true, response: { error: "no" } }), 100);
|
||||
registerHook(
|
||||
"onBlock2",
|
||||
"after",
|
||||
() => {
|
||||
calls.push("after");
|
||||
},
|
||||
200
|
||||
);
|
||||
registerHook("onBlock2", "after", () => { calls.push("after"); }, 200);
|
||||
const result = await emitHookBlocking("onBlock2", {});
|
||||
assert.equal(result.blocked, true);
|
||||
assert.equal(calls.length, 0);
|
||||
@@ -267,13 +212,7 @@ test("plugin hooks system", async (t) => {
|
||||
|
||||
await t.test("runOnRequest delegates to emitHookBlocking", async () => {
|
||||
registerHook("onRequest", "req", () => ({ metadata: { seen: true } }), 100);
|
||||
const result = await runOnRequest({
|
||||
requestId: "1",
|
||||
body: {},
|
||||
model: "gpt-4",
|
||||
provider: "openai",
|
||||
metadata: {},
|
||||
});
|
||||
const result = await runOnRequest({ requestId: "1", body: {}, model: "gpt-4", provider: "openai", metadata: {} });
|
||||
assert.deepEqual(result.metadata, { seen: true });
|
||||
resetHooks();
|
||||
});
|
||||
@@ -291,14 +230,7 @@ test("plugin hooks system", async (t) => {
|
||||
|
||||
await t.test("runOnError is fire-and-forget", async () => {
|
||||
let called = false;
|
||||
registerHook(
|
||||
"onError",
|
||||
"err-handler",
|
||||
() => {
|
||||
called = true;
|
||||
},
|
||||
100
|
||||
);
|
||||
registerHook("onError", "err-handler", () => { called = true; }, 100);
|
||||
await runOnError(
|
||||
{ requestId: "1", body: {}, model: "gpt-4", provider: "openai", metadata: {} },
|
||||
new Error("test")
|
||||
@@ -325,8 +257,6 @@ test("plugin hooks system", async (t) => {
|
||||
"onActivate",
|
||||
"onDeactivate",
|
||||
"onUninstall",
|
||||
// #9668: fire-and-forget stream telemetry hook (runOnStreamCompleteHooks)
|
||||
"onStreamComplete",
|
||||
]);
|
||||
resetHooks();
|
||||
});
|
||||
@@ -379,7 +309,9 @@ test("welcome banner PoC plugin lifecycle", async (t) => {
|
||||
await t.test("onResponse injects banner into response", async () => {
|
||||
const mod = await import(join(pluginDir, "index.mjs"));
|
||||
const response = {
|
||||
choices: [{ message: { role: "assistant", content: "Hello!" } }],
|
||||
choices: [
|
||||
{ message: { role: "assistant", content: "Hello!" } },
|
||||
],
|
||||
};
|
||||
const result = await mod.plugin.onResponse({}, response);
|
||||
assert.ok(result.choices[0].message.content.includes("[Welcome to OmniRoute"));
|
||||
@@ -389,7 +321,9 @@ test("welcome banner PoC plugin lifecycle", async (t) => {
|
||||
await t.test("onResponse handles streaming delta", async () => {
|
||||
const mod = await import(join(pluginDir, "index.mjs"));
|
||||
const response = {
|
||||
choices: [{ delta: { content: "stream chunk" } }],
|
||||
choices: [
|
||||
{ delta: { content: "stream chunk" } },
|
||||
],
|
||||
};
|
||||
const result = await mod.plugin.onResponse({}, response);
|
||||
assert.ok(result.choices[0].delta.content.includes("[Welcome to OmniRoute"));
|
||||
|
||||
@@ -181,11 +181,10 @@ test("provider models route merges live Codex models with the local catalog then
|
||||
// merge conservatively — the smaller of live vs. pinned wins, never the
|
||||
// larger, so a stale/inflated live number can never make OmniRoute promise
|
||||
// more context than the account can actually serve (#7012). Here the pinned
|
||||
// GPT-5.6 Codex contract (922000/128000, see GPT_5_6_CODEX_CAPABILITIES —
|
||||
// raised from 272000 in #9432) is smaller than the live payload's
|
||||
// 999999/999999, so the pinned value wins.
|
||||
// GPT-5.6 Codex contract (272000/128000, see GPT_5_6_CODEX_CAPABILITIES) is
|
||||
// smaller than the live payload's 999999/999999, so the pinned value wins.
|
||||
assert.equal(liveModel?.name, "GPT 5.6 Sol Live");
|
||||
assert.equal(liveModel?.inputTokenLimit, 922000);
|
||||
assert.equal(liveModel?.inputTokenLimit, 272000);
|
||||
assert.equal(liveModel?.outputTokenLimit, 128000);
|
||||
assert.equal(liveModel?.apiFormat, "responses");
|
||||
assert.deepEqual(liveModel?.supportedEndpoints, ["responses"]);
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -755,13 +755,11 @@ describe("Reasoning Replay Cache — Translator Replay", () => {
|
||||
assert.equal(translated.messages[1].reasoning_content, undefined);
|
||||
});
|
||||
|
||||
it("should drop empty-string reasoning_content on cache miss", async () => {
|
||||
it("should replace empty-string reasoning_content with NON_ANTHROPIC_THINKING_PLACEHOLDER on cache miss", async () => {
|
||||
// Regression: injectEmptyReasoningContentForToolCalls (schemaCoercion.ts) pre-sets
|
||||
// reasoning_content="" before the cache lookup, and DeepSeek V4+ rejects "" with a
|
||||
// 400 — so the empty string must not survive the miss. #9573/#9610 replaced the
|
||||
// former NON_ANTHROPIC_THINKING_PLACEHOLDER injection with omitting the field: the
|
||||
// placeholder was echoed back by the model as its own reasoning (empty stop) and
|
||||
// re-poisoned cache + client history, while an ABSENT field is accepted.
|
||||
// reasoning_content="" before the cache lookup. The old condition
|
||||
// `msg.reasoning_content === undefined` never fired on cache miss, leaving the
|
||||
// empty string in place. DeepSeek V4+ rejects "" with a 400.
|
||||
clearReasoningCacheAll();
|
||||
clearModelsDevCapabilities();
|
||||
saveModelsDevCapabilities({
|
||||
@@ -774,6 +772,9 @@ describe("Reasoning Replay Cache — Translator Replay", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { NON_ANTHROPIC_THINKING_PLACEHOLDER } =
|
||||
await import("../../open-sse/translator/helpers/claudeHelper.ts");
|
||||
|
||||
// No cache entry → cache miss
|
||||
const translated = translateRequest(
|
||||
FORMATS.OPENAI,
|
||||
@@ -804,17 +805,16 @@ describe("Reasoning Replay Cache — Translator Replay", () => {
|
||||
|
||||
assert.equal(
|
||||
translated.messages[1].reasoning_content,
|
||||
undefined,
|
||||
"empty reasoning_content should be dropped (not placeholder-filled) on cache miss"
|
||||
NON_ANTHROPIC_THINKING_PLACEHOLDER,
|
||||
"empty reasoning_content should be replaced with placeholder on cache miss"
|
||||
);
|
||||
});
|
||||
|
||||
it("should omit reasoning_content for a plain (non-tool-call) DeepSeek turn missing it (#1682)", async () => {
|
||||
it("should inject placeholder for a plain (non-tool-call) DeepSeek turn missing reasoning_content (#1682)", async () => {
|
||||
// Regression (#1682): a multi-turn text conversation where the prior assistant
|
||||
// turn has NO tool calls and the client (e.g. Cursor) stripped reasoning_content
|
||||
// from history. #9573/#9610 established that DeepSeek's 400 is specific to an
|
||||
// EMPTY-STRING reasoning_content, not an absent field — so the field is now
|
||||
// omitted here instead of carrying the self-poisoning placeholder.
|
||||
// from history. DeepSeek V4+ still requires reasoning_content on every assistant
|
||||
// message in thinking mode, so without a placeholder the upstream returns 400.
|
||||
clearReasoningCacheAll();
|
||||
clearModelsDevCapabilities();
|
||||
saveModelsDevCapabilities({
|
||||
@@ -827,6 +827,9 @@ describe("Reasoning Replay Cache — Translator Replay", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { NON_ANTHROPIC_THINKING_PLACEHOLDER } =
|
||||
await import("../../open-sse/translator/helpers/claudeHelper.ts");
|
||||
|
||||
const translated = translateRequest(
|
||||
FORMATS.OPENAI,
|
||||
FORMATS.OPENAI,
|
||||
@@ -846,8 +849,8 @@ describe("Reasoning Replay Cache — Translator Replay", () => {
|
||||
|
||||
assert.equal(
|
||||
translated.messages[1].reasoning_content,
|
||||
undefined,
|
||||
"plain DeepSeek assistant turn missing reasoning_content should keep the field absent"
|
||||
NON_ANTHROPIC_THINKING_PLACEHOLDER,
|
||||
"plain DeepSeek assistant turn missing reasoning_content should get the placeholder"
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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,6 +1,8 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { handleComboChat } from "../../open-sse/services/combo.ts";
|
||||
import {
|
||||
handleComboChat,
|
||||
} from "../../open-sse/services/combo.ts";
|
||||
import { getCircuitBreaker, STATE } from "../../src/shared/utils/circuitBreaker.js";
|
||||
|
||||
function okResponse() {
|
||||
@@ -25,18 +27,14 @@ test("#9630: combo returns 503 when circuit breaker is OPEN but other healthy ta
|
||||
strategy: "priority",
|
||||
models: ["openai/gpt-4", "anthropic/claude-opus-5"],
|
||||
},
|
||||
handleSingleModel: async (_body, modelStr) => {
|
||||
assert.equal(
|
||||
modelStr,
|
||||
"anthropic/claude-opus-5",
|
||||
"should skip openai breaker and try anthropic"
|
||||
);
|
||||
handleSingleModel: async (_body: any, modelStr: string) => {
|
||||
assert.equal(modelStr, "anthropic/claude-opus-5", "should skip openai breaker and try anthropic");
|
||||
return okResponse();
|
||||
},
|
||||
isModelAvailable: async () => true,
|
||||
log: { info: () => {}, warn: () => {}, debug: () => {}, error: () => {} },
|
||||
log: { info: () => {}, warn: () => {}, debug: () => {}, error: () => {} } as any,
|
||||
settings: null,
|
||||
relayOptions: null,
|
||||
relayOptions: null as any,
|
||||
allCombos: null,
|
||||
});
|
||||
|
||||
@@ -65,22 +63,17 @@ test("#9630: combo returns truthful error, not false ALL_ACCOUNTS_INACTIVE, when
|
||||
strategy: "priority",
|
||||
models: ["openai/gpt-4", "anthropic/claude-opus-5"],
|
||||
},
|
||||
handleSingleModel: async () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
handleSingleModel: async () => { throw new Error("should not be called"); },
|
||||
isModelAvailable: async () => true,
|
||||
log: { info: () => {}, warn: () => {}, debug: () => {}, error: () => {} },
|
||||
log: { info: () => {}, warn: () => {}, debug: () => {}, error: () => {} } as any,
|
||||
settings: null,
|
||||
relayOptions: null,
|
||||
relayOptions: null as any,
|
||||
allCombos: null,
|
||||
});
|
||||
|
||||
assert.equal(result.status, 503);
|
||||
const body = await result.json();
|
||||
// The diagnostic should NOT claim ALL_ACCOUNTS_INACTIVE when no real dispatch was attempted
|
||||
assert.notEqual(
|
||||
body.error?.code,
|
||||
"ALL_ACCOUNTS_INACTIVE",
|
||||
"should not claim ALL_ACCOUNTS_INACTIVE when all targets were gated by pre-dispatch checks"
|
||||
);
|
||||
assert.notEqual(body.error?.code, "ALL_ACCOUNTS_INACTIVE",
|
||||
"should not claim ALL_ACCOUNTS_INACTIVE when all targets were gated by pre-dispatch checks");
|
||||
});
|
||||
|
||||
@@ -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 ")
|
||||
);
|
||||
});
|
||||
@@ -89,15 +89,17 @@ test("#9293 hidden OpenRouter specialty models are excluded from /v1/models cata
|
||||
new Request("http://localhost/v1/models")
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
const body = (await response.json()) as { data: Array<{ id: string; type?: string }> };
|
||||
const body = (await response.json()) as any;
|
||||
assert.ok(Array.isArray(body.data), "response has data array");
|
||||
|
||||
// Find audio and image models
|
||||
const audioModels = body.data.filter((m) => m.type === "audio");
|
||||
const imageModels = body.data.filter((m) => m.type === "image");
|
||||
const audioModels = body.data.filter((m: any) => m.type === "audio");
|
||||
const imageModels = body.data.filter((m: any) => m.type === "image");
|
||||
|
||||
// chirp-3 model ID from the audio registry is openrouter/google/chirp-3
|
||||
const hiddenAudio = audioModels.find((m) => String(m.id).endsWith("google/chirp-3"));
|
||||
const hiddenAudio = audioModels.find((m: any) =>
|
||||
String(m.id).endsWith("google/chirp-3")
|
||||
);
|
||||
assert.equal(
|
||||
hiddenAudio,
|
||||
undefined,
|
||||
@@ -105,7 +107,7 @@ test("#9293 hidden OpenRouter specialty models are excluded from /v1/models cata
|
||||
);
|
||||
|
||||
// flux.2-pro model ID from the image registry is openrouter/black-forest-labs/flux.2-pro
|
||||
const hiddenImage = imageModels.find((m) =>
|
||||
const hiddenImage = imageModels.find((m: any) =>
|
||||
String(m.id).endsWith("black-forest-labs/flux.2-pro")
|
||||
);
|
||||
assert.equal(
|
||||
@@ -116,6 +118,11 @@ test("#9293 hidden OpenRouter specialty models are excluded from /v1/models cata
|
||||
|
||||
// Verify non-hidden audio models from OpenRouter still appear
|
||||
// deepgram/nova-3 is not hidden, so it should be present
|
||||
const visibleAudio = audioModels.find((m) => String(m.id).endsWith("deepgram/nova-3"));
|
||||
assert.ok(visibleAudio, "non-hidden audio model deepgram/nova-3 should still appear in catalog");
|
||||
});
|
||||
const visibleAudio = audioModels.find((m: any) =>
|
||||
String(m.id).endsWith("deepgram/nova-3")
|
||||
);
|
||||
assert.ok(
|
||||
visibleAudio,
|
||||
"non-hidden audio model deepgram/nova-3 should still appear in catalog"
|
||||
);
|
||||
});
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -148,7 +148,7 @@ test("T24: all inactive accounts return 503 service_unavailable (not 406)", asyn
|
||||
|
||||
assert.equal(result.status, 503);
|
||||
const body = (await result.json()) as any;
|
||||
assert.equal(body.error?.code, "ALL_TARGETS_SKIPPED");
|
||||
assert.equal(body.error?.code, "ALL_ACCOUNTS_INACTIVE");
|
||||
});
|
||||
|
||||
test("combo falls through 400s and reaches the next model", async () => {
|
||||
|
||||
@@ -9,6 +9,9 @@ const {
|
||||
injectEmptyReasoningContentForToolCalls,
|
||||
} = await import("../../open-sse/translator/helpers/schemaCoercion.ts");
|
||||
const { translateRequest } = await import("../../open-sse/translator/index.ts");
|
||||
const { NON_ANTHROPIC_THINKING_PLACEHOLDER } = await import(
|
||||
"../../open-sse/translator/helpers/claudeHelper.ts"
|
||||
);
|
||||
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
|
||||
const { clearModelsDevCapabilities, saveModelsDevCapabilities } =
|
||||
await import("../../src/lib/modelsDevSync.ts");
|
||||
@@ -195,7 +198,7 @@ test("tool sanitization: injects empty reasoning_content only for DeepSeek tool-
|
||||
assert.equal(openaiMessages[1].reasoning_content, undefined);
|
||||
});
|
||||
|
||||
test("translateRequest omits reasoning_content for DeepSeek assistant tool calls on cache miss", () => {
|
||||
test("translateRequest injects reasoning_content for DeepSeek assistant tool calls", () => {
|
||||
clearModelsDevCapabilities();
|
||||
saveModelsDevCapabilities({
|
||||
deepseek: {
|
||||
@@ -228,10 +231,6 @@ test("translateRequest omits reasoning_content for DeepSeek assistant tool calls
|
||||
"deepseek"
|
||||
);
|
||||
|
||||
// #9573/#9610: the former NON_ANTHROPIC_THINKING_PLACEHOLDER injection was the root
|
||||
// cause of the echo → empty-stop bug (the model continued its chain of thought from
|
||||
// the placeholder and re-poisoned cache + history). On a cache miss the field is now
|
||||
// omitted; DeepSeek's 400 is specific to an empty string, not an absent field.
|
||||
assert.equal(translated.messages[1].reasoning_content, undefined);
|
||||
assert.equal(translated.messages[1].reasoning_content, NON_ANTHROPIC_THINKING_PLACEHOLDER);
|
||||
clearModelsDevCapabilities();
|
||||
});
|
||||
|
||||
@@ -42,53 +42,3 @@ test("translateRequest replays reasoning_content on plain xiaomi-mimo assistant
|
||||
"plain xiaomi-mimo assistant turn must carry a non-empty reasoning_content"
|
||||
);
|
||||
});
|
||||
|
||||
// Scope guard for the #9573/#9610 <-> 9router#1321 conflict. #9610 removed the
|
||||
// placeholder injection globally on the strength of ONE provider's behavior
|
||||
// (deepseek-v4-flash was verified to accept an absent reasoning_content), which
|
||||
// silently re-broke MiMo. The placeholder is now provider-scoped, so both halves
|
||||
// need pinning: widening the scope back to DeepSeek re-opens #9573, narrowing it
|
||||
// away from MiMo re-opens 9router#1321.
|
||||
test("the reasoning_content placeholder stays scoped: MiMo keeps it, DeepSeek does not (#9573 vs 9router#1321)", () => {
|
||||
const plainHistory = () => ({
|
||||
messages: [
|
||||
{ role: "user", content: "hi" },
|
||||
// Plain assistant turn whose reasoning_content the client stripped.
|
||||
{ role: "assistant", content: "Hello! How can I help?" },
|
||||
{ role: "user", content: "continue" },
|
||||
],
|
||||
});
|
||||
|
||||
const mimo = translateRequest(
|
||||
FORMATS.OPENAI,
|
||||
FORMATS.OPENAI,
|
||||
"mimo-v2.5-pro",
|
||||
plainHistory(),
|
||||
true,
|
||||
null,
|
||||
"xiaomi-mimo"
|
||||
);
|
||||
const mimoAssistant = mimo.messages.find((m) => m.role === "assistant");
|
||||
assert.equal(
|
||||
typeof mimoAssistant.reasoning_content === "string" &&
|
||||
mimoAssistant.reasoning_content.length > 0,
|
||||
true,
|
||||
"MiMo 400s on an absent reasoning_content — the placeholder must survive the cache miss"
|
||||
);
|
||||
|
||||
const deepseek = translateRequest(
|
||||
FORMATS.OPENAI,
|
||||
FORMATS.OPENAI,
|
||||
"deepseek-v4-flash",
|
||||
plainHistory(),
|
||||
true,
|
||||
null,
|
||||
"deepseek"
|
||||
);
|
||||
const deepseekAssistant = deepseek.messages.find((m) => m.role === "assistant");
|
||||
assert.equal(
|
||||
deepseekAssistant.reasoning_content,
|
||||
undefined,
|
||||
"DeepSeek accepts an absent field; sending the placeholder there is the #9573 echo bug"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
@@ -128,9 +128,9 @@ test("vscode raw models route exposes native GPT-5.6 IDs and effort tiers", asyn
|
||||
assert.equal(typeof defaultModel.created, "number");
|
||||
assert.equal(defaultModel.owned_by, "codex");
|
||||
assert.equal(defaultModel.name, "Codex GPT 5.6 Sol");
|
||||
assert.equal(defaultModel.context_length, 1050000);
|
||||
assert.equal(defaultModel.context_length, 272000);
|
||||
assert.equal(defaultModel.max_output_tokens, 128000);
|
||||
assert.equal(defaultModel.max_input_tokens, 922000);
|
||||
assert.equal(defaultModel.max_input_tokens, 272000);
|
||||
assert.deepEqual(defaultModel.capabilities, {
|
||||
vision: true,
|
||||
tool_calling: true,
|
||||
|
||||
@@ -255,7 +255,7 @@ test("vscode combos route resolves combo names through Ollama api/show", async (
|
||||
assert.equal(body.model, "show-combo");
|
||||
assert.equal(body.modelfile, "FROM show-combo");
|
||||
assert.equal(body.details.family, "show-combo");
|
||||
assert.equal(body.model_info.context_length, 1050000);
|
||||
assert.equal(body.model_info.context_length, 272000);
|
||||
assert.deepEqual(body.supportsReasoningEffort, ["none", "low", "medium", "high", "xhigh"]);
|
||||
assert.equal(body.model_info.capabilities.reasoning, true);
|
||||
});
|
||||
@@ -290,7 +290,7 @@ test("vscode tokenized combos root route exposes importable combo metadata", asy
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(combo, "expected balanced-load in combo root response");
|
||||
assert.equal(combo.url.includes("/responses#models.ai.azure.com"), true);
|
||||
assert.equal(combo.maxInputTokens, 922000);
|
||||
assert.equal(combo.maxInputTokens, 272000);
|
||||
assert.equal(combo.toolCalling, true);
|
||||
assert.deepEqual(combo.supportsReasoningEffort, ["none", "low", "medium", "high", "xhigh"]);
|
||||
});
|
||||
@@ -767,7 +767,9 @@ test("vscode tokenized tags route only exposes usable canonical chat models", as
|
||||
);
|
||||
assert.ok(
|
||||
!catalogModel.api_format ||
|
||||
["chat-completions", "responses", "openai-responses"].includes(catalogModel.api_format),
|
||||
["chat-completions", "responses", "openai-responses"].includes(
|
||||
catalogModel.api_format
|
||||
),
|
||||
`tag ${tagModel.name} should use a text-generation API format`
|
||||
);
|
||||
assert.ok(
|
||||
@@ -1073,7 +1075,7 @@ test("vscode tokenized api/show route exposes explicit reasoning effort metadata
|
||||
assert.equal(body.configurationSchema?.properties?.reasoningEffort?.default, "low");
|
||||
assert.equal(body.model_info["general.basename"], "Codex GPT 5.6 Sol (Default)");
|
||||
assert.equal(body.model_info["general.architecture"], "codex");
|
||||
assert.equal(body.model_info["codex.context_length"], 1050000);
|
||||
assert.equal(body.model_info["codex.context_length"], 272000);
|
||||
assert.deepEqual(body.model_info.supports_reasoning_effort, [
|
||||
"low",
|
||||
"medium",
|
||||
|
||||
Reference in New Issue
Block a user