Compare commits

..

35 Commits

Author SHA1 Message Date
diegosouzapw
c079bbbe05 feat(providers): integrate wave4 free-tier gateways 2026-08-10 21:42:36 -03:00
diegosouzapw
675c030ce3 feat(providers): add Free.ai registry 2026-08-10 21:42:36 -03:00
diegosouzapw
9d251c0557 feat(providers): add FreeInference registry 2026-08-10 21:42:36 -03:00
diegosouzapw
2856f06da1 feat(providers): integrate wave3 free-tier gateways 2026-08-10 21:42:36 -03:00
diegosouzapw
56322d498a feat(providers): add Naga AI and Chat Oripe registries 2026-08-10 21:42:36 -03:00
diegosouzapw
deefd15db1 feat(providers): add HelyxAI Auriko and Poixe registries 2026-08-10 21:42:36 -03:00
diegosouzapw
ebcd16785a feat(providers): add Wave 3-A free-tier registries 2026-08-10 21:42:36 -03:00
diegosouzapw
a18d4575a9 feat(providers): integrate wave2 free-tier gateways 2026-08-10 21:42:36 -03:00
diegosouzapw
4304ee7369 fix: align meganova provider slug 2026-08-10 21:42:35 -03:00
diegosouzapw
6055e6026c feat: add wave 2 free-tier provider registries 2026-08-10 21:42:35 -03:00
diegosouzapw
ae29ec743b feat(providers): add Mixlayer Speka and TokenReply registries 2026-08-10 21:42:35 -03:00
diegosouzapw
d90ebdcd72 feat: add wave2 free-tier provider registries 2026-08-10 21:42:35 -03:00
diegosouzapw
08d18d4e68 feat(providers): integrate audited free-tier gateways 2026-08-10 21:42:35 -03:00
diegosouzapw
1e2acdecc0 feat(providers): add Zylo UnoRouter and Poolside registries 2026-08-10 21:34:00 -03:00
Diego Rodrigues de Sa e Souza
5fd2325be7 fix(ratelimit): add queue-wait timeout tests and update sequencing tests (#9533) (#9662)
* fix(ratelimit): add queue-wait timeout and update sequencing tests (#9533)

* fix(combo): distinguish pre-dispatch skips from genuine failures to prevent false 503 ALL_ACCOUNTS_INACTIVE (#9630)

Closes #9630

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-10 20:10:48 -03:00
Diego Rodrigues de Sa e Souza
3acb74fe0a feat(providers): add Muse Code CLI provider preset (#9544) (#9670)
* feat(providers): add Muse Code CLI provider preset (#9544)

* fix(providers): register muse-code canonical provider + golden snapshot

- Add muse-code to APIKEY_PROVIDERS_FRONTIER so check:provider-consistency passes
- Regenerate translate-path golden snapshot to include the muse-code entry
  (20 additive lines, no other providers changed)

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-10 20:10:42 -03:00
Diego Rodrigues de Sa e Souza
0a1127be35 fix(ci): tighten unit suite ceiling from 100min to 80min (#9532) (#9678)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-10 20:10:38 -03:00
Diego Rodrigues de Sa e Souza
44069c5f54 feat(providers): add DeepAI as paid image provider (#6671) (#9443)
* feat(providers): add DeepAI as paid API-key image provider (#6671)

* fix(api): restore accidentally deleted agent-skills coverage route

Commit a5212536c2 (DeepAI provider feature) deleted
src/app/api/agent-skills/coverage/route.ts by mistake while touching
unrelated files, breaking tests/unit/agentSkills-routes.test.ts
(ERR_MODULE_NOT_FOUND) and the openapi-routes doc-sync gate, which
still documents GET /api/agent-skills/coverage.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-10 20:10:33 -03:00
Diego Rodrigues de Sa e Souza
41208fa398 feat(images): add full combo strategy execution for image generation (#9239) (#9499)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-10 20:10:29 -03:00
Diego Rodrigues de Sa e Souza
3797bbcd82 feat(providers): warm catalog startup from disk snapshot, parallel refresh (opencode-plugin) (#9490) (#9540)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-10 20:10:24 -03:00
Diego Rodrigues de Sa e Souza
bb5b68d7bd feat(providers): add Zylo UnoRouter and Poolside registries (#9585)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-10 20:10:20 -03:00
Diego Rodrigues de Sa e Souza
0481246bf4 fix(sse): preserve original body for semantic cache signature — fixes 0% hit rate (#9775)
The semantic cache signature (generateSignature) was computed over different
bodies at read-time vs write-time in handleChatCore. The cache read at Phase
9.1 uses the original body, but the writes at Phase 9.1 (non-streaming) and
Phase 9.2 (streaming) used the body after sanitizeChatRequestBody() and
injectMemoryAndSkills() mutated messages. Since the digest includes messages,
every request stored under a key no later request would look up — 0% hit
rate, every request billed.

Fix: snapshot bodyForCacheWrite right after the cache read and use it for
both write paths, so the write-time signature equals the read-time one.

TDD: tests/unit/cache-signature-roundtrip.test.ts proves the mutated body
produces a different signature (bug) and the preserved snapshot produces an
identical one (fix).

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-10 20:10:16 -03:00
Diego Rodrigues de Sa e Souza
5e02cf7623 fix(radar): refresh entitlement-sensitive state (#9776)
* fix(radar): refresh entitlement-sensitive state

* chore(changelog): assign Radar fix to PR 9776

* test(radar): localize canonical feed fixture

* test(radar): refresh canonical feed hash

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-10 20:10:12 -03:00
Diego Rodrigues de Sa e Souza
4d0182daa0 feat: bridge audio inputs for text-only models (#9807)
* feat(modality-bridge): resolve audio input capability

* feat(modality-bridge): resolve audio runtime settings

* feat(modality-bridge): add audio transcription helpers

* feat(modality-bridge): add Audio Bridge guardrail

* feat(dashboard): make Modality Bridge audio tab functional

* docs(guardrails): document Audio Bridge runtime

* fix(modality-bridge): harden audio catalog and response header

* chore(changelog): record audio modality bridge

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-10 20:10:07 -03:00
Diego Rodrigues de Sa e Souza
5deb40a33a fix(chat): treat content-less thinking/redacted bodies as valid, not empty_choices (#9971) (#10021)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-10 18:16:40 -03:00
Diego Rodrigues de Sa e Souza
1bf38e85d5 feat(onboarding): add one-click free provider setup (#10014)
* feat(onboarding): add one-click free provider setup (#9752)

* fix(i18n): preserve existing provider URL validation labels

* fix(i18n): restore provider URL validation labels

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-10 18:16:34 -03:00
Diego Rodrigues de Sa e Souza
d3037d1fdc feat(resilience): abort persistently slow upstream streams (#9709) (#10012)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-10 18:16:13 -03:00
Diego Rodrigues de Sa e Souza
227795cd08 feat(memory): support custom remote embedding endpoints (#9622) (#10010)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-10 18:16:08 -03:00
Diego Rodrigues de Sa e Souza
995618d27a feat(quality): detect forgotten sibling tests in PRs (#9530) (#10009)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-10 18:16:04 -03:00
Diego Rodrigues de Sa e Souza
696ad182cd feat(logs): show cache read and write token counts (#9620) (#10007)
* feat(logs): show cache read and write token counts (#9620)

* test(logs): use project alias in cache token coverage

* fix(logs): keep detail rendering independent of next-intl

* fix(logs): preserve standalone detail token labels

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-10 18:15:59 -03:00
Diego Rodrigues de Sa e Souza
344e4398c8 fix(quality): green release/v3.8.50 base-reds — env-doc sync + file-size freeze (#9985) (#10032)
* fix(quality): green release/v3.8.50 base-reds — env-doc sync + file-size freeze (#9985)

Sweep base-reds from issue #9985 on release/v3.8.50:
- env-doc-sync: add COMMANDCODE_API_URL + ANTIGRAVITY_ALLOW_SIGNATURE_BYPASS to
  .env.example and ENVIRONMENT.md (in code, missing from docs); add
  OMNIROUTE_STRICT_SYSTEM_PROVIDERS + TLS_FINGERPRINT_PROVIDERS to ENVIRONMENT.md
  (in .env.example, missing from doc). Restores the 3-way env contract.
- file-size: freeze open-sse/utils/proxyFetch.ts at 1207 (new proxied-TLS fetch
  helper over the 1000 cap). Owner-authorized quick rebaseline; slim for v3.9.0.

Co-authored-by: OmniRoute maintenance <maintainers@omniroute.local>

* fix(quality): green open-sse+dashboard typecheck base-reds (#9985)

Release-equivalent fast-gates surface 5 real TS regressions inherited by the
base from merged Fal/guardrails/cursor work (fast-gates PR->release do not run
these, so they accrued on release/v3.8.50):
- open-sse/handlers/imageGeneration/providers/fal.ts: normalizeProviderImagePayload
  missing 4th 'b64_json' arg (TS2554).
- open-sse/handlers/videoGeneration/falHandler.ts: narrow video to Record before .url.
- src/app/api/v1/images/generations/route.ts: type the toJsonErrorPayload read.
- src/lib/guardrails/visionBridgeHelpers.ts: cast through unknown for UA fetch.
- src/lib/providers/mergeProviderModelListing.ts: drop index-signature requirement
  that made interface RegistryModel[] unassignable (TS2322, from #9911).

All fixed in source (keeps the gates meaningful); each reproduces on the base tip.

Co-authored-by: OmniRoute maintenance <maintainers@omniroute.local>

* fix(quality): allowlist onnxruntime-node in dependency allowlist (#9985)

check:deps base-red — onnxruntime-node is a real production dep (transformers
embedding path) landed via the LLMLingua/transformers bump (#9962) without an
allowlist entry. Legit package: microsoft onnxruntime, verified in registry.

* fix(quality): rebaseline CodeQL ratchet 1->2 for #9940 fingerprint alerts (#9985)

Base-red: 2nd js/insufficient-password-hash alert on chatBodyAdmission API-key
fingerprints (sha256->16-hex admission-lane key), not password verification.
Reproduces on release/v3.8.50 tip. Owner-authorized rebaseline (revisit v3.9.0).

* fix(quality): green release/v3.8.50 unit base-reds (#9985)

8 unit-test base-reds reproducing on the pristine release tip, fixed in-source
(fast-gates PR->release do not run the unit suite, so these accrued silently):
- ServiceSupervisor: spawn-failure now resolves with error status (was throwing);
  health-probe-failure path still rejects. Distinct via spawnFailed flag.
- stream + responseSanitizer: numeric passthrough id preserved as string (was
  regenerated chatcmpl-); finish chunk with empty delta no longer swallowed by
  the emptyChoices guard.
- proxyFetch: genuine (non-abort) proxy transport failures keep the underlying
  reason in the surfaced error.
- auto-combo builtinCatalog: advertised undefined-variant auto/* ids (auto/chat,
  auto/best-chat, auto/pro-chat) materialize instead of throwing 'Unknown'.
- getTranslations en.json: add missing providers.iconUrlInvalid.
- optional-transformers-dependency.test: reconcile to #9962's deliberate
  move of @huggingface/transformers to a regular dep (napi onnxruntime).

Co-authored-by: OmniRoute maintenance <maintainers@omniroute.local>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@gmail.com>
Co-authored-by: OmniRoute maintenance <maintainers@omniroute.local>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-10 18:15:16 -03:00
Diego Rodrigues de Sa e Souza
8fc4023f94 fix(migrations): allow fresh install past mass-migration guard (#9934) (#10022)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-10 11:18:19 -03:00
Diego Rodrigues de Sa e Souza
aafdc4d4c6 fix(images): normalize image endpoint error format (#9981) (#10020)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-10 11:18:14 -03:00
Diego Rodrigues de Sa e Souza
fed0858f89 fix(encryption): identify failing credential in decrypt errors (#9927) (#10019)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-10 11:18:09 -03:00
Diego Rodrigues de Sa e Souza
2b6977229b fix(search): nest Exa contents options for /search (#9914) (#10018)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-10 11:18:04 -03:00
229 changed files with 10505 additions and 564 deletions

View File

@@ -1173,6 +1173,12 @@ CURSOR_USER_AGENT="Cursor/3.4"
# Or enable for all providers at once:
# CLI_COMPAT_ALL=1
# Allow the Antigravity request translator to skip its strict CLI request-signature
# validation when the upstream refuses real signatures (debug/antiquated-CLI mode).
# Default: real signatures enforced (unset) — signature bypass disabled.
# Used by: open-sse/translator/request/openai-to-gemini.ts
# ANTIGRAVITY_ALLOW_SIGNATURE_BYPASS=0
# ── Kimi Coding CLI identity overrides ──
# Used by: src/lib/oauth/providers/kimi-coding.ts — sent in OAuth + API headers.
# Leave unset to use the captured defaults baked into the OmniRoute build.
@@ -1848,6 +1854,17 @@ APP_LOG_TO_FILE=true
# Accepted values: true|1|on (enable). Unset or anything else = disabled (default).
# STREAM_RECOVERY_MIDSTREAM_ENABLED=true
# Active-stream throughput watchdog (#9709). Detects streams that keep sending
# heartbeats/chunks but produce too little useful assistant text. Separate from
# STREAM_IDLE_TIMEOUT_MS (silence) and the hard upstream attempt deadline. OFF by
# default. Tool-call/reasoning phases suspend judgement; post-commit streams are
# never blindly replayed.
# STREAM_THROUGHPUT_WATCHDOG_ENABLED=true
# STREAM_THROUGHPUT_WATCHDOG_WARMUP_MS=30000
# STREAM_THROUGHPUT_WATCHDOG_WINDOW_MS=30000
# STREAM_THROUGHPUT_WATCHDOG_MIN_BYTES_PER_SECOND=4
# STREAM_THROUGHPUT_WATCHDOG_MIN_USEFUL_BYTES=1
# Stagger interval (ms) between provider token healthchecks at startup.
# Used by: src/lib/tokenHealthCheck.ts. Default: 3000.
# HEALTHCHECK_STAGGER_MS=3000
@@ -2003,6 +2020,12 @@ APP_LOG_TO_FILE=true
# Default: 0.33.2
# COMMAND_CODE_VERSION=0.33.2
# Base URL for the Command Code usage/quota upstream, used by smartphone
# quota-fetcher telemetry.
# Used by: open-sse/services/usage/command-code.ts
# Default: https://api.commandcode.ai
# COMMANDCODE_API_URL=https://api.commandcode.ai
# ── MITM debug proxy (development only) ──
# Used by: src/mitm/server.cjs — captures upstream traffic for inspection.
# MITM_LOCAL_PORT=443

View File

@@ -194,6 +194,25 @@ jobs:
"$HOME/.local/bin/osv-scanner" --version || true
"$HOME/.local/bin/oasdiff" --version || true
zizmor --version || true
- name: Forgotten sibling tests (advisory)
env:
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
node scripts/quality/build-test-impact-map.mjs
node scripts/check/check-forgotten-sibling-tests.mjs \
--summary-file forgotten-sibling-tests.md \
--json-file forgotten-sibling-tests.json
cat forgotten-sibling-tests.md >> "$GITHUB_STEP_SUMMARY"
- name: Upload forgotten sibling report
if: always()
uses: actions/upload-artifact@v7
with:
name: forgotten-sibling-tests
path: |
forgotten-sibling-tests.md
forgotten-sibling-tests.json
if-no-files-found: ignore
retention-days: 30
# 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
@@ -279,7 +298,8 @@ jobs:
GITHUB_BASE_REF: ${{ github.base_ref }}
run: |
git fetch --no-tags origin "$GITHUB_BASE_REF" || true
node scripts/quality/build-test-impact-map.mjs
# The advisory sibling-test step generates the same map earlier in this job.
[ -f config/quality/test-impact-map.json ] || node scripts/quality/build-test-impact-map.mjs
SEL="$(node scripts/quality/select-impacted-tests.mjs)"
# Shadow evidence (#8084): persist every selection so TIA false negatives can
# be measured against fast-unit's full-suite verdict across releases BEFORE

View File

@@ -1033,7 +1033,11 @@ 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 });
const baseConfigHook = createOmniRouteConfigHook(resolved, {
cache: sharedCache,
diskSnapshotReader: defaultDiskSnapshotReader,
diskSnapshotWriter: defaultDiskSnapshotWriter,
});
const configWithSyncCommand = async (input: Config) => {
await baseConfigHook(input);
const cfg = input as Config & {
@@ -4741,7 +4745,7 @@ export type OmniRouteDiskSnapshotWriter = (
export type OmniRouteDiskSnapshotReader = (
providerId: string,
identityFingerprint: string
) => Promise<Omit<OmniRouteFetchCacheEntry, "expiresAt"> | undefined>;
) => Promise<(Omit<OmniRouteFetchCacheEntry, "expiresAt"> & { writtenAt?: number }) | undefined>;
/**
* Bind a snapshot to the endpoint and effective credential tuple without
@@ -4824,15 +4828,36 @@ 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. */
/** 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;
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)
// ────────────────────────────────────────────────────────────────────────────
@@ -5067,7 +5092,6 @@ export function createDebugLoggingFetch(
}
};
}
export const noopDiskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
export type OmniRouteReadAuthJson = () => Promise<AuthJsonShape | undefined | null>;
@@ -5170,8 +5194,8 @@ export function createOmniRouteConfigHook(
const compressionMetaFetcher =
deps.compressionMetaFetcher ?? defaultOmniRouteCompressionMetaFetcher;
const providersFetcher = deps.providersFetcher ?? defaultOmniRouteProvidersFetcher;
const diskSnapshotReader = deps.diskSnapshotReader ?? defaultDiskSnapshotReader;
const diskSnapshotWriter = deps.diskSnapshotWriter ?? defaultDiskSnapshotWriter;
const diskSnapshotReader = deps.diskSnapshotReader ?? noopDiskSnapshotReader;
const diskSnapshotWriter = deps.diskSnapshotWriter ?? noopDiskSnapshotWriter;
const now = deps.now ?? Date.now;
const cache: OmniRouteFetchCache = deps.cache ?? new Map();
const logger = deps.logger ?? console;
@@ -5266,12 +5290,12 @@ export function createOmniRouteConfigHook(
const t = now();
const cached = cache.get(cacheKey);
let rawModels: OmniRouteRawModelEntry[];
let rawCombos: OmniRouteRawCombo[];
let rawAutoCombos: OmniRouteRawAutoCombo[];
let rawEnrichment: OmniRouteEnrichmentMap;
let rawCompressionCombos: OmniRouteCompressionCombo[];
let rawConnections: OmniRouteProviderConnection[];
let rawModels: OmniRouteRawModelEntry[] = [];
let rawCombos: OmniRouteRawCombo[] = [];
let rawAutoCombos: OmniRouteRawAutoCombo[] = [];
let rawEnrichment: OmniRouteEnrichmentMap = new Map();
let rawCompressionCombos: OmniRouteCompressionCombo[] = [];
let rawConnections: OmniRouteProviderConnection[] = [];
if (cached && cached.expiresAt > t) {
rawModels = cached.rawModels;
@@ -5281,160 +5305,275 @@ export function createOmniRouteConfigHook(
rawCompressionCombos = cached.rawCompressionCombos;
rawConnections = cached.rawConnections;
} else {
// 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) {
// ─────────────────────────────────────────────────────────────────────
// 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";
logger.warn(
"[omniroute-plugin] config shim: /api/pricing/models fetch failed; publishing raw-id static catalog",
err
`[omniroute-plugin] config shim: warm startup from disk snapshot (${snapshotResult.rawModels.length} models, age ${ageLabel})`
);
}
}
// 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
);
// ─────────────────────────────────────────────────────────────────────
// 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;
}
}
}
// 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,
// 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,
});
}
// 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
);
// 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;
}
}
}
}

View File

@@ -33,6 +33,7 @@ import {
createOmniRouteProviderHook,
OmniRoutePlugin,
resolveOmniRoutePluginOptions,
_resetInflightRefresh,
type OmniRouteCombosFetcher,
type OmniRouteEnrichmentEntry,
type OmniRouteEnrichmentFetcher,
@@ -47,6 +48,16 @@ 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
// ────────────────────────────────────────────────────────────────────────────
@@ -1239,7 +1250,10 @@ test("config: diskCache hydrates stale snapshot when /v1/models throws", async (
);
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")),
logger.entries.some((e) =>
String(e[0]).includes("using stale disk cache") ||
String(e[0]).includes("warm startup from disk snapshot")
),
"disk-cache hydration breadcrumb emitted"
);
});

View File

@@ -0,0 +1,827 @@
/**
* 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
_tasks Symbolic link
View File

@@ -0,0 +1 @@
/home/diegosouzapw/dev/proxys/OmniRoute/_tasks

View File

@@ -0,0 +1 @@
- **feat(providers):** add DeepAI as paid API-key image provider ([#6671](https://github.com/diegosouzapw/OmniRoute/issues/6671))

View File

@@ -0,0 +1,7 @@
feat(images): execute full combo strategy + fallback in /v1/images/generations (#9239)
Add open-sse/services/imageCombo.ts that expands combo targets, filters
to images-capable, executes priority strategy with handleImageGeneration
per target, and returns first success or last failure. Route patches
detect combo names before model resolution and divert to the new
execution path.

View File

@@ -0,0 +1,5 @@
---
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.

View File

@@ -0,0 +1 @@
- Add an advisory forgotten-sibling-tests report to pull-request quality checks. The report traces changed modules through their static consumers to candidate sibling tests, while keeping barrel and dynamic-import cases non-blocking and requiring reviewed, referenced exceptions.

View File

@@ -0,0 +1 @@
- feat(providers): add Muse Code CLI provider preset (#9544)

View File

@@ -0,0 +1,2 @@
- Show cache-read and cache-write token counts in request log rows and details when providers
report them.

View File

@@ -0,0 +1 @@
- feat(memory): support custom OpenAI-compatible endpoints for Memory embeddings (#9622)

View File

@@ -0,0 +1 @@
- feat(resilience): add an opt-in watchdog for persistently slow upstream streams (#9709)

View File

@@ -0,0 +1,4 @@
- **Onboarding:** add an explicit, reviewable one-click setup for eligible no-auth LLM providers,
with per-provider caution links, selectable confirmation, idempotent creation, and safe partial
retries. Existing provider connections are never changed and setup completion never enables
providers silently. ([#9752](https://github.com/diegosouzapw/OmniRoute/issues/9752))

View File

@@ -0,0 +1 @@
- **feat(modality bridge):** Transcribe chat audio for text-only models through the existing speech-to-text providers, with configurable limits, caching, runtime stats, and a dashboard self-test ([#9807](https://github.com/diegosouzapw/OmniRoute/pull/9807))

View File

@@ -0,0 +1 @@
- **fix(ci):** tighten unit suite ceiling from 100min to 80min as a conservative step (#9532)

View File

@@ -0,0 +1 @@
- **fix(ratelimit):** added queue-wait timeout tests and updateFromResponseBody sequencing tests for the existing RATE_LIMIT_QUEUE_TIMEOUT feature in withRateLimit (#9533)

View File

@@ -0,0 +1 @@
- **fix(radar):** refresh signed catalog/referral caches when supporter entitlement changes, preserve the one-time live-to-community downgrade, and test real provider connection IDs from the setup tour

View File

@@ -0,0 +1 @@
- fix(search): nest Exa contents options (text/highlights) for /search API (#9914)

View File

@@ -0,0 +1 @@
- fix(encryption): name failing credential + recovery path in decrypt errors, dedupe per connection (#9927)

View File

@@ -0,0 +1 @@
- fix(migrations): don't abort on fresh install with only the 001 seed (#9934)

View File

@@ -0,0 +1 @@
- fix(chat): don't misclassify content-less thinking/redacted Claude bodies as empty_choices (#9971)

View File

@@ -0,0 +1 @@
- fix(quality): green release/v3.8.50 base-reds — sync 4 env vars into .env.example/ENVIRONMENT.md and freeze the new proxied-TLS proxyFetch helper in the file-size baseline (#9985)

View File

@@ -98,6 +98,7 @@
"omniglyph",
"open",
"opencode-ai",
"onnxruntime-node",
"ora",
"parse5",
"pino",

View File

@@ -420,8 +420,10 @@
"open-sse/executors/default.ts": 1355,
"open-sse/executors/kiro.ts": 1390,
"open-sse/translator/request/openai-to-kiro.ts": 1374,
"open-sse/utils/sseHeartbeat.ts": 194
"open-sse/utils/sseHeartbeat.ts": 194,
"open-sse/utils/proxyFetch.ts": 1207
},
"_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.",
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) \u2014 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 \u2014 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) \u2014 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/* \u2261 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).",

View File

@@ -0,0 +1,4 @@
{
"version": 1,
"entries": []
}

View File

@@ -149,10 +149,11 @@
"dedicatedGate": true
},
"codeqlAlerts": {
"value": 1,
"value": 2,
"direction": "down",
"dedicatedGate": true,
"_rebaseline_2026_08_06_base_grew": "Base branch file-size drift: translator-openai-to-gemini.test.ts grew 1619->1622 (test assertions for Gemini translator compatibility). CodeQL alert (js/insufficient-password-hash in raycast.ts) is pre-existing base-red; incremented baseline to match."
"_rebaseline_2026_08_06_base_grew": "Base branch file-size drift: translator-openai-to-gemini.test.ts grew 1619->1622 (test assertions for Gemini translator compatibility). CodeQL alert (js/insufficient-password-hash in raycast.ts) is pre-existing base-red; incremented baseline to match.",
"_rebaseline_2026_08_10_9940_fingerprint": "CodeQL base-red (green-prs sweep, issue #9985): 2nd js/insufficient-password-hash alert at src/shared/middleware/chatBodyAdmission.ts:265,269 introduced by #9940 (per-connection virtual admission lanes). Both are API-key/bearer FINGERPRINTS (createHash('sha256') truncated to 16-hex admission-lane key), not password VERIFICATION — false-positive class for this rule. Reproduces on release/v3.8.50 tip. Owner-authorized rebaseline 1->2; revisit at v3.9.0."
},
"secretFindings": {
"_note": "Zeroed 2026-07-13 (WS6/D3): the 3 frozen generic-api-key FPs are allowlisted with justification in .gitleaks.toml — any NEW finding regresses the ratchet.",

View File

@@ -29,11 +29,28 @@ changes:
| `Build (advisory)` | Non-draft code PRs and Mergify queue branches; Node 24, `npm-ci-retry`, `check:node-runtime`, `npm run build` with `OMNIROUTE_USE_TURBOPACK=1`; no artifact upload because no downstream quality job consumes it | **Advisory** (`continue-on-error: true`; remove after one week of stable release-PR runs) |
| `Docs Gates (fast-path)` | Docs/code PRs; API docs refs and docs-all | Yes |
| `Fast Quality Gates` | Code PRs; static checks, typecheck, dashboard typecheck, impacted unit tests | Yes |
| `Forgotten sibling tests` | Code PRs; changed modules traced to static consumers and candidate sibling tests; barrel and dynamic-import paths are reported as advisory diagnostics, with referenced allowlist exceptions | **Advisory** |
| `Vitest (fast-path)` | Code PRs; fast vitest suite | Yes |
| `Unit Tests fast-path` | Code PRs; 4-shard unit suite | Yes |
| `No new ESLint warnings` | Code PRs; suppressions-aware lint guard | Yes for own-origin, advisory for forks |
| `Merge integrity (changelog + generated skills)` | Non-draft PRs; changelog and generated skill sync | Yes for own-origin, advisory for forks |
#### Forgotten sibling tests report
`npm run check:forgotten-sibling-tests` reuses the import resolver behind the test-impact map.
For every changed production module, it reports deterministic
`changed module/symbol -> static consumer -> candidate sibling test` chains when the candidate
test is absent from the pull-request diff. The Markdown summary and JSON result are retained as
the `forgotten-sibling-tests` workflow artifact for calibration before any blocking rollout.
Barrel re-exports and dynamic imports are resolution diagnostics only; they never create a
blocking finding. Reviewed exceptions live in
`config/quality/forgotten-sibling-allowlist.json`. Each entry must name the consumer and candidate
test, give a specific rationale, and link a GitHub issue or pull request. Malformed entries fail
closed. Exceptions cannot suppress a deleted candidate test or a diff that adds `.skip`/`.todo`;
assertion weakening and other masking remain owned by the independently blocking
`check:test-masking` gate.
### Job: `lint`
Runs on every PR to `main`. Blocks merge on failure.

View File

@@ -260,6 +260,31 @@ it is unit-testable without a real Bottleneck limiter.
---
## 6. Slow-stream throughput watchdog (#9709)
The optional `resilienceSettings.streamRecovery.throughputWatchdog` guard detects
an upstream that is still sending chunks but producing assistant output below the
configured useful-output rate. It is deliberately distinct from the idle timeout:
heartbeats and metadata reset neither timer and do not count as progress. It is also
distinct from the hard attempt deadline (#9153), which remains an absolute safety
ceiling regardless of output quality.
The watchdog requires a warm-up period followed by a complete rolling window before
it can abort. It counts text deltas from Chat Completions and Responses API output
events (a conservative UTF-8 byte proxy), ignores usage-only and empty events, and
suspends judgement while tool-call or reasoning events are in flight. It is disabled
by default and can be enabled with `STREAM_THROUGHPUT_WATCHDOG_ENABLED=true`; the
window, warm-up, minimum rate, and minimum measurable output are bounded by the
normal resilience-settings normalization layer.
When enabled, a watchdog abort is applied only to the active upstream attempt. Before
any client-visible bytes, the existing same-account early-recovery path may reopen
the attempt. After commit, the stream is never blindly replayed; only the existing
safe mid-stream continuation contract can stitch a suffix. Finalization remains
single-shot, so usage accounting and semaphore release are not duplicated.
---
## Other Resilience Features
- **19 routing strategies** (priority, weighted, round-robin, context-relay, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, fusion, pipeline) — see [AUTO-COMBO.md](../routing/AUTO-COMBO.md).

View File

@@ -161,13 +161,15 @@ The `memory_vec_meta` table (migration `073_memory_vec.sql`) stores:
## Settings extension
Seven new fields were added to `MemorySettingsExtended` (plan 21, D9) in
Nine embedding and vector fields are available in `MemorySettingsExtended` in
`src/shared/schemas/memory.ts`, persisted via `src/lib/db/settings.ts`:
| Field | Type | Default | Description |
| ------------------------ | -------------------------------------------------- | -------- | ------------------------------------------------ |
| `embeddingSource` | `"remote" \| "static" \| "transformers" \| "auto"` | `"auto"` | Which embedding source to use |
| `embeddingProviderModel` | `string \| null` | `null` | Provider/model in `provider/model` format |
| `customBaseUrl` | `string \| null` | `null` | Memory-only OpenAI-compatible endpoint base URL |
| `customModelId` | `string \| null` | `null` | Model ID sent to the custom endpoint |
| `transformersEnabled` | `boolean` | `false` | Opt-in for Transformers.js (MiniLM, ~400MB) |
| `staticEnabled` | `boolean` | `false` | Opt-in for static potion-base-8M local model |
| `rerankEnabled` | `boolean` | `false` | Enable reranking step (adds +200-500ms/req) |
@@ -176,6 +178,14 @@ Seven new fields were added to `MemorySettingsExtended` (plan 21, D9) in
These are exposed via `GET /PUT /api/settings/memory` (schema `MemorySettingsExtendedSchema`).
For the `remote` source, Memory also accepts the optional `customBaseUrl` and
`customModelId` settings. Together they select an OpenAI-compatible `/embeddings`
endpoint and model without changing the global embedding registry. The endpoint is
normalized before use and checked by the provider outbound URL policy: HTTP(S) is
required, embedded credentials and query strings are rejected, and cloud-metadata
addresses remain blocked. Empty values preserve the selected registry provider. Errors
returned to the dashboard are sanitized and endpoint credentials are never logged.
> **TODO (D20):** Scope `global` (sharing memories across all API keys) is not
> implemented in this release. It requires schema changes and a global retrieval
> path. Track separately.

View File

@@ -1,13 +1,13 @@
---
title: "Radar Free-Model Catalog"
version: 3.8.50
lastUpdated: 2026-08-07
lastUpdated: 2026-08-08
---
# Radar Free-Model Catalog
> **Source of truth:** `src/lib/radar/`, `src/lib/db/radar.ts`, `src/app/api/radar/`
> **Last updated:** 2026-08-07 — v3.8.50
> **Last updated:** 2026-08-08 — v3.8.50
Radar is an **optional add-on** that overlays a signed, freshly-curated free-model
catalog on top of the release baseline (`FREE_MODEL_BUDGETS` in
@@ -23,6 +23,23 @@ below.
---
## Delivery status in v3.8.50
The following status distinguishes what this OSS release implements from later Radar
workstreams. It is a code-level status, not a promise that a particular hosted deployment
or external integration is currently available.
| Area | Status in this release |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Signed catalog client | Implemented behind `RADAR_ENABLED`, with separate opt-in, Ed25519 verification, local encrypted settings/cache, non-destructive overlay, scheduler, and dashboard. |
| Contributor activation | The dashboard links to the server-hosted GitHub claim flow and accepts an existing `omr_…` key. Contributor eligibility is resolved by the private service; the OSS client contains no GitHub token or issuance logic. |
| Supporter-key activation | Implemented. The raw key is validated, encrypted at rest, masked on reads, and sent only by the server-side sync. Changing or clearing the key invalidates both entitlement-sensitive feed caches. |
| Referral links | Implemented as a separately signed, hourly-refreshed feed. Fixed links are available to the community tier immediately; limited campaigns remain live-tier data. |
| Payments and transactional email | Not implemented in the OSS client. Purchase, donation, receipt review, and mail delivery belong to the private service and its later operational workstream. |
| Research-agent workstream | Not part of this client release. Curated feed contents remain server-side data; no autonomous research agent runs in an OmniRoute installation. |
---
## Flag: `RADAR_ENABLED` (default off)
Radar is gated end-to-end by the `RADAR_ENABLED` feature flag
@@ -67,7 +84,8 @@ When both are on, the sync path is:
plain, unauthenticated-by-default GET. OmniRoute never posts usage data, provider
configuration, or model traffic to the feed service.
3. The response is verified, validated, and cached locally (see
[Security model](#security-model)). Nothing else touches the network for Radar.
[Security model](#security-model)). Radar has exactly two server-side network paths:
`syncRadar()` for the catalog and `syncRadarReferrals()` for the standalone referrals feed.
The **supporter key** is an optional Bearer token (`radar_settings.supporter_key`)
that lets the feed service decide which tier to serve (see
@@ -77,6 +95,9 @@ that lets the feed service decide which tier to serve (see
helpers (`src/lib/db/encryption.ts`) used for provider credentials.
- Set via `POST /api/radar/settings` (`{ supporterKey: "omr_" + 40 hex chars }`) and
**never echoed back** — the response returns a masked form (`omr_****abcd`).
- Changing or clearing it atomically invalidates both the catalog and referrals caches. The
next sync/read resolves the new entitlement server-side; saving a key does not itself make
a network request or consume a single-use activation key.
- Sent to the feed service as a Bearer token on the sync GET — nothing else about the
key ever leaves the client.
@@ -102,10 +123,10 @@ pattern as `RADAR_FEED_URL`) and relayed to the dashboard through the existing
`GET /api/radar/settings` response (`contributorClaimUrl`, `supporterPlansUrl`) — the
client component never reads `process.env` itself.
| Var | Purpose |
| -------------------------------- | ---------------------------------------------------------------------------------------------- |
| `RADAR_CONTRIBUTOR_CLAIM_URL` | Overrides the contributor-claim URL (default `https://radar.omniroute.online/auth/github`). |
| `RADAR_SUPPORTER_PLANS_URL` | Overrides the supporter-plans URL (default `https://radar.omniroute.online/planos`). |
| Var | Purpose |
| ----------------------------- | ------------------------------------------------------------------------------------------- |
| `RADAR_CONTRIBUTOR_CLAIM_URL` | Overrides the contributor-claim URL (default `https://radar.omniroute.online/auth/github`). |
| `RADAR_SUPPORTER_PLANS_URL` | Overrides the supporter-plans URL (default `https://radar.omniroute.online/planos`). |
Once a visitor has a key (`omr_` + 40 hex chars), the activation screen
(`src/app/(dashboard)/dashboard/radar/page.tsx`) has a paste-key input as the primary
@@ -117,7 +138,7 @@ as a UX nicety; the server's Zod schema is the authoritative check either way. O
key is set, the activation screen shows the masked form (`supporterKeyMasked` from
`GET /api/radar/settings`) instead of an empty input, with a "change key" control to
paste a new one — the raw key is never redisplayed. The two claim/plans buttons above
remain the way to *obtain* a key in the first place; this input is where an operator
remain the way to _obtain_ a key in the first place; this input is where an operator
who already has one activates it.
---
@@ -207,11 +228,11 @@ handle.
### The served tier comes from a response header, not the signed body
The signed feed **body**'s `tier` field is always `"live"` — the feed service ships
**one signed artifact per version**, so the body cannot carry a per-request tier
without invalidating the Ed25519 signature (re-signing per request would defeat the
point of a pinned, cacheable, verifiable artifact). The tier actually served for a
given request is instead carried in the **`x-omniroute-feed-tier` response header**,
decided server-side from the request's `Authorization` key.
**two signed artifacts per version**: live includes current campaigns and community
omits them. Each artifact is signed over its own exact bytes. The body still does not
serve as the entitlement decision; the tier actually selected for a request is carried
in the **`x-omniroute-feed-tier` response header**, decided server-side from the request's
`Authorization` key.
`syncRadar()` (`src/lib/radar/sync.ts::parseServedTierHeader()`) is the single place
that resolves the tier a client should trust:
@@ -223,7 +244,7 @@ that resolves the tier a client should trust:
2. Fall back to the signed body's `tier` field (always `"live"`) only when step 1
yields nothing.
3. The resolved tier is what gets cached and returned as `{ status: "updated",
version, tier }` — this is the value the dashboard shows, never the raw body
version, tier }` — this is the value the dashboard shows, never the raw body
field.
---
@@ -265,18 +286,18 @@ Every merged entry carries an `origin` field the UI renders as a badge:
Five local routes back the UI, all under `src/app/api/radar/`:
| Route | Method | Purpose |
| ----------------------- | ------ | -------------------------------------------------------------------------------------------------- |
| `/api/radar/catalog` | GET | Returns the merged catalog (`getRadarCatalog()`) from the local cache. |
| `/api/radar/sync` | POST | Triggers `syncRadar()` server-side; returns the resulting status. |
| `/api/radar/settings` | GET | Returns `{ optIn, hasSupporterKey, supporterKeyMasked }` — never the raw key. |
| `/api/radar/settings` | POST | Sets opt-in and/or the (encrypted) supporter key. |
| `/api/radar/referrals` | GET | Returns `{ fixed, campaigns, tier }` from the local cache — see [Referral links](#referral-links-free-credits) below. |
| Route | Method | Purpose |
| ---------------------- | ------ | --------------------------------------------------------------------------------------------------------------------- |
| `/api/radar/catalog` | GET | Returns the merged catalog (`getRadarCatalog()`) from the local cache. |
| `/api/radar/sync` | POST | Triggers `syncRadar()` server-side; returns the resulting status. |
| `/api/radar/settings` | GET | Returns `{ optIn, hasSupporterKey, supporterKeyMasked }` — never the raw key. |
| `/api/radar/settings` | POST | Sets opt-in and/or the (encrypted) supporter key. |
| `/api/radar/referrals` | GET | Returns `{ fixed, campaigns, tier }` from the local cache — see [Referral links](#referral-links-free-credits) below. |
**Hard rule: these routes never proxy the feed service.** The browser only ever talks
to the local OmniRoute server; `syncRadar()` is the single module in the whole client
that touches the network for Radar (`src/lib/radar/sync.ts`), and it always runs
server-side, never client-side. This keeps the feed URL and any supporter key
to the local OmniRoute server. The two modules that touch the Radar service are
`src/lib/radar/sync.ts` (catalog) and `src/lib/radar/referralsSync.ts` (referrals); both
always run server-side, never client-side. This keeps the feed URL and any supporter key
out of client-facing network traffic entirely.
All five routes return `404` when `RADAR_ENABLED` is off (see
@@ -348,11 +369,13 @@ touches the network for referrals, mirroring `syncRadar()`'s contract exactly: f
Ed25519 signature over the exact response bytes (`verifyFeedBytes`), validates against
`RadarReferralsFeedSchema`, and caches into the `radar_referrals_cache` table
(migration `142_radar_referrals_cache.sql`) — a table entirely separate from the
catalog's `radar_feed_cache`. A 10 MB response cap and a `generatedAt` floor (an
incoming feed with a `generatedAt` no newer than the cached one is treated as `stale`
and never overwrites the cache — guards against a replay of an older signed artifact)
mirror the catalog sync's own `MAX_FEED_BYTES`/version-floor guards. Never throws —
always returns a status object; errors never carry a stack trace in `reason`.
catalog's `radar_feed_cache`. A 10 MB response cap and a `generatedAt` floor reject an
incoming feed older than the cached one, guarding against replay of an older signed
artifact. An equal timestamp is accepted: the server intentionally gives the community
and live referral variants the same deterministic `generatedAt`, so the signed payload
and served tier can change after a supporter-key change without the underlying link set
changing. Never throws — always returns a status object; errors never carry a stack trace
in `reason`.
Two triggers keep the referrals cache warm, both independent of the catalog's own
24h cadence:

View File

@@ -30,6 +30,18 @@ See **[WEB-COOKIE-GUIDE.md](./WEB-COOKIE-GUIDE.md)** for general setup instructi
## Quick Start: Connect Your First Provider
### Optional first-run free-provider setup
The first-run wizard offers an explicit **Set up free providers** card. It derives the current
eligible list from OmniRoute's no-auth provider registry, then lets you review and deselect each
provider before confirming. OmniRoute shows the provider's caution notice and a link to its site
so you can review third-party terms, privacy, availability, and rate limits first.
This action is optional: finishing the wizard never creates free-provider connections silently.
It creates only providers that are still missing, leaves existing customized connections
untouched, and reports created, already-configured, and failed providers individually. You can
safely retry only the failures after a partial result.
### Option A: Free Provider (No Credit Card)
1. Open the dashboard at `http://localhost:20128`

View File

@@ -340,6 +340,7 @@ Route upstream LLM provider calls through an HTTP or SOCKS5 proxy for egress con
| `SOCKS_HANDSHAKE_TIMEOUT_MS` | `10000` | `open-sse/utils/socksConnectorWithFamily.ts` | SOCKS5 handshake (connect) timeout in ms. Raise it when a single residential gateway host is hit by high concurrency (e.g. 100 simultaneous requests) — the real handshake can exceed 10s under a saturated pool even though the proxy is reachable, which otherwise surfaces as a false `[Proxy Fast-Fail] Proxy unreachable`. Capped at `120000`. |
| `PROXY_FAIL_OPEN` | `false` | `src/sse/handlers/chatHelpers.ts` | When `false` (default), a request whose assigned proxy fails to resolve is **refused (fail-closed)** rather than falling back to a direct connection — prevents real-IP leaks. Set `true` to restore the legacy DIRECT fallback. |
| `ENABLE_TLS_FINGERPRINT` | `false` | `open-sse/executors` | Spoof TLS fingerprint using wreq-js (mimics Chrome 124). Counters JA3/JA4 blocking. |
| `TLS_FINGERPRINT_PROVIDERS` | _(unset)_ | `open-sse/utils/proxyFetch.ts` | Comma-separated provider allowlist for the new proxied TLS routing (`open-sse/utils/proxyFetch.ts`). Direct TLS keeps its legacy behavior when unset; only these providers route through the Chrome-124 fingerprint bridge. |
| `OMNIROUTE_TURNSTILE_IGNORE_TLS_ERRORS` | `false` | `open-sse/services/claudeTurnstileSolver.ts` | Allow the Claude Turnstile Playwright browser context to ignore HTTPS certificate errors. |
### Scenarios
@@ -468,6 +469,7 @@ detection above).
| `OMNIROUTE_BOOTSTRAPPED` | `false` | `src/app/(dashboard)/dashboard/page.tsx` | Set `true` by bootstrap script after initial setup. Controls setup wizard visibility. |
| `OMNIROUTE_ALLOW_BODY_PROJECT_OVERRIDE` | `0` | `open-sse/executors/antigravity.ts` | Escape hatch: allow request body to override the Antigravity project field. |
| `ANTIGRAVITY_CREDITS` | `off` | `open-sse/services/antigravityCredits.ts` | Google One AI credits policy: `off` never injects credits, `retry` injects once after an eligible quota 429, and `always` injects on the first request. |
| `ANTIGRAVITY_ALLOW_SIGNATURE_BYPASS` | `0` | `open-sse/translator/request/openai-to-gemini.ts` | Allow the Antigravity request translator to skip its strict CLI request-signature validation when the upstream refuses real signatures (debug/antiquated-CLI mode). Non-zero enables the bypass. |
| `AGY_TOKEN_FILE` | `~/.gemini/antigravity-cli/antigravity-oauth-token` | `src/app/api/providers/agy-auth/apply-local/route.ts` | Override the Antigravity CLI (agy) token-file path for the auto-detect local login import. |
### OAuth CLI Bridge (Internal)
@@ -791,6 +793,7 @@ Embedding layer, vector store and reranking knobs for the persistent memory subs
| `MEMORY_TYPED_DECAY_EPISODIC_DAYS` | `30` | TTL (days) after which an unused `episodic` memory decays. `0` makes episodic immune too. Durable types (`factual`/`procedural`/`semantic`) are always immune. The decay clock re-bases on `last_accessed_at`. |
| `MEMORY_TYPED_DECAY_ACCESS_IMMUNITY` | `3` | A memory injected `>=` this many times becomes immune to decay regardless of type. `0` disables access immunity. |
| `MEMORY_TYPED_DECAY_SWEEP_INTERVAL` | `0` (disabled) | Interval (seconds) for the optional periodic decay sweep in `src/lib/memory/typedDecay.ts`. `0`/unset = no periodic sweep. Doubly opt-in: also requires `MEMORY_TYPED_DECAY_ENABLED=true`. |
| `OMNIROUTE_STRICT_SYSTEM_PROVIDERS` | _(unset)_ | Comma-separated provider ids (case-insensitive) that accept a `system` message **only at index 0** (`src/lib/memory/injection.ts`). For these, the cache-safe mid-array memory splice is unsafe in multi-turn conversations, so memory is merged/prepended as the leading system message instead. Defaults to only `xiaomi-mimo`/`mimo`; extend for self-hosted OpenAI-compatible endpoints (e.g. Qwen3.5+/3.6) whose chat template enforces the same single-leading-system-message constraint. |
### Low-RAM Docker Example
@@ -927,6 +930,11 @@ Anthropic-compatible provider instead.
| `PROVIDER_COOLDOWN_MAX_MS` | `300000` (5 min) | `open-sse/services/providerCooldownTracker.ts` | Maximum cooldown (ms) cap before a failed provider/connection is retried regardless. Only used when `PROVIDER_COOLDOWN_ENABLED`. |
| `STREAM_RECOVERY_ENABLED` | _(unset → off)_ | `src/lib/resilience/settings.ts` (seed) → `open-sse/services/streamRecovery.ts` (logic) | **What:** transparent recovery of truncated upstream streams (free-claude-code port). Holds the opening SSE window up to `STREAM_RECOVERY.HOLDBACK_MS` (750 ms) so a _pre-commit_ cutoff — one that happens before any byte reaches the client — is re-opened and retried invisibly. **When to enable:** flaky/upstreams that frequently 0-byte-truncate at stream start; leave OFF if you cannot afford up to 750 ms of added time-to-first-token on every stream. Accepts `true`/`1`/`on`. Seeds the persisted Resilience setting; the Dashboard setting wins once set. |
| `STREAM_RECOVERY_MIDSTREAM_ENABLED` | _(unset → off)_ | `src/lib/resilience/settings.ts` (seed) → `open-sse/services/streamRecovery.ts` (logic) | **What:** mid-stream continuation (Fase 4.4) — after a _post-commit_ truncation (bytes already reached the client), re-request with the partial text as an assistant prefill and stitch the missing suffix. Plain-text OpenAI-compatible streams only; never fires with a tool call in flight. **When to enable:** long generations that get cut mid-answer and you accept the recovered tail arriving as one burst rather than token-by-token. Independent of `STREAM_RECOVERY_ENABLED` (different risk profile). Accepts `true`/`1`/`on`. |
| `STREAM_THROUGHPUT_WATCHDOG_ENABLED` | _(unset → off)_ | `src/lib/resilience/settings.ts``open-sse/services/throughputWatchdog.ts` | Opt-in active-stream useful-output watchdog. Detects streams that keep sending chunks but remain below the configured assistant-output rate; heartbeats, usage events, empty deltas, and tool/reasoning phases do not masquerade as progress. Separate from idle and hard-deadline timeouts. |
| `STREAM_THROUGHPUT_WATCHDOG_WARMUP_MS` | `30000` | `src/lib/resilience/settings/normalize.ts` | Grace period before throughput evaluation, bounded to 0600000 ms. |
| `STREAM_THROUGHPUT_WATCHDOG_WINDOW_MS` | `30000` | `src/lib/resilience/settings/normalize.ts` | Rolling useful-output window, bounded to 1000600000 ms; one complete window is required before abort. |
| `STREAM_THROUGHPUT_WATCHDOG_MIN_BYTES_PER_SECOND` | `4` | `src/lib/resilience/settings/normalize.ts` | Minimum UTF-8 assistant-output byte rate (conservative token proxy), bounded to 11000000. |
| `STREAM_THROUGHPUT_WATCHDOG_MIN_USEFUL_BYTES` | `1` | `src/lib/resilience/settings/normalize.ts` | Minimum non-zero useful-output sample considered measurable, bounded to 11000000 bytes. |
| `HEALTHCHECK_STAGGER_MS` | `3000` | `src/lib/tokenHealthCheck.ts` | Stagger interval (ms) between provider token healthchecks at startup. |
| `HEALTHCHECK_JITTER_MIN_MS` | `500` | `src/lib/tokenHealthCheck.ts` | Minimum randomized jitter (ms) added on top of `HEALTHCHECK_STAGGER_MS` between provider token healthchecks, to prevent bursting (Issue #1220). |
| `HEALTHCHECK_JITTER_MAX_MS` | `5000` | `src/lib/tokenHealthCheck.ts` | Maximum randomized jitter (ms) added on top of `HEALTHCHECK_STAGGER_MS` between provider token healthchecks, to prevent bursting (Issue #1220). |
@@ -935,10 +943,9 @@ Anthropic-compatible provider instead.
| `MAX_RETRY_INTERVAL_SEC` | `30` | `src/sse/services/cooldownAwareRetry.ts` | Max backoff interval (seconds) between cooldown retries. Capped by this value regardless of upstream `Retry-After`. |
| `HEADROOM_URL` | `http://localhost:8787` | `src/lib/headroom/detect.ts` | Headroom token-saver proxy URL. The dashboard lifecycle (`api/headroom/*`) spawns a local `headroom-ai` CLI on loopback by default; override only to point at an external Docker sidecar proxy. |
### Stream-recovery tuning constants (not env vars)
### Stream-recovery tuning constants
The two `STREAM_RECOVERY_*` flags above are the only operator-facing toggles. The
recovery behavior is otherwise tuned by hardcoded constants in
The recovery holdback behavior is tuned by hardcoded constants in
`open-sse/config/constants.ts` (`STREAM_RECOVERY`), shown here for reference —
changing them requires a code edit, not an env var:
@@ -1092,6 +1099,7 @@ Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy),
| `OMNIROUTE_EMERGENCY_FALLBACK` | enabled | `open-sse/services/emergencyFallback.ts` | Set `false` (or `0`) to disable the emergency budget-exhaustion fallback that reroutes failed requests to the free `nvidia`/`openai/gpt-oss-120b` model. Effective precedence is Feature Flags DB override > env var > default; if unavailable, the service falls back to the raw env value. |
| `COMMAND_CODE_CALLBACK_PORT` | _(unset)_ | `src/app/api/providers/command-code/auth/shared.ts` | Local port used for OAuth-style callbacks from the Command Code CLI helper. |
| `COMMAND_CODE_VERSION` | `0.33.2` | `open-sse/executors/commandCode.ts` | Value sent as the `x-command-code-version` header to the Command Code upstream. Override to bump the CLI version. |
| `COMMANDCODE_API_URL` | `https://api.commandcode.ai` | `open-sse/services/usage/command-code.ts` | Base URL for the Command Code usage/quota upstream used by the smartphone quota-fetcher telemetry. Override for a self-hosted/alternative Command Code API. |
| `MITM_LOCAL_PORT` | `443` | `src/mitm/server.cjs` | Local bind port for the MITM debug proxy. |
| `MITM_DISABLE_TLS_VERIFY` | `0` | `src/mitm/server.cjs` | Set `1` to disable upstream TLS verification (development only). |
| `MITM_IDLE_TIMEOUT_MS` | `60000` | `src/mitm/socketTimeouts.ts`, `src/mitm/server.cjs` | Idle socket timeout (ms) for proxied connections; idle sockets past this are torn down to avoid leaking half-open tunnels. |

View File

@@ -7,7 +7,7 @@ lastUpdated: 2026-08-08
# Guardrails
> **Source of truth:** `src/lib/guardrails/`
> **Last updated:** 2026-08-08 — v3.8.50 (Modality Bridge PR-2: dashboard settings page, live stats, test action, and media-provider shortcuts)
> **Last updated:** 2026-08-08 — v3.8.50 (Modality Bridge PR-3: Audio Bridge runtime and functional Audio settings tab)
Guardrails enforce safety, policy, and content transformations at the boundary
between OmniRoute and upstream providers. Each guardrail can inspect (and
@@ -20,12 +20,13 @@ request. Blocking is an explicit decision (`block: true`), never an accident.
## Built-in Guardrails
The registry auto-loads four guardrails in priority order on import
The registry auto-loads five guardrails in priority order on import
(see `registry.ts``registerDefaultGuardrails()`):
| Priority | Name | Stage(s) | File |
| -------- | ------------------- | -------------- | --------------------- |
| `5` | `vision-bridge` | `preCall` | `visionBridge.ts` |
| `6` | `audio-bridge` | `preCall` | `audioBridge.ts` |
| `10` | `pii-masker` | `pre` + `post` | `piiMasker.ts` |
| `20` | `prompt-injection` | `preCall` | `promptInjection.ts` |
| `95` | `credential-masker` | `pre` + `post` | `credentialMasker.ts` |
@@ -113,8 +114,8 @@ The new `modalityBridge*` keys are Zod-validated in `updateSettingsSchema`
`modalityBridgeVisionMode`, `modalityBridgeVisionModel`,
`modalityBridgeVisionTaskAware`, `modalityBridgeVisionPrompt`,
`modalityBridgeVisionTimeout`, `modalityBridgeVisionMaxImages`, the
`modalityBridgeCache*` trio, and the PR-3-reserved `modalityBridgeAudio*`
group. Migration `141_modality_bridge_settings.sql` copies existing legacy
`modalityBridgeCache*` trio, and the `modalityBridgeAudio*` group used by the
Audio Bridge. Migration `141_modality_bridge_settings.sql` copies existing legacy
`visionBridge*` values to the matching new keys (idempotent, never overwrites
an operator-set `modalityBridge*` value); the legacy keys stay accepted as a
read fallback for one release cycle.
@@ -130,8 +131,8 @@ swap is already visible in the response body's `model` field.
`GET /api/modality-bridge/stats` (management auth, same tier as
`GET /api/settings`) returns the in-memory per-modality counters
`{ bridged, cacheHits, failures, lastUsedAt }` for `vision` (and the
PR-3-reserved `audio`). Counters reset on process restart by design
`{ bridged, cacheHits, failures, lastUsedAt }` for `vision` and `audio`.
Counters reset on process restart by design
(telemetry, not accounting).
#### Dashboard configuration
@@ -139,11 +140,12 @@ PR-3-reserved `audio`). Counters reset on process restart by design
The dedicated dashboard page is
`/dashboard/settings/modality-bridge`. Its URL-addressable `Vision`, `Audio`,
and `Video` tabs preserve query parameters while switching the `tab` value.
The Vision tab is live: it exposes enablement, mode, model selection (including
the automatic default), task-aware prompting, advanced timeout/image/cache
limits, runtime counters, and a guarded sample request. Audio and Video are
explicit placeholders: Audio is reserved for PR-3, while Video remains tracked
in issue `#9760`.
The Vision tab exposes enablement, mode, model selection (including the automatic
default), task-aware prompting, advanced timeout/image/cache limits, runtime
counters, and a guarded sample request. The Audio tab is also live: it exposes
enablement, an STT-only model picker with Auto, timeout/max-clip limits, audio
counters, and an `input_audio` sample test. Video remains the explicit placeholder
tracked in issue `#9760`.
The former Vision Bridge card under AI settings is a compatibility link to the
new page; it no longer owns a second copy of the form. Media Providers also
@@ -165,6 +167,64 @@ new mode/task-aware/cache defaults and the settings resolver live in
`deps` constructor option so tests can inject fake `getSettings` and
`callVisionModel` implementations.
### Audio Bridge (`audioBridge.ts`) — Modality Bridge PR-3
Intercepts audio-bearing chat requests before they reach a target that is not
known to accept audio input. It never reroutes the chat request: audio parts are
transcribed through the existing OpenAI-compatible multipart endpoint and the
chosen chat model continues with text transcripts.
Flow:
1. Resolve `supportsAudio` through `getResolvedModelCapabilities()`. Explicit
provider-registry metadata wins, then static model metadata, then synced
`modalities_input`. A declared input list without `audio` is `false`; no
capability evidence remains `null`. Both `false` and `null` activate the
conservative bridge, while `true` bypasses it.
2. Resolve `modalityBridgeAudio*` settings and extract spliceable top-level
audio parts from every message through the shared `detectMediaParts()`
detector. Supported wire shapes are OpenAI `input_audio`, `audio_url`, and
`source.media_type: "audio/*"`. Nested audio is detected for routing but not
removed by the splice path. Work is capped by `modalityBridgeAudioMaxClips`;
later parts stay untouched.
3. Honor a configured `provider/model`, or let `selectAudioBridgeModel()` walk
`AUDIO_TRANSCRIPTION_PROVIDERS` in stable catalog order and select the first
model with a usable active provider credential.
4. `callAudioTranscription()` converts base64/data-URI audio to a multipart
`file`, or downloads a remote `audio_url` through the public-only outbound
guard with DNS pinning and a 25 MB bound. It then POSTs the file and selected
model to the local `/v1/audio/transcriptions` self-loop, authenticated with
`resolveSelfLoopBearer()`. The existing transcription route performs normal
credential lookup, cooldown/rate-limit handling, and provider dispatch.
5. Successful calls replace their parts with `[Audio N]: <transcript>`. Calls
run with `Promise.allSettled`: an individual failure preserves that original
audio part (#4012 contract). If every call fails and the target is proven
`supportsAudio === false`, the parts become
`[Audio N]: (unavailable — no STT provider connected)` (#8430 contract). For
an unknown target (`null`), an all-failure result stays untouched. A proven
text-only target with no usable STT credential receives the same explicit
stub without issuing a network call.
Successful transcripts use the process-wide Modality Bridge LRU/TTL cache. The
key combines the audio reference, the stable `audio-transcription` operation
label, and selected STT model; failures are never cached. Audio attempts update
the shared `bridged`, `cacheHits`, `failures`, and `lastUsedAt` counters.
Transformed responses carry
`x-omniroute-modality-bridge: audio->text;model=<sttModel>;parts=<n>`; untouched
requests do not receive an Audio Bridge segment.
Runtime settings are DB-backed and Zod-validated:
| Key | Default | Range |
| ----------------------------- | ------- | -------------- |
| `modalityBridgeAudioEnabled` | `true` | — |
| `modalityBridgeAudioModel` | `""` | Auto or STT ID |
| `modalityBridgeAudioTimeout` | `60000` | 1000300000 |
| `modalityBridgeAudioMaxClips` | `3` | 110 |
The shared cache remains controlled by `modalityBridgeCacheEnabled`,
`modalityBridgeCacheTtlMinutes`, and `modalityBridgeCacheMaxEntries`.
### PII Masker (`piiMasker.ts`)
Runs on **both** stages.
@@ -370,8 +430,8 @@ Environment variables read by the built-in guardrails:
| `PII_REDACTION_ENABLED` | `pii-masker` | When `true`, request PII is redacted (independent of injection mode). |
| `PII_RESPONSE_SANITIZATION` / `_MODE` | `pii-masker` (downstream) | Controls response-side masker behavior. |
The Vision Bridge reads runtime config from the DB-backed settings store
(`getSettings()`), not env vars. The primary keys are
The Modality Bridge guardrails read runtime config from the DB-backed settings
store (`getSettings()`), not env vars. Vision's primary keys are
`modalityBridgeVisionEnabled`, `modalityBridgeVisionMode`,
`modalityBridgeVisionModel`, `modalityBridgeVisionTaskAware`,
`modalityBridgeVisionPrompt`, `modalityBridgeVisionTimeout`,
@@ -382,6 +442,11 @@ fallback; dashboard writes use the primary keys. Defaults and the fallback
resolver live in `src/shared/constants/modalityBridgeDefaults.ts`, with legacy
constants retained in `src/shared/constants/visionBridgeDefaults.ts`.
Audio uses `modalityBridgeAudioEnabled`, `modalityBridgeAudioModel`,
`modalityBridgeAudioTimeout`, and `modalityBridgeAudioMaxClips`, plus the shared
`modalityBridgeCache*` settings. Audio has no legacy-key fallback because these
keys were introduced with the Modality Bridge schema.
## Custom Guardrails
```typescript
@@ -419,9 +484,11 @@ Steps:
Use `resetGuardrailsForTests()` between tests to start from a known state.
Pass `{ registerDefaults: false }` to start with an empty registry and
register only the guardrails under test. The Vision Bridge guardrail accepts
dependency injection (`deps.getSettings`, `deps.callVisionModel`) so tests can
exercise the full flow without DB or network access.
register only the guardrails under test. Vision Bridge accepts dependency
injection (`deps.getSettings`, `deps.callVisionModel`); Audio Bridge exposes the
equivalent seams for settings, capabilities, STT model selection, credential
checks, and transcription. Tests can therefore exercise both flows without DB
or network access.
## See Also
@@ -430,6 +497,7 @@ exercise the full flow without DB or network access.
prompt-injection and PII masking
- `src/shared/constants/visionBridgeDefaults.ts` — Vision Bridge defaults and
forced-bridge model list
- `src/shared/constants/modalityBridgeDefaults.ts` — shared Vision/Audio runtime defaults
- `docs/architecture/RESILIENCE_GUIDE.md` — orthogonal layer (circuit breaker, cooldowns)
- `docs/reference/ENVIRONMENT.md` — full env var reference

View File

@@ -330,3 +330,15 @@ export const STREAM_RECOVERY = {
BUFFER_MAX_BYTES: 65536,
EARLY_RETRY_MAX: 4,
} as const;
/**
* Active-stream quality watchdog defaults (#9709). This is separate from the
* idle timeout (no chunks) and the absolute upstream-attempt deadline: it only
* evaluates useful assistant output after warm-up plus one complete window.
*/
export const STREAM_THROUGHPUT_WATCHDOG = {
WARMUP_MS: 30_000,
WINDOW_MS: 30_000,
MIN_USEFUL_BYTES_PER_SECOND: 4,
MIN_USEFUL_BYTES: 1,
} as const;

View File

@@ -14,6 +14,7 @@ import { notion_webProvider } from "./registry/notion-web/index.ts";
import { anthropicProvider } from "./registry/anthropic/index.ts";
import { sambanovaProvider } from "./registry/sambanova/index.ts";
import { puterProvider } from "./registry/puter/index.ts";
import { deepaiProvider } from "./registry/deepai/index.ts";
import { upstageProvider } from "./registry/upstage/index.ts";
import { nebiusProvider } from "./registry/nebius/index.ts";
import { fireworksProvider } from "./registry/fireworks/index.ts";
@@ -227,6 +228,33 @@ import { digitaloceanProvider } from "./registry/digitalocean/index.ts";
import { hcnsecProvider } from "./registry/hcnsec/index.ts";
import { promptqlProvider } from "./registry/promptql/index.ts";
import { hyperagentProvider } from "./registry/hyperagent/index.ts";
import { muse_codeProvider } from "./registry/muse-code/index.ts";
import { zyloApiProvider } from "./registry/zylo-api/index.ts";
import { poolsideProvider } from "./registry/poolside/index.ts";
import { fastrouterProvider } from "./registry/fastrouter/index.ts";
import { anyapiProvider } from "./registry/anyapi/index.ts";
import { electronhubProvider } from "./registry/electronhub/index.ts";
import { llmgatewayProvider } from "./registry/llmgateway/index.ts";
import { llmKiwiProvider } from "./registry/llm-kiwi/index.ts";
import { literouterProvider } from "./registry/literouter/index.ts";
import { mnnAiProvider } from "./registry/mnn-ai/index.ts";
import { meganovaAiProvider } from "./registry/meganova-ai/index.ts";
import { mixlayerProvider } from "./registry/mixlayer/index.ts";
import { spekaProvider } from "./registry/speka/index.ts";
import { tokenreplyProvider } from "./registry/tokenreply/index.ts";
import { yoloAutoProvider } from "./registry/yolo-auto/index.ts";
import { dxntProvider } from "./registry/dxnt/index.ts";
import { cloudcodeOneProvider } from "./registry/cloudcode-one/index.ts";
import { ofoxaiProvider } from "./registry/ofoxai/index.ts";
import { zerolimitaiProvider } from "./registry/zerolimitai/index.ts";
import { chatanywhereProvider } from "./registry/chatanywhere/index.ts";
import { helyxaiProvider } from "./registry/helyxai/index.ts";
import { aurikoProvider } from "./registry/auriko/index.ts";
import { poixeAiProvider } from "./registry/poixe-ai/index.ts";
import { nagaAiProvider } from "./registry/naga-ai/index.ts";
import { chatOripeProvider } from "./registry/chat-oripe/index.ts";
import { freeinferenceProvider } from "./registry/freeinference/index.ts";
import { freeAiProvider } from "./registry/free-ai/index.ts";
export const REGISTRY: Record<string, RegistryEntry> = {
aimlapi: aimlapiProvider,
@@ -241,6 +269,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
sambanova: sambanovaProvider,
puter: puterProvider,
upstage: upstageProvider,
deepai: deepaiProvider,
nebius: nebiusProvider,
fireworks: fireworksProvider,
llamagate: llamagateProvider,
@@ -455,5 +484,32 @@ export const REGISTRY: Record<string, RegistryEntry> = {
hcnsec: hcnsecProvider,
promptql: promptqlProvider,
hyperagent: hyperagentProvider,
"muse-code": muse_codeProvider,
"zylo-api": zyloApiProvider,
unorouter: unorouterProvider,
poolside: poolsideProvider,
fastrouter: fastrouterProvider,
anyapi: anyapiProvider,
electronhub: electronhubProvider,
llmgateway: llmgatewayProvider,
"llm-kiwi": llmKiwiProvider,
literouter: literouterProvider,
"mnn-ai": mnnAiProvider,
"meganova-ai": meganovaAiProvider,
mixlayer: mixlayerProvider,
speka: spekaProvider,
tokenreply: tokenreplyProvider,
"yolo-auto": yoloAutoProvider,
dxnt: dxntProvider,
"cloudcode-one": cloudcodeOneProvider,
ofoxai: ofoxaiProvider,
zerolimitai: zerolimitaiProvider,
chatanywhere: chatanywhereProvider,
helyxai: helyxaiProvider,
auriko: aurikoProvider,
"poixe-ai": poixeAiProvider,
"naga-ai": nagaAiProvider,
"chat-oripe": chatOripeProvider,
freeinference: freeinferenceProvider,
"free-ai": freeAiProvider,
};

View File

@@ -0,0 +1,11 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
export const aurikoProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "auriko",
alias: "auriko",
baseUrl: "https://api.auriko.ai/v1/chat/completions",
modelsUrl: "https://api.auriko.ai/v1/models",
models: [],
passthroughModels: true,
});

View File

@@ -0,0 +1,12 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
// The upstream brand and hostname are ambiguous, so avoid unverified quota claims.
export const chatOripeProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "chat-oripe",
alias: "chat-oripe",
baseUrl: "https://api.oriper.com/v1/chat/completions",
modelsUrl: "https://api.oriper.com/v1/models",
models: [],
passthroughModels: true,
});

View File

@@ -0,0 +1,12 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
// International endpoint; audited free access is limited to non-commercial use.
export const chatanywhereProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "chatanywhere",
alias: "chatanywhere",
baseUrl: "https://api.chatanywhere.org/v1/chat/completions",
modelsUrl: "https://api.chatanywhere.org/v1/models",
models: [],
passthroughModels: true,
});

View File

@@ -0,0 +1,20 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
/**
* CloudCode.ONE - OpenAI-compatible API with published free model aliases.
*
* The Anthropic-compatible endpoint is documented separately; this registry
* covers the OpenAI-compatible API surface audited for this migration.
*/
export const cloudcodeOneProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "cloudcode-one",
alias: "cloudcode-one",
baseUrl: "https://api.cloudcode.one/v1/chat/completions",
modelsUrl: "https://api.cloudcode.one/v1/models",
models: [
{ id: "glm-4.7-flash", name: "GLM 4.7 Flash" },
{ id: "glm-4.6v-flash", name: "GLM 4.6V Flash" },
],
passthroughModels: true,
});

View File

@@ -0,0 +1,13 @@
import type { RegistryEntry } from "../shared";
export const deepaiProvider: RegistryEntry = {
id: "deepai",
alias: "deepai",
format: "custom",
baseUrl: "https://api.deepai.org",
authType: "apikey",
authHeader: "api-key",
models: [
{ id: "text2img", name: "Text to Image" },
],
};

View File

@@ -0,0 +1,17 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
/**
* DXNT - OpenAI-compatible API with a free account quota.
*
* Models are discovered from the provider's authenticated catalog rather than
* copied into a static list, so account-specific availability remains intact.
*/
export const dxntProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "dxnt",
alias: "dxnt",
baseUrl: "https://www.dxnt.com/v1/chat/completions",
modelsUrl: "https://www.dxnt.com/v1/models",
models: [],
passthroughModels: true,
});

View File

@@ -0,0 +1,11 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
export const freeAiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "free-ai",
alias: "free-ai",
baseUrl: "https://api.free.ai/v1/chat/",
modelsUrl: "https://api.free.ai/v1/models",
models: [],
passthroughModels: true,
});

View File

@@ -0,0 +1,11 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
export const freeinferenceProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "freeinference",
alias: "freeinference",
baseUrl: "https://freeinference.org/v1/chat/completions",
modelsUrl: "https://freeinference.org/v1/models",
models: [],
passthroughModels: true,
});

View File

@@ -0,0 +1,11 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
export const helyxaiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "helyxai",
alias: "helyxai",
baseUrl: "https://helyxai.space/v1/chat/completions",
modelsUrl: "https://helyxai.space/v1/models",
models: [],
passthroughModels: true,
});

View File

@@ -0,0 +1,11 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
export const literouterProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "literouter",
alias: "literouter",
baseUrl: "https://api.literouter.com/v1/chat/completions",
modelsUrl: "https://api.literouter.com/v1/models",
models: [],
passthroughModels: true,
});

View File

@@ -0,0 +1,11 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
export const meganovaAiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "meganova-ai",
alias: "meganova-ai",
baseUrl: "https://api.meganova.ai/v1/chat/completions",
modelsUrl: "https://api.meganova.ai/v1/models",
models: [],
passthroughModels: true,
});

View File

@@ -0,0 +1,11 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
export const mixlayerProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "mixlayer",
alias: "mixlayer",
baseUrl: "https://models.mixlayer.ai/v1/chat/completions",
modelsUrl: "https://models.mixlayer.ai/v1/models",
models: [{ id: "qwen/qwen3.5-4b-free", name: "Qwen 3.5 4B (free)" }],
passthroughModels: true,
});

View File

@@ -0,0 +1,11 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
export const mnnAiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "mnn-ai",
alias: "mnn-ai",
baseUrl: "https://api.mnnai.ru/v1/chat/completions",
modelsUrl: "https://api.mnnai.ru/v1/models",
models: [],
passthroughModels: true,
});

View File

@@ -0,0 +1,106 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
/**
* Muse Code CLI — Meta's agentic coding tool.
*
* Wire format: OpenAI Responses API (POST /responses).
* Auth: Bearer token from META_API_KEY env var.
* Reasoning efforts: xhigh/ultra -> high (handled generically).
*
* @see https://github.com/joymadhu49/muse-openrouter-shim
*/
export const muse_codeProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "muse-code",
alias: "mc",
passthroughModels: true,
defaultContextLength: 200000,
models: [
{
id: "llama-4-maverick",
name: "Llama 4 Maverick",
contextLength: 1048576,
maxOutputTokens: 131072,
supportsReasoning: true,
supportsXHighEffort: true,
toolCalling: true,
supportsVision: true,
targetFormat: "openai-responses",
unsupportedParams: ["logprobs", "topLogprobs", "logitBias"],
},
{
id: "llama-4-scout",
name: "Llama 4 Scout",
contextLength: 1048576,
maxOutputTokens: 131072,
supportsReasoning: true,
supportsXHighEffort: true,
toolCalling: true,
supportsVision: true,
targetFormat: "openai-responses",
unsupportedParams: ["logprobs", "topLogprobs", "logitBias"],
},
{
id: "llama-3.3-70b",
name: "Llama 3.3 70B",
contextLength: 131072,
maxOutputTokens: 32768,
supportsReasoning: false,
toolCalling: true,
targetFormat: "openai-responses",
unsupportedParams: ["logprobs", "topLogprobs"],
},
{
id: "llama-3.1-405b",
name: "Llama 3.1 405B",
contextLength: 131072,
maxOutputTokens: 32768,
supportsReasoning: false,
toolCalling: true,
targetFormat: "openai-responses",
unsupportedParams: ["logprobs", "topLogprobs"],
},
{
id: "llama-3.1-70b",
name: "Llama 3.1 70B",
contextLength: 131072,
maxOutputTokens: 32768,
supportsReasoning: false,
toolCalling: true,
targetFormat: "openai-responses",
unsupportedParams: ["logprobs", "topLogprobs"],
},
{
id: "llama-3.1-8b",
name: "Llama 3.1 8B",
contextLength: 131072,
maxOutputTokens: 32768,
supportsReasoning: false,
toolCalling: true,
targetFormat: "openai-responses",
unsupportedParams: ["logprobs", "topLogprobs"],
},
{
id: "llama-3.2-90b-vision",
name: "Llama 3.2 90B Vision",
contextLength: 131072,
maxOutputTokens: 32768,
supportsReasoning: false,
toolCalling: true,
supportsVision: true,
targetFormat: "openai-responses",
unsupportedParams: ["logprobs", "topLogprobs"],
},
{
id: "llama-3.2-11b-vision",
name: "Llama 3.2 11B Vision",
contextLength: 131072,
maxOutputTokens: 32768,
supportsReasoning: false,
toolCalling: true,
supportsVision: true,
targetFormat: "openai-responses",
unsupportedParams: ["logprobs", "topLogprobs"],
},
],
});

View File

@@ -0,0 +1,12 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
// Free access terms may permit data collection or training use; discover models dynamically.
export const nagaAiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "naga-ai",
alias: "naga-ai",
baseUrl: "https://api.naga.ac/v1/chat/completions",
modelsUrl: "https://api.naga.ac/v1/models",
models: [],
passthroughModels: true,
});

View File

@@ -0,0 +1,11 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
export const ofoxaiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "ofoxai",
alias: "ofoxai",
baseUrl: "https://api.ofox.ai/v1/chat/completions",
modelsUrl: "https://api.ofox.ai/v1/models",
models: [],
passthroughModels: true,
});

View File

@@ -0,0 +1,11 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
export const poixeAiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "poixe-ai",
alias: "poixe-ai",
baseUrl: "https://api.poixe.com/v1/chat/completions",
modelsUrl: "https://api.poixe.com/v1/models",
models: [],
passthroughModels: true,
});

View File

@@ -0,0 +1,11 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
export const poolsideProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "poolside",
alias: "poolside",
baseUrl: "https://inference.poolside.ai/v1/chat/completions",
modelsUrl: "https://inference.poolside.ai/v1/models",
models: [],
passthroughModels: true,
});

View File

@@ -0,0 +1,11 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
export const spekaProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "speka",
alias: "speka",
baseUrl: "https://speka.me/v1/chat/completions",
modelsUrl: "https://speka.me/v1/models",
models: [],
passthroughModels: true,
});

View File

@@ -0,0 +1,11 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
export const tokenreplyProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "tokenreply",
alias: "tokenreply",
baseUrl: "https://api.tokenreply.com/v1/chat/completions",
modelsUrl: "https://api.tokenreply.com/v1/models",
models: [],
passthroughModels: true,
});

View File

@@ -1,14 +1,11 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
export const unorouterProvider: RegistryEntry = {
export const unorouterProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "unorouter",
alias: "unorouter",
format: "openai",
executor: "default",
baseUrl: "https://api.unorouter.ai/v1/chat/completions",
authType: "apikey",
authHeader: "bearer",
defaultContextLength: 128000,
models: [{ id: "auto", name: "Auto (Best Available)" }],
baseUrl: "https://api.unorouter.com/v1/chat/completions",
modelsUrl: "https://api.unorouter.com/v1/models",
models: [],
passthroughModels: true,
};
});

View File

@@ -0,0 +1,17 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
/**
* Yolo-Auto - OpenAI-compatible API with a request-limited free tier.
*
* The catalog is kept intentionally small: the documented free-tier model is
* seeded while passthrough discovery allows the service to publish updates.
*/
export const yoloAutoProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "yolo-auto",
alias: "yolo-auto",
baseUrl: "https://yolo-auto.com/v1/chat/completions",
modelsUrl: "https://yolo-auto.com/v1/models",
models: [{ id: "qwen3.6-35b-a3b", name: "Qwen 3.6 35B A3B" }],
passthroughModels: true,
});

View File

@@ -0,0 +1,11 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
export const zerolimitaiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "zerolimitai",
alias: "zerolimitai",
baseUrl: "https://www.zerolimitai.com/api/v1/chat/completions",
modelsUrl: "https://www.zerolimitai.com/api/v1/models",
models: [],
passthroughModels: true,
});

View File

@@ -0,0 +1,11 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
export const zyloApiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "zylo-api",
alias: "zylo",
baseUrl: "https://api.zyloai.net/v1/chat/completions",
modelsUrl: "https://api.zyloai.net/v1/models",
models: [],
passthroughModels: true,
});

View File

@@ -50,6 +50,7 @@ export interface RegistryModel {
supportsReasoning?: boolean;
supportedThinkingEfforts?: readonly string[];
supportsVision?: boolean;
supportsAudio?: boolean;
supportsXHighEffort?: boolean;
maxOutputTokens?: number;
targetFormat?: string;

View File

@@ -1037,6 +1037,13 @@ export async function handleChatCore({
log?.debug?.("FORMAT", `${sourceFormat}${targetFormat} | stream=${stream}`);
// Preserve original body for cache signature — the body variable is mutated
// multiple times below (sanitization, memory/skills injection) before the
// cache store path runs at Phase 9.1 (non-streaming) / Phase 9.2 (streaming).
// Without this snapshot, the write-time signature differs from the read-time
// one, producing 0% hit rate. (#cache-signature-asymmetry)
const bodyForCacheWrite = body;
// ── Phase 9.1: Semantic cache check (temp=0, any streaming mode) ──
const cacheHit = await checkSemanticCache({
semanticCacheEnabled,
@@ -2976,6 +2983,8 @@ export async function handleChatCore({
const okStatus = res.response.status >= 200 && res.response.status < 300;
let streamRecoveryEnabled = false;
let continueMidStreamEnabled = false;
let throughputWatchdog =
resolveResilienceSettings(null).streamRecovery.throughputWatchdog;
if (okStatus) {
try {
// Reuse the request-consolidated settings read (see line ~2076) — no
@@ -2990,6 +2999,7 @@ export async function handleChatCore({
const goalOverride = !operatorExplicit && agentGoalPolicy.streamRecoveryEnabled;
streamRecoveryEnabled = sr.enabled || goalOverride;
continueMidStreamEnabled = sr.continueMidStream === true;
throughputWatchdog = sr.throughputWatchdog;
if (goalOverride && !sr.enabled) {
log?.info?.(
"AGENT_GOAL",
@@ -2999,11 +3009,13 @@ export async function handleChatCore({
} catch {
streamRecoveryEnabled = false;
continueMidStreamEnabled = false;
throughputWatchdog =
resolveResilienceSettings(null).streamRecovery.throughputWatchdog;
}
}
let clientBody: ReadableStream<Uint8Array>;
if (streamRecoveryEnabled) {
if (streamRecoveryEnabled || throughputWatchdog.enabled) {
// Run the SAME upstream (same account/creds) with a given body and return
// its 2xx stream, or null. Used both by the early-retry re-open (same body)
// and the mid-stream continuation (assistant-prefilled body).
@@ -3085,6 +3097,12 @@ export async function handleChatCore({
"STREAM_RECOVERY",
`mid-stream continuation attempt ${attempt}/${STREAM_RECOVERY.EARLY_RETRY_MAX}`
),
throughputWatchdog,
onWatchdogAbort: () =>
log?.warn?.(
"STREAM_WATCHDOG",
"active upstream stream stayed below the configured useful-output rate"
),
}
);
} else {
@@ -4557,7 +4575,7 @@ export async function handleChatCore({
// ── Phase 9.1: Cache store (non-streaming, temp=0) ──
storeSemanticCacheResponse({
enabled: semanticCacheEnabled,
body,
body: bodyForCacheWrite,
headers: clientRawRequest?.headers,
translatedResponse,
model,
@@ -4911,7 +4929,7 @@ export async function handleChatCore({
enabled: semanticCacheEnabled,
streamStatus,
streamResponseBody,
body,
body: bodyForCacheWrite,
headers: clientRawRequest?.headers,
model,
apiKeyId: apiKeyInfo?.id ?? undefined,

View File

@@ -89,7 +89,7 @@ export async function handleFalAIImageEdit({
const payload = await response.json();
const normalizedBody =
body.response_format === undefined ? { ...body, response_format: "b64_json" } : body;
const imagesOut = await normalizeProviderImagePayload(payload, normalizedBody, log);
const imagesOut = await normalizeProviderImagePayload(payload, normalizedBody, log, "b64_json");
return saveImageSuccessResult({
provider,
model: editModel,

View File

@@ -576,15 +576,21 @@ function sanitizeResponsesUsage(usage: unknown): unknown {
/**
* Normalize response ID to use chatcmpl- prefix.
* Preserves numeric/short custom ids as their string form rather than
* regenerating them — a passthrough numeric id (e.g. `123`) must stay `"123"`
* so streaming clients can correlate chunks (#3427/#5776). Only a genuinely
* missing/empty id gets a fresh `chatcmpl-` token.
*/
function normalizeResponseId(id: unknown): string {
if (!id || typeof id !== "string") {
if (!id || (typeof id !== "string" && typeof id !== "number")) {
return `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 29)}`;
}
// Already correct format
if (id.startsWith("chatcmpl-")) return id;
// Keep custom IDs but don't break them
return id;
const str = String(id);
if (str === "") {
return `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 29)}`;
}
// Already correct format, or a custom/numeric id — keep it.
return str;
}
function normalizeResponsesId(id: unknown): string {

View File

@@ -523,6 +523,19 @@ export function translateNonStreamingResponse(
}
}
// #9971: a content-less-but-valid Claude body (thinking / redacted_thinking
// / tool_use-only, or a truncated extended-thinking-only stream) has blocks
// but no final text. Surfacing it here helps correlate a live VPS capture
// with detectMalformedNonStream's clause; the content itself is valid output
// (see detectMalformedNonStream), so this is observation, not a decision.
if (textContent.length === 0 && process.env.DEBUG_CLAUDE_NONSTREAM === "true") {
console.log(
`[ClaudeNonStream] ${contentBlocks.length} content block(s), empty textContent ` +
`(thinking=${thinkingContent.length}, toolCalls=${toolCalls.length}); ` +
`content-less-but-valid body preserved (not empty_choices)`
);
}
const message: JsonRecord = { role: "assistant" };
if (textContent) {
message.content = textContent;

View File

@@ -336,8 +336,10 @@ function buildExaRequest(
query: params.query,
numResults: params.maxResults,
type: "auto",
text: true,
highlights: true,
contents: {
text: true,
highlights: true,
},
};
if (includes.length) body.includeDomains = includes;
if (excludes.length) body.excludeDomains = excludes;

View File

@@ -90,7 +90,10 @@ function absoluteFalUrl(value: unknown, baseUrl: string): string | undefined {
function normalizeFalVideoResponse(payload: unknown) {
const record = payload && typeof payload === "object" ? (payload as Record<string, unknown>) : {};
const video = record.video && typeof record.video === "object" ? record.video : null;
const video =
record.video && typeof record.video === "object"
? (record.video as Record<string, unknown>)
: null;
const url = video && typeof video.url === "string" ? video.url.trim() : "";
if (!url) {

View File

@@ -182,6 +182,16 @@ export async function createBuiltinAutoCombo(modelStr: string, suffix: string) {
return virtualCombo;
}
// Advertised `auto/*` ids whose template maps to no variant (auto/chat,
// auto/best-chat, auto/pro-chat) still materialize via the default
// (unconstrained) virtual combo rather than throwing "Unknown built-in".
if (Object.prototype.hasOwnProperty.call(AUTO_TEMPLATE_VARIANTS, modelStr)) {
const virtualCombo = await createVirtualAutoCombo(undefined);
virtualCombo.name = modelStr;
virtualCombo.id = modelStr;
return virtualCombo;
}
// #4235 Phase B: `auto/<category>[:<tier>]` (e.g. auto/coding:fast, auto/vision).
const parsed = parseAutoSuffix(suffix);
if (parsed.valid) {

View File

@@ -0,0 +1,199 @@
/**
* Image Combo Strategy Execution
*
* Executes a full Combo strategy for image generation requests. Expands combo
* targets via resolveComboTargets(), filters to images-capable targets, runs
* each target via handleImageGeneration() using a priority strategy, provides
* per-credential resolution, and returns the first success or last failure.
*
* #9239
*/
import { getComboByName, getCombos } from "@/lib/db/combos";
import { resolveComboTargets } from "@omniroute/open-sse/services/combo.ts";
import { getImageModelEntry, parseImageModel } from "@omniroute/open-sse/config/imageRegistry.ts";
import {
getProviderCredentialsWithQuotaPreflight,
clearRecoveredProviderState,
} from "@/sse/services/auth";
import { isAllRateLimitedCredentials } from "@/app/api/v1/_shared/rateLimit";
import { handleImageGeneration } from "@omniroute/open-sse/handlers/imageGeneration.ts";
import { attachOmniRouteMetaHeaders } from "@/domain/omnirouteResponseMeta";
import { generateRequestId } from "@/shared/utils/requestId";
import { calculateModalCost } from "@/lib/usage/costCalculator";
import { toJsonErrorPayload } from "@/shared/utils/upstreamError";
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
import * as logger from "@/sse/utils/logger";
/**
* Execute a full combo strategy for an image generation request.
*
* 1. Resolve combo targets via resolveComboTargets.
* 2. Filter to images-capable targets (those with an entry in the image registry).
* 3. Iterate targets in priority order; for each target, resolve credentials and
* call handleImageGeneration. Return the first success or the last failure.
* 4. Attach combo name, selected target, and fallback count to response headers.
*/
export async function executeImageCombo(
comboName: string,
body: Record<string, unknown>,
auth: {
request: Request;
policy: { apiKeyInfo?: { id?: string; name?: string } | null };
},
startTime: number,
log: typeof logger
): Promise<Response> {
// 1. Resolve combo targets
const combo = await getComboByName(comboName);
if (!combo) {
// Model name is not a combo; the caller should handle this as a direct model
return errorResponse(
HTTP_STATUS.BAD_REQUEST,
`Combo not found: ${comboName}`
);
}
const allCombos = await getCombos();
const targets = resolveComboTargets(combo as never, allCombos as never);
if (!targets || targets.length === 0) {
return errorResponse(
HTTP_STATUS.BAD_REQUEST,
`Combo "${comboName}" has no usable targets`
);
}
// 2. Filter to images-capable targets
const imageTargets = targets.filter((t) => {
if (!t.modelStr) return false;
const entry = getImageModelEntry(t.modelStr);
return entry !== null;
});
if (imageTargets.length === 0) {
return errorResponse(
HTTP_STATUS.BAD_REQUEST,
`No images-capable targets in combo "${comboName}"`
);
}
// 3. Iterate targets in priority order (first healthy target wins)
let lastError: { status: number; error: string } | null = null;
let successResult: { data: unknown; provider: string; model: string } | null = null;
let fallbackCount = 0;
let selectedProvider = "";
let selectedModel = "";
for (const target of imageTargets) {
const { provider: targetProvider, model: targetModel } = parseImageModel(target.modelStr);
if (!targetProvider) {
lastError = { status: 400, error: `Invalid image model: ${target.modelStr}` };
fallbackCount += 1;
continue;
}
// Resolve provider credentials
let credentials = null;
try {
credentials = await getProviderCredentialsWithQuotaPreflight(targetProvider);
} catch {
// DB unavailable — skip this target
lastError = { status: 502, error: `Failed to resolve credentials for ${targetProvider}` };
fallbackCount += 1;
continue;
}
if (!credentials) {
lastError = { status: 400, error: `No credentials for image provider: ${targetProvider}` };
fallbackCount += 1;
continue;
}
if (isAllRateLimitedCredentials(credentials)) {
lastError = {
status: 429,
error: `[${targetProvider}] All accounts rate limited`,
};
fallbackCount += 1;
continue;
}
// Execute image generation for this target
const result = await handleImageGeneration({
body: { ...body, model: target.modelStr },
credentials,
log,
signal: auth.request?.signal || null,
});
if (result.success) {
await clearRecoveredProviderState(credentials);
selectedProvider = targetProvider;
selectedModel = target.modelStr;
successResult = {
data: result.data,
provider: targetProvider,
model: target.modelStr,
};
break;
}
// Classify the failure
const status = result.status || 500;
const error = typeof result.error === "string" ? result.error : "Image generation failed";
// Terminal failures (400 bad model, 403 banned, etc.) — stop iterating
// Non-terminal failures (429, 5xx) — try next target
if (status === 400 || status === 403 || status === 401) {
return errorResponse(
status,
`[${targetProvider}] ${error}`
);
}
lastError = { status, error: `[${targetProvider}] ${error}` };
fallbackCount += 1;
}
// 4. Build response
if (successResult) {
const n = Math.max(
Number(body.n) || 1,
(
successResult.data as { data?: { data?: unknown[] } }
).data?.data?.length || 0
);
const costUsd = await calculateModalCost(
"image",
selectedProvider,
selectedModel,
{ n }
);
const headers = new Headers({ "Content-Type": "application/json" });
attachOmniRouteMetaHeaders(headers, {
provider: selectedProvider,
model: selectedModel,
costUsd,
latencyMs: Date.now() - startTime,
requestId: generateRequestId(),
strategy: "priority",
fallbackAttempts: fallbackCount,
});
return new Response(
JSON.stringify((successResult.data as { data: unknown }).data),
{ status: 200, headers }
);
}
// All targets failed — return the last error
const errorPayload = toJsonErrorPayload(
lastError?.error || "All combo targets failed",
"Image combo targets all failed"
);
return new Response(JSON.stringify(errorPayload), {
status: lastError?.status || 502,
headers: { "Content-Type": "application/json" },
});
}

View File

@@ -11,6 +11,13 @@
* without real sockets. The ReadableStream wiring lives in `createRecoverableStream`.
*/
import { STREAM_RECOVERY } from "../config/constants.ts";
import {
createThroughputWatchdog,
ThroughputWatchdogError,
type ThroughputWatchdogOptions,
} from "./throughputWatchdog.ts";
export { ThroughputWatchdogError } from "./throughputWatchdog.ts";
/** Raised internally when an upstream stream ends without a terminal SSE marker. */
export class TruncatedStreamError extends Error {
@@ -123,7 +130,9 @@ const RETRYABLE_ERROR_NAMES = new Set(["TimeoutError", "BodyTimeoutError"]);
* the executor retry/failover loop, not here.
*/
export function isRetryableStreamError(error: unknown): boolean {
if (error instanceof TruncatedStreamError) return true;
if (error instanceof TruncatedStreamError || error instanceof ThroughputWatchdogError) {
return true;
}
if (!error || typeof error !== "object") return false;
const name = (error as { name?: unknown }).name;
@@ -289,6 +298,10 @@ export interface RecoverableStreamOptions {
maxContinuations?: number;
/** Observability hook fired on each continuation attempt. */
onContinue?: (attempt: number, assistantSoFar: string) => void;
/** Opt-in active-stream output-quality watchdog. Disabled when omitted. */
throughputWatchdog?: ThroughputWatchdogOptions;
/** Sanitized observability hook fired before the active attempt is aborted. */
onWatchdogAbort?: (error: ThroughputWatchdogError) => void;
}
/**
@@ -312,6 +325,7 @@ export function createRecoverableStream(
let retries = 0;
let finalized = false;
let cancelled = false;
let throughputWatchdog = createThroughputWatchdog(options.throughputWatchdog);
const runFinalize = () => {
if (finalized) return;
@@ -342,6 +356,7 @@ export function createRecoverableStream(
if (!next) return false;
reader = next.getReader();
holdback.discard(); // reuse the (still-uncommitted) buffer for the new attempt
throughputWatchdog = createThroughputWatchdog(options.throughputWatchdog);
return true;
};
@@ -519,6 +534,32 @@ export function createRecoverableStream(
if (value === undefined) continue;
const watchdogDecision = throughputWatchdog.observe(value);
if (watchdogDecision.abort) {
const error = new ThroughputWatchdogError();
options.onWatchdogAbort?.(error);
if (!holdback.committed && (await tryReopen(error))) continue;
if (holdback.committed) {
try {
await reader.cancel(error);
} catch {
// The active attempt may have closed while the watchdog was deciding.
}
if (await tryContinue(controller)) {
runFinalize();
controller.close();
return;
}
runFinalize();
controller.error(error);
return;
}
flushHeld(controller);
runFinalize();
controller.close();
return;
}
if (holdback.committed) {
emit(controller, value);
return;

View File

@@ -0,0 +1,175 @@
/**
* Deterministic quality watchdog for active SSE streams.
*
* Unlike the idle timeout, this only makes a decision after a warm-up period and
* a complete rolling window. Heartbeats/metadata and tool/reasoning phases do not
* count as assistant output (and tool/reasoning phases suspend judgement).
*/
export interface ThroughputWatchdogOptions {
enabled?: boolean;
warmupMs?: number;
windowMs?: number;
minUsefulBytesPerSecond?: number;
minUsefulBytes?: number;
now?: () => number;
}
export interface ThroughputWatchdogDecision {
abort: boolean;
reason?: "throughput_too_low";
usefulBytes: number;
rateBytesPerSecond: number;
protectedPhase: boolean;
}
export class ThroughputWatchdogError extends Error {
readonly code = "STREAM_THROUGHPUT_TOO_LOW";
constructor(message = "Upstream stream throughput remained below the configured minimum") {
super(message);
this.name = "ThroughputWatchdogError";
}
}
type ParsedEvent = { usefulBytes: number; protectedPhase: boolean };
function parseEvent(event: string): ParsedEvent {
const lines = event.split(/\r?\n/);
const eventName = lines
.find((line) => /^event:\s*/i.test(line))
?.replace(/^event:\s*/i, "")
.trim();
const data = lines
.filter((line) => /^data:\s*/i.test(line))
.map((line) => line.replace(/^data:\s*/i, "").trim())
.join("\n");
if (!data || data === "[DONE]") return { usefulBytes: 0, protectedPhase: false };
let payload: unknown;
try {
payload = JSON.parse(data);
} catch {
return { usefulBytes: 0, protectedPhase: false };
}
const record = payload as Record<string, unknown>;
const type = typeof record.type === "string" ? record.type : eventName;
if (type && /(reasoning|thinking|tool|function_call)/i.test(type)) {
return { usefulBytes: 0, protectedPhase: true };
}
const choices = Array.isArray(record.choices) ? record.choices : [];
let useful = "";
let protectedPhase = false;
for (const choice of choices) {
const delta = (choice as Record<string, unknown>).delta;
if (!delta || typeof delta !== "object") continue;
const deltaRecord = delta as Record<string, unknown>;
if (Array.isArray(deltaRecord.tool_calls) || deltaRecord.function_call) {
protectedPhase = true;
}
for (const key of ["content", "text"]) {
if (typeof deltaRecord[key] === "string") useful += deltaRecord[key] as string;
}
if (
typeof deltaRecord.reasoning_content === "string" ||
typeof deltaRecord.reasoning === "string"
) {
protectedPhase = true;
}
}
const outputText = typeof record.delta === "string" ? record.delta : undefined;
if (outputText) useful += outputText;
const nestedDelta = record.delta;
if (nestedDelta && typeof nestedDelta === "object") {
const nested = nestedDelta as Record<string, unknown>;
const nestedType = typeof nested.type === "string" ? nested.type : "";
if (/(reasoning|thinking|tool|function_call)/i.test(nestedType)) {
protectedPhase = true;
}
if (typeof nested.text === "string") useful += nested.text;
}
const contentBlock = record.content_block;
if (contentBlock && typeof contentBlock === "object") {
const blockType = (contentBlock as Record<string, unknown>).type;
if (typeof blockType === "string" && /(reasoning|thinking|tool_use)/i.test(blockType)) {
protectedPhase = true;
}
}
if (protectedPhase) useful = "";
return {
usefulBytes: useful ? new TextEncoder().encode(useful).byteLength : 0,
protectedPhase,
};
}
export class ThroughputWatchdog {
private readonly enabled: boolean;
private readonly warmupMs: number;
private readonly windowMs: number;
private readonly minimumRate: number;
private readonly minimumBytes: number;
private readonly now: () => number;
private startedAt: number | null = null;
private buffer = "";
private readonly decoder = new TextDecoder();
private samples: Array<{ at: number; bytes: number }> = [];
private protectedPhase = false;
constructor(options: ThroughputWatchdogOptions = {}) {
this.enabled = options.enabled === true;
this.warmupMs = Math.max(0, Math.floor(options.warmupMs ?? 30_000));
this.windowMs = Math.max(1, Math.floor(options.windowMs ?? 30_000));
this.minimumRate = Math.max(0, options.minUsefulBytesPerSecond ?? 1);
this.minimumBytes = Math.max(1, Math.floor(options.minUsefulBytes ?? 1));
this.now = options.now ?? (() => Date.now());
}
observe(chunk: Uint8Array | string): ThroughputWatchdogDecision {
const at = this.now();
if (this.startedAt === null) this.startedAt = at;
if (!this.enabled) return this.decision(false, 0);
this.buffer += typeof chunk === "string" ? chunk : this.decoder.decode(chunk, { stream: true });
const events = this.buffer.split(/\r?\n\r?\n/);
this.buffer = events.pop() ?? "";
let useful = 0;
for (const event of events) {
const parsed = parseEvent(event);
useful += parsed.usefulBytes;
if (parsed.protectedPhase) this.protectedPhase = true;
if (parsed.usefulBytes > 0) this.protectedPhase = false;
}
if (useful > 0) this.samples.push({ at, bytes: useful });
const cutoff = at - this.windowMs;
this.samples = this.samples.filter((sample) => sample.at >= cutoff);
const windowBytes = this.samples.reduce((sum, sample) => sum + sample.bytes, 0);
const elapsed = at - (this.startedAt ?? at);
const rate = windowBytes / Math.max(1, this.windowMs / 1000);
const ready = elapsed >= this.warmupMs + this.windowMs;
const measurable = windowBytes === 0 || windowBytes >= this.minimumBytes;
const abort = ready && !this.protectedPhase && measurable && rate < this.minimumRate;
return this.decision(abort, windowBytes, rate);
}
private decision(
abort: boolean,
usefulBytes: number,
rateBytesPerSecond = 0
): ThroughputWatchdogDecision {
return {
abort,
reason: abort ? "throughput_too_low" : undefined,
usefulBytes,
rateBytesPerSecond,
protectedPhase: this.protectedPhase,
};
}
}
export function createThroughputWatchdog(
options: ThroughputWatchdogOptions = {}
): ThroughputWatchdog {
return new ThroughputWatchdog(options);
}

View File

@@ -211,7 +211,8 @@ export function detectMalformedNonStream(resp: unknown): MalformedReason | null
// `choices`. Without this branch every non-streaming Claude response (incl. plain text)
// falls through to `empty_choices` → a false 502 (#5108, regression from #4942).
if (body.type === "message" && Array.isArray(body.content)) {
const hasOutput = (body.content as unknown[]).some((block) => {
const content = body.content as unknown[];
const hasOutput = content.some((block) => {
// A malformed/partial provider response could carry a null (or non-object)
// entry in `content`; guard before type-asserting so the detector never
// throws on `null.type` (that would crash the whole non-stream classifier).
@@ -229,16 +230,18 @@ export function detectMalformedNonStream(resp: unknown): MalformedReason | null
) {
return true;
}
// Extended-thinking block: valid when it carries visible thinking text OR a
// non-empty `signature` (cryptographic proof the thinking step ran, so it is a
// valid completion even when the thinking text is "").
if (
b.type === "thinking" &&
((typeof b.thinking === "string" && (b.thinking as string).length > 0) ||
(typeof b.signature === "string" && (b.signature as string).length > 0))
) {
return true;
}
// Extended-thinking block: valid structural output whenever the model
// entered the thinking phase, even with no visible thinking text and no
// `signature`. #9971: the Claude Code OAuth upstream can truncate long
// large-input+large-output generations around the ~3-min turn boundary,
// leaving a content-less thinking-only body whose final text (and, when
// cut mid-think, its signature) never arrived. The block's very presence
// is proof the turn produced output upstream, so it is a valid
// in-progress completion, NOT a genuinely empty terminal response.
// (Previously only a non-empty `thinking` text OR `signature` counted —
// #5108 — which misclassified these content-less bodies as empty_choices
// → 502.)
if (b.type === "thinking") return true;
// Redacted thinking and tool_use are valid structural output.
if (b.type === "redacted_thinking") return true;
if (b.type === "tool_use" && typeof b.id === "string" && (b.id as string).length > 0) {
@@ -246,7 +249,27 @@ export function detectMalformedNonStream(resp: unknown): MalformedReason | null
}
return false;
});
return hasOutput ? null : "empty_choices";
if (hasOutput) return null;
// No per-block output. Two distinct situations remain:
// 1) A block IS present but invalid (e.g. text:"", a lone "(empty response)"
// sentinel, or only null entries) — the model genuinely produced no
// usable output. That is a MALFORMED-200 empty_choices regardless of
// stop_reason (parity with the OpenAI content:"" path).
// 2) `content: []` — no block at all. Only a genuinely *terminal* response
// (a final stop_reason with no output) is empty_choices. #9971: a
// truncated / non-terminal body — the Claude Code OAuth upstream cutting
// a long generation mid-turn, or a content-less thinking-only stream
// that never emitted a terminal event — carries content:[] with no
// reachable end, so flagging it would turn an upstream truncation into a
// false 502. Require a terminal stop_reason before calling a block-less
// response genuinely empty.
if (content.length === 0) {
const stopReason = typeof body.stop_reason === "string" ? body.stop_reason : "";
const isTerminal = stopReason.length > 0;
return isTerminal ? "empty_choices" : null;
}
return "empty_choices";
}
// ── Chat Completions shape ──

View File

@@ -1096,9 +1096,12 @@ async function patchedFetch(
continue;
}
tagProxyUnreachable(error);
const originalMsg = error instanceof Error ? error.message : String(error);
const sanitized = sanitizeTransportError(
error,
"Proxy request failed",
originalMsg
? `Proxy request failed: ${originalMsg}`
: "Proxy request failed",
"PROXY_REQUEST_FAILED"
);
console.error(

View File

@@ -1640,7 +1640,8 @@ export function createSSEStream(options: StreamOptions = {}) {
(parsed.choices.length === 1 &&
parsed.choices[0]?.delta &&
typeof parsed.choices[0].delta === "object" &&
Object.keys(parsed.choices[0].delta).length === 0))
Object.keys(parsed.choices[0].delta).length === 0 &&
!parsed.choices[0]?.finish_reason))
) {
const emptyChoicesUsage = extractUsage(parsed) ?? parsed.usage;
if (hasValidUsage(emptyChoicesUsage)) {
@@ -1850,6 +1851,7 @@ export function createSSEStream(options: StreamOptions = {}) {
passthroughSawFinishReason = true;
}
if (isFinishChunk && passthroughHasToolCalls) {
toolFinishTime = now;
try {

View File

@@ -181,6 +181,7 @@
"check:known-symbols": "bun scripts/check/check-known-symbols.ts",
"check:route-guard-membership": "node --import tsx scripts/check/check-route-guard-membership.ts",
"check:test-discovery": "node scripts/check/check-test-discovery.mjs",
"check:forgotten-sibling-tests": "node scripts/check/check-forgotten-sibling-tests.mjs",
"check:mutation-test-coverage": "node scripts/check/check-mutation-test-coverage.mjs --strict",
"check:complexity": "node scripts/check/check-complexity.mjs",
"check:dead-code": "node scripts/check/check-dead-code.mjs",

View File

@@ -0,0 +1,293 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { globSync } from "tinyglobby";
import { resolveImport } from "../quality/build-test-impact-map.mjs";
const DEFAULT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const SOURCE_ROOTS = ["src/", "open-sse/", "bin/"];
const SOURCE_GLOBS = [
"src/**/*.{ts,tsx,mts,js,mjs}",
"open-sse/**/*.{ts,tsx,mts,js,mjs}",
"bin/**/*.{ts,tsx,mts,js,mjs}",
];
const IGNORE = [
"**/__tests__/**",
"**/*.test.*",
"**/*.spec.*",
"**/fixtures/**",
"**/generated/**",
];
const STATIC_IMPORT_RE =
/(?:import|export)[^'"()]*from\s*['"]([^'"]+)['"]|require\(\s*['"]([^'"]+)['"]\s*\)/g;
const DYNAMIC_IMPORT_RE = /import\(\s*['"]([^'"]+)['"]\s*\)/g;
const TEST_MASK_RE =
/^\+.*(?:\b(?:it|test|describe)\.(?:skip|todo)\b|\b(?:xit|xtest|xdescribe)\s*\()/;
const REFERENCE_RE = /^(?:#\d+|https:\/\/github\.com\/[^/]+\/[^/]+\/(?:issues|pull)\/\d+)$/;
function normalize(file) {
return file.split(path.sep).join("/");
}
function isProduction(file) {
return (
SOURCE_ROOTS.some((root) => file.startsWith(root)) &&
!IGNORE.some((pattern) => {
const token = pattern.replaceAll("**/", "").replaceAll("/**", "").replaceAll("*", "");
return token && file.includes(token);
})
);
}
function isBarrel(file, code) {
return /(?:^|\/)index\.[cm]?[jt]sx?$/.test(file) && /\bexport\s+(?:\*|\{)/.test(code);
}
function importEdges(root) {
const edges = [];
const files = globSync(SOURCE_GLOBS, { cwd: root, absolute: true, ignore: IGNORE });
for (const absolute of files) {
const consumer = normalize(path.relative(root, absolute));
const code = fs.readFileSync(absolute, "utf8");
for (const match of code.matchAll(STATIC_IMPORT_RE)) {
const resolved = resolveImport(match[1] || match[2], absolute, root);
if (resolved) {
edges.push({
module: normalize(path.relative(root, resolved)),
consumer,
kind: isBarrel(consumer, code) ? "barrel" : "static",
});
}
}
for (const match of code.matchAll(DYNAMIC_IMPORT_RE)) {
const resolved = resolveImport(match[1], absolute, root);
if (resolved) {
edges.push({
module: normalize(path.relative(root, resolved)),
consumer,
kind: "dynamic-import",
});
}
}
}
return edges.sort((a, b) =>
`${a.module}\0${a.consumer}\0${a.kind}`.localeCompare(`${b.module}\0${b.consumer}\0${b.kind}`)
);
}
export function validateAllowlist(value) {
const entries = Array.isArray(value) ? value : value?.entries;
if (!Array.isArray(entries))
throw new Error("forgotten-sibling allowlist must contain an entries array");
return entries.map((entry, index) => {
for (const field of ["consumer", "candidateTest", "rationale", "reference"]) {
if (typeof entry?.[field] !== "string" || !entry[field].trim()) {
throw new Error(`forgotten-sibling allowlist entry ${index} requires ${field}`);
}
}
if (entry.rationale.trim().length < 20) {
throw new Error(`forgotten-sibling allowlist entry ${index} rationale must be specific`);
}
if (!REFERENCE_RE.test(entry.reference.trim())) {
throw new Error(
`forgotten-sibling allowlist entry ${index} reference must be a GitHub issue or PR`
);
}
return {
consumer: normalize(entry.consumer.trim()),
candidateTest: normalize(entry.candidateTest.trim()),
rationale: entry.rationale.trim(),
reference: entry.reference.trim(),
};
});
}
export function analyzeForgottenSiblingTests({
root = DEFAULT_ROOT,
changedEntries,
impactMap,
allowlist,
changedSymbolsByFile = {},
addedTestLines = [],
}) {
const changed = new Map(changedEntries.map((entry) => [normalize(entry.file), entry.status]));
const changedModules = [...changed.keys()].filter(isProduction).sort();
const maskingAdded = addedTestLines.some((line) => TEST_MASK_RE.test(line));
const allow = new Map(
allowlist.map((entry) => [`${entry.consumer}\0${entry.candidateTest}`, entry])
);
const findings = [];
const diagnostics = [];
const suppressed = [];
const maskingRisks = [];
for (const edge of importEdges(root)) {
if (!changedModules.includes(edge.module)) continue;
const tests = [...new Set(impactMap.sources?.[edge.consumer] || [])].sort();
if (edge.kind !== "static") {
diagnostics.push({
changedModule: edge.module,
consumer: edge.consumer,
kind: edge.kind,
message: `${edge.kind} resolution is advisory and never blocks`,
});
continue;
}
for (const candidateTest of tests) {
const status = changed.get(candidateTest);
const masking = status === "D" || (status && maskingAdded);
if (masking) {
maskingRisks.push({
changedModule: edge.module,
consumer: edge.consumer,
candidateTest,
reason:
status === "D"
? "candidate sibling test was deleted"
: "candidate sibling test adds skip/todo masking",
});
continue;
}
if (status) continue;
const finding = {
changedModule: edge.module,
changedSymbols: [...(changedSymbolsByFile[edge.module] || [])].sort(),
consumer: edge.consumer,
candidateTest,
reason: "candidate sibling test is absent from the PR diff",
};
const exception = allow.get(`${edge.consumer}\0${candidateTest}`);
if (exception) suppressed.push({ ...finding, exception });
else findings.push(finding);
}
}
return { mode: "advisory", findings, diagnostics, suppressed, maskingRisks };
}
function arg(name, fallback = "") {
const index = process.argv.indexOf(name);
return index >= 0 && process.argv[index + 1] ? process.argv[index + 1] : fallback;
}
function git(root, args) {
return execFileSync("git", args, { cwd: root, encoding: "utf8" });
}
function changedEntries(root, base) {
return git(root, ["diff", "--name-status", "--diff-filter=ACMRD", `${base}...HEAD`])
.trim()
.split(/\r?\n/)
.filter(Boolean)
.map((line) => {
const [status, ...files] = line.split("\t");
return { status: status[0], file: files.at(-1) };
});
}
function changedSymbols(root, base, entries) {
const result = {};
const declaration =
/^\+\s*(?:export\s+)?(?:async\s+)?(?:function|class|const|let|var|interface|type|enum)\s+([A-Za-z_$][\w$]*)/;
for (const entry of entries.filter(({ file }) => isProduction(file))) {
const diff = git(root, ["diff", "--unified=0", `${base}...HEAD`, "--", entry.file]);
result[entry.file] = [
...new Set(
diff
.split(/\r?\n/)
.map((line) => line.match(declaration)?.[1])
.filter(Boolean)
),
];
}
return result;
}
function markdown(result, base) {
const lines = [
"## Forgotten sibling tests (advisory)",
"",
`Base: \`${base}\``,
`Unallowlisted findings: ${result.findings.length}`,
`Reviewed exceptions: ${result.suppressed.length}`,
`Resolution diagnostics: ${result.diagnostics.length}`,
`Masking/deletion risks (owned by blocking sibling gates): ${result.maskingRisks.length}`,
"",
];
if (result.findings.length) {
lines.push("### Candidate tests absent from this diff", "");
for (const item of result.findings) {
const symbol = item.changedSymbols.length ? ` (${item.changedSymbols.join(", ")})` : "";
lines.push(
`- \`${item.changedModule}\`${symbol} -> \`${item.consumer}\` -> \`${item.candidateTest}\``
);
}
lines.push("", "> Report-only calibration: these findings do not fail the job.", "");
}
for (const [heading, items] of [
["Resolution diagnostics", result.diagnostics],
["Test masking/deletion risks", result.maskingRisks],
]) {
if (!items.length) continue;
lines.push(`### ${heading}`, "");
for (const item of items)
lines.push(
`- \`${item.changedModule}\` -> \`${item.consumer}\`${item.candidateTest ? ` -> \`${item.candidateTest}\`` : ""}: ${item.reason || item.message}`
);
lines.push("");
}
return `${lines.join("\n")}\n`;
}
function main() {
const root = DEFAULT_ROOT;
const base = arg(
"--base",
process.env.GITHUB_BASE_SHA ||
(process.env.GITHUB_BASE_REF ? `origin/${process.env.GITHUB_BASE_REF}` : "HEAD~1")
);
const mapPath = arg("--impact-map", path.join(root, "config/quality/test-impact-map.json"));
const allowlistPath = arg(
"--allowlist",
path.join(root, "config/quality/forgotten-sibling-allowlist.json")
);
const summaryPath = arg("--summary-file", "");
const jsonPath = arg("--json-file", "");
const entries = changedEntries(root, base);
const impactMap = JSON.parse(fs.readFileSync(mapPath, "utf8"));
const allowlist = validateAllowlist(JSON.parse(fs.readFileSync(allowlistPath, "utf8")));
const addedTestLines = git(root, ["diff", "--unified=0", `${base}...HEAD`, "--", "tests/"])
.split(/\r?\n/)
.filter((line) => line.startsWith("+") && !line.startsWith("+++"));
const result = analyzeForgottenSiblingTests({
root,
changedEntries: entries,
impactMap,
allowlist,
changedSymbolsByFile: changedSymbols(root, base, entries),
addedTestLines,
});
const report = markdown(result, base);
process.stdout.write(report);
for (const [target, contents] of [
[summaryPath, report],
[jsonPath, `${JSON.stringify(result, null, 2)}\n`],
]) {
if (!target) continue;
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, contents);
}
}
if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] || "")) {
try {
main();
} catch (error) {
console.error(
`forgotten-sibling-tests: ${error instanceof Error ? error.message : String(error)}`
);
process.exit(1);
}
}

View File

@@ -9,11 +9,11 @@ const IMPORT_RE =
/(?:import|export)[^'"]*from\s*['"]([^'"]+)['"]|require\(\s*['"]([^'"]+)['"]\s*\)|import\(\s*['"]([^'"]+)['"]\s*\)/g;
const EXTS = [".ts", ".tsx", ".mts", ".js", ".mjs"];
function resolveImport(spec, fromFile) {
export function resolveImport(spec, fromFile, root = ROOT) {
let base;
if (spec.startsWith("@/")) base = path.join(ROOT, "src", spec.slice(2));
if (spec.startsWith("@/")) base = path.join(root, "src", spec.slice(2));
else if (spec.startsWith("@omniroute/open-sse"))
base = path.join(ROOT, "open-sse", spec.replace(/^@omniroute\/open-sse\/?/, ""));
base = path.join(root, "open-sse", spec.replace(/^@omniroute\/open-sse\/?/, ""));
else if (spec.startsWith(".")) base = path.resolve(path.dirname(fromFile), spec);
else return null;
for (const e of EXTS) {
@@ -26,7 +26,7 @@ function resolveImport(spec, fromFile) {
return fs.existsSync(base) && fs.statSync(base).isFile() ? base : null;
}
function sourceDepsOf(entry) {
export function sourceDepsOf(entry, root = ROOT) {
const seen = new Set();
const stack = [entry];
const sources = new Set();
@@ -43,9 +43,9 @@ function sourceDepsOf(entry) {
for (const m of code.matchAll(IMPORT_RE)) {
const spec = m[1] || m[2] || m[3];
if (!spec) continue;
const r = resolveImport(spec, f);
const r = resolveImport(spec, f, root);
if (!r) continue;
const rel = path.relative(ROOT, r);
const rel = path.relative(root, r);
if (SRC_ROOTS.some((s) => rel.startsWith(s + path.sep))) sources.add(rel);
stack.push(r);
}
@@ -59,27 +59,35 @@ function sourceDepsOf(entry) {
// e2e/integration tests, which can't run under node:test (they 99-false-failed before).
// Mirror EXACTLY the package.json `test:unit` / `test:unit:ci` globs (incl. memory,
// usage, combo, dashboard, serial, and *.test.mjs). Drift here → false __RUN_ALL__.
const testFiles = globSync(
[
"tests/unit/*.test.ts",
"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts",
"tests/unit/**/*.test.mjs",
"tests/unit/dashboard/**/*.test.ts",
// Quarentena serial (P0.3): também são node:test — a TIA precisa mapeá-los.
"tests/unit/serial/**/*.test.ts",
],
{ cwd: ROOT, absolute: true }
);
const map = {};
for (const tf of testFiles) {
const relTest = path.relative(ROOT, tf);
for (const src of sourceDepsOf(tf)) {
(map[src] ||= []).push(relTest);
export function buildTestImpactMap(root = ROOT) {
const testFiles = globSync(
[
"tests/unit/*.test.ts",
"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts",
"tests/unit/**/*.test.mjs",
"tests/unit/dashboard/**/*.test.ts",
// Quarentena serial (P0.3): também são node:test — a TIA precisa mapeá-los.
"tests/unit/serial/**/*.test.ts",
],
{ cwd: root, absolute: true }
);
const map = {};
for (const tf of testFiles) {
const relTest = path.relative(root, tf);
for (const src of sourceDepsOf(tf, root)) {
(map[src] ||= []).push(relTest);
}
}
for (const k of Object.keys(map)) map[k].sort();
return { generatedFrom: "import-graph", sources: map, testFileCount: testFiles.length };
}
if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] || "")) {
const result = buildTestImpactMap();
const { testFileCount, ...map } = result;
const out = path.join(ROOT, "config/quality/test-impact-map.json");
fs.writeFileSync(out, JSON.stringify(map, null, 2) + "\n");
console.log(
`test-impact-map: ${Object.keys(map.sources).length} source files mapped from ${testFileCount} test files`
);
}
for (const k of Object.keys(map)) map[k].sort();
const out = path.join(ROOT, "config/quality/test-impact-map.json");
fs.writeFileSync(out, JSON.stringify({ generatedFrom: "import-graph", sources: map }, null, 2) + "\n");
console.log(
`test-impact-map: ${Object.keys(map).length} source files mapped from ${testFiles.length} test files`
);

View File

@@ -589,11 +589,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",

View File

@@ -0,0 +1,104 @@
"use client";
import { useState } from "react";
import { useTranslations } from "next-intl";
import type { MemorySettingsExtended } from "@/shared/schemas/memory";
type Props = {
settings: MemorySettingsExtended;
onSave: (updates: Partial<MemorySettingsExtended>) => Promise<boolean>;
saving?: boolean;
};
function normalizeBaseUrl(value: string): string | null {
const trimmed = value.trim();
if (!trimmed) return null;
try {
const url = new URL(trimmed);
if (
(url.protocol !== "http:" && url.protocol !== "https:") ||
url.username ||
url.password ||
url.search ||
url.hash
) {
return null;
}
return trimmed.replace(/\/+$/, "");
} catch {
return null;
}
}
export default function CustomEmbeddingEndpointFields({ settings, onSave, saving }: Props) {
const t = useTranslations("memory");
const [baseUrl, setBaseUrl] = useState(settings.customBaseUrl ?? "");
const [modelId, setModelId] = useState(settings.customModelId ?? "");
const [error, setError] = useState<string | null>(null);
const save = async () => {
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
const normalizedModelId = modelId.trim() || null;
if ((baseUrl.trim() || normalizedModelId) && (!normalizedBaseUrl || !normalizedModelId)) {
setError(t("embedding.customEndpointInvalid"));
return;
}
setError(null);
const saved = await onSave({
customBaseUrl: normalizedBaseUrl,
customModelId: normalizedModelId,
});
if (!saved) {
setError(t("embedding.customEndpointSaveFailed"));
return;
}
setBaseUrl(normalizedBaseUrl ?? "");
setModelId(normalizedModelId ?? "");
};
return (
<div className="mt-4 pt-4 border-t border-border/60 space-y-3">
<div>
<label className="block text-sm font-medium text-text-main mb-1">
{t("embedding.customBaseUrlLabel")}
</label>
<input
value={baseUrl}
onChange={(event) => setBaseUrl(event.target.value)}
placeholder="http://localhost:8000/v1"
disabled={saving}
data-testid="embedding-custom-base-url"
className="w-full px-3 py-2 rounded-lg bg-background border border-border text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-text-main mb-1">
{t("embedding.customModelIdLabel")}
</label>
<input
value={modelId}
onChange={(event) => setModelId(event.target.value)}
placeholder="my-embedding-model"
disabled={saving}
data-testid="embedding-custom-model-id"
className="w-full px-3 py-2 rounded-lg bg-background border border-border text-sm"
/>
</div>
<p className="text-xs text-text-muted">{t("embedding.customEndpointHelp")}</p>
{error && (
<p role="alert" className="text-xs text-red-400">
{error}
</p>
)}
<button
type="button"
onClick={() => void save()}
disabled={saving}
data-testid="embedding-custom-save"
className="px-3 py-2 rounded-lg bg-violet-500 text-white text-sm disabled:opacity-50"
>
{t("embedding.customEndpointSave")}
</button>
</div>
);
}

View File

@@ -3,6 +3,7 @@
import { useTranslations } from "next-intl";
import type { MemorySettingsExtended } from "@/shared/schemas/memory";
import type { EmbeddingProviderListing } from "@/lib/memory/embedding/types";
import CustomEmbeddingEndpointFields from "./CustomEmbeddingEndpointFields";
interface Props {
settings: MemorySettingsExtended;
@@ -101,10 +102,11 @@ export default function EmbeddingSourceSelector({ settings, providers, onSave, s
<option key={m.id} value={m.id}>
{m.name} ({m.dimensions ? `${m.dimensions}d` : "?"})
</option>
)),
))
)}
</select>
)}
<CustomEmbeddingEndpointFields settings={settings} onSave={onSave} saving={saving} />
</div>
)}

View File

@@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useDisplayBaseUrl } from "@/shared/hooks";
import { FreeProviderOnboardingCard } from "./steps/FreeProviderOnboardingCard";
import { TierTour } from "./steps/TierTour";
const STEP_IDS = ["welcome", "tiers", "security", "provider", "test", "done"];
@@ -357,6 +358,12 @@ export default function OnboardingWizard() {
{currentStep.id === "provider" && (
<div className="space-y-4">
<p className="text-sm text-text-muted text-center">{t("providerDesc")}</p>
<FreeProviderOnboardingCard />
<div className="flex items-center gap-3 text-[11px] text-text-muted">
<span className="h-px flex-1 bg-white/10" />
<span>{t("freeProviders.orUseApiKey")}</span>
<span className="h-px flex-1 bg-white/10" />
</div>
<div className="grid grid-cols-3 gap-2">
{COMMON_PROVIDERS.map((p) => (
<button

View File

@@ -0,0 +1,179 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { useTranslations } from "next-intl";
interface FreeProviderOption {
id: string;
name: string;
website: string;
caution: string;
defaultModel?: string;
}
interface SetupResult {
providerId: string;
status: "created" | "skipped" | "failed";
reason?: string;
}
async function readJson(response: Response): Promise<Record<string, unknown>> {
return (await response.json().catch(() => ({}))) as Record<string, unknown>;
}
export function FreeProviderOnboardingCard() {
const t = useTranslations("onboarding.freeProviders");
const [providers, setProviders] = useState<FreeProviderOption[]>([]);
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [confirmed, setConfirmed] = useState(false);
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState("");
const [results, setResults] = useState<SetupResult[]>([]);
useEffect(() => {
let active = true;
void fetch("/api/providers/free-onboarding")
.then(async (response) => {
const data = await readJson(response);
if (!response.ok) throw new Error("load-failed");
const options = Array.isArray(data.providers)
? (data.providers as FreeProviderOption[])
: [];
if (!active) return;
setProviders(options);
setSelectedIds(options.map((provider) => provider.id).sort());
})
.catch(() => {
if (active) setError(t("loadFailed"));
})
.finally(() => {
if (active) setLoading(false);
});
return () => {
active = false;
};
}, [t]);
const failedIds = useMemo(
() => results.filter((result) => result.status === "failed").map((result) => result.providerId),
[results]
);
const toggleProvider = (providerId: string) => {
setSelectedIds((current) =>
current.includes(providerId)
? current.filter((id) => id !== providerId)
: [...current, providerId].sort()
);
setConfirmed(false);
};
const submit = async (providerIds = selectedIds) => {
setSubmitting(true);
setError("");
try {
const response = await fetch("/api/providers/free-onboarding", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ providerIds, confirmed: true }),
});
const data = await readJson(response);
if (!response.ok) throw new Error("setup-failed");
setResults(Array.isArray(data.results) ? (data.results as SetupResult[]) : []);
} catch {
setError(t("setupFailed"));
} finally {
setSubmitting(false);
}
};
if (loading) return <p className="text-xs text-text-muted">{t("loading")}</p>;
if (providers.length === 0 && !error) {
return <p className="text-xs text-text-muted">{t("alreadyConfigured")}</p>;
}
return (
<section className="space-y-3 rounded-xl border border-primary/20 bg-primary/5 p-4">
<div>
<h3 className="text-sm font-semibold text-text-main">{t("title")}</h3>
<p className="mt-1 text-xs leading-relaxed text-text-muted">{t("description")}</p>
</div>
<div className="max-h-48 space-y-2 overflow-y-auto pr-1">
{providers.map((provider) => (
<label
key={provider.id}
className="flex cursor-pointer items-start gap-3 rounded-lg border border-white/10 bg-white/[0.03] p-3"
>
<input
type="checkbox"
checked={selectedIds.includes(provider.id)}
onChange={() => toggleProvider(provider.id)}
className="mt-1 accent-primary"
/>
<span className="min-w-0 flex-1">
<span className="block text-xs font-medium text-text-main">{provider.name}</span>
<span className="mt-1 block text-[11px] leading-relaxed text-amber-300/90">
{provider.caution}
</span>
<a
href={provider.website}
target="_blank"
rel="noreferrer"
className="mt-1 inline-block text-[11px] text-primary hover:underline"
onClick={(event) => event.stopPropagation()}
>
{t("reviewProviderSite")}
</a>
</span>
</label>
))}
</div>
<label className="flex cursor-pointer items-start gap-2 text-xs text-text-muted">
<input
data-testid="free-provider-confirmation"
type="checkbox"
checked={confirmed}
onChange={(event) => setConfirmed(event.target.checked)}
className="mt-0.5 accent-primary"
/>
<span>{t("confirmation")}</span>
</label>
{error && <p className="text-xs text-red-400">{error}</p>}
{results.length > 0 && (
<ul className="space-y-1 text-xs text-text-muted">
{results.map((result) => (
<li key={result.providerId}>
{result.providerId}: {t(`result.${result.status}`)}
</li>
))}
</ul>
)}
<div className="flex flex-wrap gap-2">
<button
data-testid="setup-free-providers"
type="button"
disabled={!confirmed || selectedIds.length === 0 || submitting}
onClick={() => void submit()}
className="rounded-lg bg-primary px-4 py-2 text-xs font-medium text-white disabled:cursor-not-allowed disabled:opacity-50"
>
{submitting ? t("settingUp") : t("setupSelected")}
</button>
{failedIds.length > 0 && (
<button
type="button"
disabled={submitting}
onClick={() => void submit(failedIds)}
className="rounded-lg border border-white/10 px-4 py-2 text-xs text-text-main disabled:opacity-50"
>
{t("retryFailed")}
</button>
)}
</div>
</section>
);
}

View File

@@ -1,10 +1,16 @@
"use client";
import { useState, useEffect, useCallback, useMemo } from "react";
import { useTranslations } from "next-intl";
import { useState, useEffect, useCallback } from "react";
import { useLocale, useTranslations } from "next-intl";
import { useSearchParams } from "next/navigation";
import Link from "next/link";
import { Card } from "@/shared/components";
import {
firstProviderConnectionId,
providerConnectionsRequestUrl,
type RadarSetupConnection,
} from "@/lib/radar/setupConnections";
import type { RadarLocalizedText } from "@/lib/radar/feedSchema";
// ---------------------------------------------------------------------------
// Types
@@ -14,17 +20,16 @@ import { Card } from "@/shared/components";
* Localized text: either a plain string or an {en, pt?} object.
* The renderer resolves the best locale with EN fallback (D25 compat).
*/
type LocalizedText = string | { en: string; pt?: string };
interface SetupInfo {
keyUrl: string | null;
steps: LocalizedText[];
steps: RadarLocalizedText[];
}
interface ProviderSetupData {
provider: string;
setup: SetupInfo | null;
configured: boolean;
connectionId: string | null;
}
// ---------------------------------------------------------------------------
@@ -32,9 +37,9 @@ interface ProviderSetupData {
// ---------------------------------------------------------------------------
/** Resolve a LocalizedText to a display string. */
function resolveText(text: LocalizedText, locale: string): string {
function resolveText(text: RadarLocalizedText, locale: string): string {
if (typeof text === "string") return text;
if (locale === "pt" && text.pt) return text.pt;
if (locale.toLowerCase().startsWith("pt") && text.pt) return text.pt;
return text.en;
}
@@ -44,9 +49,9 @@ function resolveText(text: LocalizedText, locale: string): string {
export default function RadarSetupPage() {
const t = useTranslations("radarSetupPage");
const locale = useLocale();
const searchParams = useSearchParams();
const provider = searchParams.get("provider");
const locale = "en"; // Could be derived from next-intl locale later
const [setupData, setSetupData] = useState<ProviderSetupData | null>(null);
const [loading, setLoading] = useState(true);
@@ -63,18 +68,25 @@ export default function RadarSetupPage() {
async function load() {
try {
const res = await fetch("/api/radar/catalog");
const [res, connectionsRes] = await Promise.all([
fetch("/api/radar/catalog"),
fetch(providerConnectionsRequestUrl(provider)),
]);
if (res.status === 404) {
setError(t("flagDisabled"));
setLoading(false);
return;
}
if (!res.ok) throw new Error(`HTTP ${res.status}`);
if (!connectionsRes.ok) throw new Error(`HTTP ${connectionsRes.status}`);
const data = await res.json();
const connectionsData = (await connectionsRes.json()) as {
connections?: RadarSetupConnection[];
};
// Find ALL entries for this provider and extract setup from the first one that has it
const providerEntries = data.entries.filter(
(e: { provider: string }) => e.provider === provider,
(e: { provider: string }) => e.provider === provider
);
if (providerEntries.length === 0) {
@@ -85,17 +97,19 @@ export default function RadarSetupPage() {
// Find setup info from feed entries (they carry the setup field)
const entryWithSetup = providerEntries.find(
(e: { setup?: SetupInfo | null }) => e.setup && (e.setup.steps.length > 0 || e.setup.keyUrl),
(e: { setup?: SetupInfo | null }) =>
e.setup && (e.setup.steps.length > 0 || e.setup.keyUrl)
);
// Check if provider is configured (has connections)
// We infer this from whether the provider exists in the catalog at all
// The actual connection check would need a separate API — for now we show
// the guide regardless
const connectionId = firstProviderConnectionId(
Array.isArray(connectionsData.connections) ? connectionsData.connections : [],
provider
);
setSetupData({
provider,
setup: entryWithSetup?.setup ?? null,
configured: false, // Will be enriched when connection-status API is available
configured: connectionId !== null,
connectionId,
});
} catch (err) {
setError(err instanceof Error ? err.message : t("loadFailed"));
@@ -109,15 +123,11 @@ export default function RadarSetupPage() {
// Test connection — uses the EXISTING connection-test endpoint
const handleTestConnection = useCallback(async () => {
if (!provider) return;
if (!setupData?.connectionId) return;
setTesting(true);
setTestResult(null);
try {
// The existing test endpoint is POST /api/providers/[id]/test
// We need the connection ID — for now we use the provider ID as a proxy.
// In a full implementation, the setup page would list connections for
// this provider and test each one. Here we test the first connection.
const res = await fetch(`/api/providers/${encodeURIComponent(provider)}/test`, {
const res = await fetch(`/api/providers/${encodeURIComponent(setupData.connectionId)}/test`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
@@ -136,7 +146,7 @@ export default function RadarSetupPage() {
} finally {
setTesting(false);
}
}, [provider, t]);
}, [setupData?.connectionId, t]);
if (!provider) {
return (
@@ -165,9 +175,7 @@ export default function RadarSetupPage() {
<p className="text-sm text-text-muted mt-1">{t("setupSubtitle")}</p>
</div>
{error && (
<div className="p-3 rounded-lg bg-red-500/10 text-red-400 text-sm">{error}</div>
)}
{error && <div className="p-3 rounded-lg bg-red-500/10 text-red-400 text-sm">{error}</div>}
{loading ? (
<div className="flex items-center justify-center min-h-[200px]">
@@ -248,15 +256,13 @@ export default function RadarSetupPage() {
<div className="flex items-center gap-3">
<button
onClick={handleTestConnection}
disabled={testing}
disabled={testing || !setupData.connectionId}
className="px-4 py-2 text-sm font-medium rounded-lg border border-violet-500 text-violet-400 hover:bg-violet-500/10 transition-colors disabled:opacity-50"
>
{testing ? t("testing") : t("testButton")}
</button>
{testResult && (
<span
className={`text-sm ${testResult.ok ? "text-green-400" : "text-red-400"}`}
>
<span className={`text-sm ${testResult.ok ? "text-green-400" : "text-red-400"}`}>
{testResult.message}
</span>
)}

View File

@@ -0,0 +1,197 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import { Card, ModelSelectField, Toggle } from "@/shared/components";
import type { ApiModel } from "@/shared/components/ModelSelectField";
import {
MODALITY_BRIDGE_DEFAULTS,
resolveAudioBridgeRuntimeSettings,
} from "@/shared/constants/modalityBridgeDefaults";
import ModalityBridgeAudioTestButton from "./ModalityBridgeAudioTestButton";
import ModalityBridgeStatsRow from "./ModalityBridgeStatsRow";
interface AudioState {
modalityBridgeAudioEnabled: boolean;
modalityBridgeAudioModel: string;
modalityBridgeAudioTimeout: number;
modalityBridgeAudioMaxClips: number;
}
function fromApi(data: Record<string, unknown>): AudioState {
const runtime = resolveAudioBridgeRuntimeSettings(data);
return {
modalityBridgeAudioEnabled: runtime.enabled,
modalityBridgeAudioModel: runtime.model,
modalityBridgeAudioTimeout: runtime.timeoutMs,
modalityBridgeAudioMaxClips: runtime.maxClips,
};
}
function asSettingsRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
}
function clampNumber(raw: string, min: number, max: number, fallback: number): number {
const parsed = Number.parseInt(raw, 10);
return Math.min(max, Math.max(min, Number.isFinite(parsed) ? parsed : fallback));
}
export default function ModalityBridgeAudioTab() {
const t = useTranslations("settings");
const [settings, setSettings] = useState<AudioState | null>(null);
const isSttModel = useCallback(
(model: ApiModel) => model.type === "audio" && model.subtype === "transcription",
[]
);
useEffect(() => {
let cancelled = false;
fetch("/api/settings")
.then((response) => (response.ok ? response.json() : null))
.then((data: unknown) => {
if (!cancelled) setSettings(fromApi(asSettingsRecord(data)));
})
.catch(() => {
if (!cancelled) setSettings(fromApi({}));
});
return () => {
cancelled = true;
};
}, []);
const update = async (patch: Partial<AudioState>) => {
try {
const response = await fetch("/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(patch),
});
if (response.ok) {
setSettings((previous) => (previous ? { ...previous, ...patch } : previous));
}
} catch (error) {
console.error("Failed to update Audio Bridge settings:", error);
}
};
if (!settings) return null;
const setLocal = (patch: Partial<AudioState>) => {
setSettings((previous) => (previous ? { ...previous, ...patch } : previous));
};
const commitNumber = (
key: "modalityBridgeAudioTimeout" | "modalityBridgeAudioMaxClips",
raw: string,
min: number,
max: number,
fallback: number
) => {
const value = clampNumber(raw, min, max, fallback);
setLocal({ [key]: value });
void update({ [key]: value });
};
return (
<Card
title={t("modalityBridgeAudioTitle")}
subtitle={t("modalityBridgeAudioDesc")}
icon="graphic_eq"
>
<div className="space-y-4">
<Toggle
checked={settings.modalityBridgeAudioEnabled}
onChange={(checked) => void update({ modalityBridgeAudioEnabled: checked })}
label={t("modalityBridgeAudioEnabled")}
description={t("modalityBridgeAudioEnabledDesc")}
/>
<ModelSelectField
label={t("modalityBridgeAudioModel")}
value={settings.modalityBridgeAudioModel}
placeholder={t("modalityBridgeAudioModelAuto")}
allowEmpty
modelFilter={isSttModel}
modelSource="catalog"
onChange={(value) => void update({ modalityBridgeAudioModel: value })}
className="text-sm"
/>
<details className="rounded-control border border-border p-3">
<summary className="cursor-pointer text-sm font-medium">
{t("modalityBridgeAdvanced")}
</summary>
<div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<NumberField
testId="modality-bridge-audio-timeout"
label={t("modalityBridgeTimeoutMs")}
min={1000}
max={300000}
value={settings.modalityBridgeAudioTimeout}
onChange={(value) => setLocal({ modalityBridgeAudioTimeout: value })}
onBlur={(raw) =>
commitNumber(
"modalityBridgeAudioTimeout",
raw,
1000,
300000,
MODALITY_BRIDGE_DEFAULTS.audioTimeoutMs
)
}
/>
<NumberField
testId="modality-bridge-audio-max-clips"
label={t("modalityBridgeAudioMaxClips")}
min={1}
max={10}
value={settings.modalityBridgeAudioMaxClips}
onChange={(value) => setLocal({ modalityBridgeAudioMaxClips: value })}
onBlur={(raw) =>
commitNumber(
"modalityBridgeAudioMaxClips",
raw,
1,
10,
MODALITY_BRIDGE_DEFAULTS.audioMaxClips
)
}
/>
</div>
</details>
<ModalityBridgeStatsRow kind="audio" />
<ModalityBridgeAudioTestButton />
</div>
</Card>
);
}
interface NumberFieldProps {
testId: string;
label: string;
min: number;
max: number;
value: number;
onChange: (value: number) => void;
onBlur: (raw: string) => void;
}
function NumberField({ testId, label, min, max, value, onChange, onBlur }: NumberFieldProps) {
return (
<label className="block text-sm font-medium">
{label}
<input
type="number"
data-testid={testId}
min={min}
max={max}
value={value}
onChange={(event) => onChange(Number.parseInt(event.currentTarget.value, 10) || 0)}
onBlur={(event) => onBlur(event.currentTarget.value)}
className="mt-1 w-full rounded-control border border-border bg-surface px-3 py-2 text-sm"
/>
</label>
);
}

View File

@@ -0,0 +1,104 @@
"use client";
import { useState } from "react";
import { useTranslations } from "next-intl";
const SAMPLE_INPUT = {
model: "modality-bridge/self-test",
messages: [
{
role: "user",
content: [
{ type: "text", text: "Transcribe this audio clip." },
{
type: "input_audio",
input_audio: {
data: "UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA=",
format: "wav",
},
},
],
},
],
};
const DISABLED_GUARDRAILS = [
"vision-bridge",
"pii-masker",
"prompt-injection",
"credential-masker",
];
function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" ? (value as Record<string, unknown>) : null;
}
function findAudioMeta(value: unknown): Record<string, unknown> | null {
const body = asRecord(value);
if (!Array.isArray(body?.results)) return null;
for (const entry of body.results) {
const result = asRecord(entry);
if (result?.guardrail === "audio-bridge") return asRecord(result.meta);
}
return null;
}
function readErrorMessage(value: unknown): string | null {
const error = asRecord(asRecord(value)?.error);
return typeof error?.message === "string" ? error.message : null;
}
export default function ModalityBridgeAudioTestButton() {
const t = useTranslations("settings");
const [running, setRunning] = useState(false);
const [result, setResult] = useState<string | null>(null);
const runTest = async () => {
setRunning(true);
setResult(null);
try {
const response = await fetch("/api/guardrails/test", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ input: SAMPLE_INPUT, disabledGuardrails: DISABLED_GUARDRAILS }),
});
const body: unknown = await response.json().catch(() => null);
if (!response.ok) throw new Error(readErrorMessage(body) ?? `HTTP ${response.status}`);
const meta = findAudioMeta(body);
if (typeof meta?.clipsProcessed === "number" && meta.clipsProcessed >= 1) {
setResult(
t("modalityBridgeAudioTestOk", {
count: meta.clipsProcessed,
model: String(meta.sttModel ?? "unknown"),
})
);
} else {
setResult(t("modalityBridgeAudioTestNoop"));
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setResult(t("modalityBridgeAudioTestError", { message }));
} finally {
setRunning(false);
}
};
return (
<div className="flex flex-col items-start gap-2">
<button
type="button"
className="rounded-control border border-border px-3 py-2 text-sm font-medium hover:bg-surface-2 disabled:cursor-not-allowed disabled:opacity-50"
disabled={running}
onClick={() => void runTest()}
>
{t(running ? "modalityBridgeAudioTestRunning" : "modalityBridgeAudioTestButton")}
</button>
{result && (
<p className="text-xs text-text-muted" role="status">
{result}
</p>
)}
</div>
);
}

View File

@@ -5,6 +5,7 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
import ModalityBridgeComingSoonTab from "@/app/(dashboard)/dashboard/settings/components/modalityBridge/ModalityBridgeComingSoonTab";
import ModalityBridgeAudioTab from "@/app/(dashboard)/dashboard/settings/components/modalityBridge/ModalityBridgeAudioTab";
import ModalityBridgeVisionTab from "@/app/(dashboard)/dashboard/settings/components/modalityBridge/ModalityBridgeVisionTab";
type TabId = "vision" | "audio" | "video";
@@ -61,9 +62,7 @@ function ModalityBridgePageContent() {
<div id="modality-bridge-tabpanel" role="tabpanel">
{activeTab === "vision" && <ModalityBridgeVisionTab />}
{activeTab === "audio" && (
<ModalityBridgeComingSoonTab bodyKey="modalityBridgeAudioComingSoon" />
)}
{activeTab === "audio" && <ModalityBridgeAudioTab />}
{activeTab === "video" && (
<ModalityBridgeComingSoonTab bodyKey="modalityBridgeVideoComingSoon" />
)}

View File

@@ -38,6 +38,7 @@ export async function GET(request: Request) {
id: model.id,
name: model.name || model.root || model.id,
type: model.type || "chat",
...(typeof model.subtype === "string" ? { subtype: model.subtype } : {}),
custom: model.custom === true,
...(model.free === true ? { free: true } : {}),
...(model.capabilities ? { capabilities: model.capabilities } : {}),

View File

@@ -14,17 +14,35 @@ import { buildErrorBody } from "@omniroute/open-sse/utils/error";
* Returns a 424 (Failed Dependency) response with a clear, sanitized message
* when the connection carries that flag; otherwise null (proceed normally).
*/
const STALE_ENCRYPTION_MESSAGE =
"Stored API key cannot be decrypted (STORAGE_ENCRYPTION_KEY changed or unset). Re-enter the API key.";
export function buildStaleEncryptionKeyResponse(
connection: { credentialDecryptFailed?: unknown } | null | undefined
connection:
| {
credentialDecryptFailed?: unknown;
id?: unknown;
provider?: unknown;
}
| null
| undefined
): NextResponse | null {
if (!connection || connection.credentialDecryptFailed !== true) return null;
// #9927 — surface WHICH credential failed plus the recovery path so the
// dashboard points the operator at the account to re-authenticate instead of
// a generic "API key cannot be decrypted".
const provider = typeof connection.provider === "string" ? connection.provider : "";
const id = typeof connection.id === "string" ? connection.id : "";
const identity = [provider && `provider "${provider}"`, id && `connection ${id}`]
.filter(Boolean)
.join(", ");
const message =
`Stored credential${identity ? ` for ${identity}` : ""} cannot be decrypted ` +
`(STORAGE_ENCRYPTION_KEY changed or unset). Re-authenticate this account, or verify ` +
`STORAGE_ENCRYPTION_KEY matches the key used to store it.`;
// buildErrorBody sanitizes the message (Rule #12); override the type so the
// client can key off the specific stale-encryption cause.
const body = buildErrorBody(424, STALE_ENCRYPTION_MESSAGE);
const body = buildErrorBody(424, message);
body.error.type = "storage_encryption_stale";
return NextResponse.json(body, { status: 424 });
}

View File

@@ -0,0 +1,69 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { createProviderConnection, getProviderConnections } from "@/lib/db/providers";
import {
getEligibleFreeOnboardingProviders,
selectUnconfiguredFreeOnboardingProviders,
setupFreeProviderConnections,
withFreeProviderSetupLock,
} from "@/lib/providers/freeOnboarding";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
const setupSchema = z.object({
providerIds: z.array(z.string().trim().min(1)).min(1).max(20),
confirmed: z.literal(true),
});
export async function GET(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
try {
const candidates = getEligibleFreeOnboardingProviders();
const connections = await getProviderConnections();
return NextResponse.json({
providers: selectUnconfiguredFreeOnboardingProviders(candidates, connections),
});
} catch {
return NextResponse.json({ error: "Failed to load free providers" }, { status: 500 });
}
}
export async function POST(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
let rawBody: unknown;
try {
rawBody = await request.json();
} catch {
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}
const validation = validateBody(setupSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
try {
const result = await withFreeProviderSetupLock(() =>
setupFreeProviderConnections({
requestedIds: validation.data.providerIds,
candidates: getEligibleFreeOnboardingProviders(),
listExisting: () => getProviderConnections(),
create: (input) => createProviderConnection(input),
})
);
return NextResponse.json(result);
} catch (error) {
if (error instanceof Error && error.message.startsWith("Ineligible free provider IDs:")) {
return NextResponse.json(
{ error: "One or more providers are not eligible" },
{ status: 400 }
);
}
return NextResponse.json({ error: "Failed to set up free providers" }, { status: 500 });
}
}

View File

@@ -19,6 +19,7 @@ import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy";
import { v1ImageGenerationSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { getComboByName } from "@/lib/db/combos";
import { getAllCustomModels } from "@/lib/db/models";
import { resolveProxyForConnection } from "@/lib/db/settings";
import { resolveImageRouteModel } from "@/lib/images/imageRouteModel";
@@ -116,6 +117,24 @@ async function postHandler(request, context) {
const policy = await enforceApiKeyPolicy(request, body.model);
if (policy.rejection) return policy.rejection;
// #9239: Detect combo name and divert to full image combo execution.
// Checks before resolveImageRouteModel so we skip single-target flattening.
if (body.model && typeof body.model === "string" && !body.model.includes("/")) {
const combo = await getComboByName(body.model as string);
if (combo) {
const { executeImageCombo } = await import(
"@omniroute/open-sse/services/imageCombo"
);
return executeImageCombo(
body.model as string,
body,
{ request, policy },
startTime,
log
);
}
}
// #3205/#3215: resolve a combo/alias name (`image`) or a user-prefixed custom image
// model (`myImg/gpt-image-2`) to its internal `<nodeId>/<model>` form so the
// custom-model lookup and handler's resolvedProvider extraction resolve correctly.
@@ -307,7 +326,9 @@ async function postHandler(request, context) {
});
}
const errorPayload = toJsonErrorPayload((result as any).error, "Image generation provider error");
const errorPayload = toJsonErrorPayload((result as any).error, "Image generation provider error") as {
error?: { message?: string };
};
const message =
typeof errorPayload?.error?.message === "string"
? errorPayload.error.message

View File

@@ -0,0 +1,87 @@
/**
* Muse Code CLI proprietary model catalog endpoint.
*
* Muse CLI calls GET /muse-code/models (or --base-url/muse-code/models)
* to discover available models. Returns the proprietary Muse format:
*
* { object: "list", data: [{ id, object, created, owned_by, metadata }] }
*
* Each model's metadata includes: name, family, reasoning, tool_call,
* modalities, limit, cost.
*/
import { muse_codeProvider } from "@omniroute/open-sse/config/providers/registry/muse-code/index.ts";
const MUSECODE_TIMESTAMP = Math.floor(Date.now() / 1000);
interface MuseCodeModel {
id: string;
object: "model";
created: number;
owned_by: string;
metadata: {
name: string;
family: string;
reasoning: boolean;
tool_call: boolean;
modalities: string[];
limit: number;
cost: number;
};
}
function buildModelCatalog(): MuseCodeModel[] {
const data: MuseCodeModel[] = [];
for (const model of muse_codeProvider.models) {
let family = "llama";
if (model.id.includes("llama-4")) family = "llama-4";
else if (model.id.includes("llama-3.3")) family = "llama-3.3";
else if (model.id.includes("llama-3.2")) family = "llama-3.2";
else if (model.id.includes("llama-3.1")) family = "llama-3.1";
const modalities: string[] = ["text"];
if (model.supportsVision) modalities.push("image");
data.push({
id: model.id,
object: "model",
created: MUSECODE_TIMESTAMP,
owned_by: "meta",
metadata: {
name: model.name,
family,
reasoning: !!model.supportsReasoning,
tool_call: !!model.toolCalling,
modalities,
limit: model.contextLength ?? 200_000,
cost: model.id.includes("maverick") || model.id.includes("405b") ? 3 : 1,
},
});
}
return data;
}
// Cache the catalog for the lifetime of the process — model list is static.
const CATALOG = buildModelCatalog();
const CATALOG_PAYLOAD = JSON.stringify({ object: "list", data: CATALOG }, null, 2);
export async function OPTIONS() {
return new Response(null, {
headers: {
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "*",
},
});
}
export async function GET() {
return new Response(CATALOG_PAYLOAD, {
status: 200,
headers: {
"content-type": "application/json",
"cache-control": "public, max-age=3600",
},
});
}

View File

@@ -4744,7 +4744,26 @@
"configure": "تكوين مقدمي الخدمات"
},
"tierFlowDiagramAlt": "OmniRoute مخطط احتياطي ثلاثي الطبقات",
"apiKeyMgmt": "إدارة مفتاح واجهة برمجة التطبيقات (API)"
"apiKeyMgmt": "إدارة مفتاح واجهة برمجة التطبيقات (API)",
"freeProviders": {
"title": "__MISSING__:Set up free providers",
"description": "__MISSING__:Optionally enable selected no-signup providers. Review each provider's notice and site before continuing; availability and rate limits are controlled by third parties.",
"loading": "__MISSING__:Loading free providers...",
"loadFailed": "__MISSING__:Could not load free providers.",
"alreadyConfigured": "__MISSING__:All eligible free providers are already configured.",
"reviewProviderSite": "__MISSING__:Review provider site and terms",
"confirmation": "__MISSING__:I reviewed these third-party providers and want OmniRoute to create the selected connections.",
"setupSelected": "__MISSING__:Set up selected providers",
"settingUp": "__MISSING__:Setting up...",
"setupFailed": "__MISSING__:Could not set up the selected providers. Nothing existing was changed.",
"retryFailed": "__MISSING__:Retry failed providers",
"orUseApiKey": "__MISSING__:or connect with an API key",
"result": {
"created": "__MISSING__:created",
"skipped": "__MISSING__:already configured",
"failed": "__MISSING__:failed"
}
}
},
"providers": {
"title": "المزودون",
@@ -7684,7 +7703,19 @@
"modalityBridgeVideoComingSoon": "تجسير الفيديو (عينة الإطار + الترجمة) في قائمة الانتظار - راجع المشكلة #9760.",
"modalityBridgeMovedTitle": "تم نقل Vision Bridge",
"modalityBridgeMovedBody": "إعدادات Vision Bridge الآن موجودة في صفحة Modality Bridge المخصصة.",
"modalityBridgeMovedCta": "فتح إعدادات جسر الوضعية"
"modalityBridgeMovedCta": "فتح إعدادات جسر الوضعية",
"modalityBridgeAudioTitle": "جسر الصوت",
"modalityBridgeAudioDesc": "قم بنسخ الصوت باستخدام نموذج تحويل الكلام إلى نص قبل المتابعة مع نموذج النص المختار.",
"modalityBridgeAudioEnabled": "تفعيل جسر الصوت",
"modalityBridgeAudioEnabledDesc": "استبدل أجزاء الصوت بالنصوص عندما لا يمكن للنموذج المستهدف معالجة الصوت.",
"modalityBridgeAudioModel": "نموذج تحويل الكلام إلى نص",
"modalityBridgeAudioModelAuto": "تلقائي (أول مزود STT متصل)",
"modalityBridgeAudioMaxClips": "أقصى عدد لمقاطع الصوت لكل طلب",
"modalityBridgeAudioTestButton": "اختبر باستخدام صوت عينة",
"modalityBridgeAudioTestRunning": "اختبار الصوت…",
"modalityBridgeAudioTestOk": "جسر الصوت OK — تم نسخ {count} مقطع(ات) بواسطة {model}",
"modalityBridgeAudioTestNoop": "لم يتم تفعيل جسر الصوت (قد يدعم الهدف الصوت، لم يتم الاتصال بمزود STT، أو أن الجسر معطل)",
"modalityBridgeAudioTestError": "فشل اختبار الصوت: {message}"
},
"contextRtk": {
"title": "محرك آر تي كيه",

View File

@@ -4744,7 +4744,26 @@
"configure": "Provayderləri konfiqurasiya edin"
},
"tierFlowDiagramAlt": "OmniRoute 3 səviyyəli ehtiyat diaqramı",
"apiKeyMgmt": "API Açar İdarəetmə"
"apiKeyMgmt": "API Açar İdarəetmə",
"freeProviders": {
"title": "__MISSING__:Set up free providers",
"description": "__MISSING__:Optionally enable selected no-signup providers. Review each provider's notice and site before continuing; availability and rate limits are controlled by third parties.",
"loading": "__MISSING__:Loading free providers...",
"loadFailed": "__MISSING__:Could not load free providers.",
"alreadyConfigured": "__MISSING__:All eligible free providers are already configured.",
"reviewProviderSite": "__MISSING__:Review provider site and terms",
"confirmation": "__MISSING__:I reviewed these third-party providers and want OmniRoute to create the selected connections.",
"setupSelected": "__MISSING__:Set up selected providers",
"settingUp": "__MISSING__:Setting up...",
"setupFailed": "__MISSING__:Could not set up the selected providers. Nothing existing was changed.",
"retryFailed": "__MISSING__:Retry failed providers",
"orUseApiKey": "__MISSING__:or connect with an API key",
"result": {
"created": "__MISSING__:created",
"skipped": "__MISSING__:already configured",
"failed": "__MISSING__:failed"
}
}
},
"providers": {
"title": "Providers",
@@ -7684,7 +7703,19 @@
"modalityBridgeVideoComingSoon": "Video bridging (frame sampling + captioning) arxa planda var — məsələyə baxın #9760.",
"modalityBridgeMovedTitle": "Vision Bridge köçürüldü",
"modalityBridgeMovedBody": "Vision Bridge parametrləri indi xüsusi Modality Bridge səhifəsində mövcuddur.",
"modalityBridgeMovedCta": "Modallıq Körpüsü parametrlərini açın"
"modalityBridgeMovedCta": "Modallıq Körpüsü parametrlərini açın",
"modalityBridgeAudioTitle": "Səs Körpüsü",
"modalityBridgeAudioDesc": "Seçilmiş mətn modelinə davam etməzdən əvvəl səsdən mətbə çevirmək üçün nitqdən mətbə modelindən istifadə edin.",
"modalityBridgeAudioEnabled": "Səs Körpüsünü Aktivləşdirin",
"modalityBridgeAudioEnabledDesc": "Hədəf model audio emal edə bilmədikdə audio hissələri transkriptlərlə əvəz edin.",
"modalityBridgeAudioModel": "Nitqdan mətnə model",
"modalityBridgeAudioModelAuto": "Avtomatik (ilk qoşulmuş STT təminatçısı)",
"modalityBridgeAudioMaxClips": "Tələb başına maksimum audio kliplər",
"modalityBridgeAudioTestButton": "Nümunə audio ilə test edin",
"modalityBridgeAudioTestRunning": "Səsi test edir…",
"modalityBridgeAudioTestOk": "Audio Bridge OK — {count} klip(ler) {model} tərəfindən transkribasiya edildi",
"modalityBridgeAudioTestNoop": "Audio Bridge aktivləşmədi (hədəf audio dəstəkləyə bilər, STT provayderi qoşulmayıb, ya da körpü deaktivdir)",
"modalityBridgeAudioTestError": "Səs testi uğursuz oldu: {message}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -4744,7 +4744,26 @@
"configure": "Конфигурирайте доставчици"
},
"tierFlowDiagramAlt": "3-степенна резервна диаграма на OmniRoute",
"apiKeyMgmt": "API Key Mgmt"
"apiKeyMgmt": "API Key Mgmt",
"freeProviders": {
"title": "__MISSING__:Set up free providers",
"description": "__MISSING__:Optionally enable selected no-signup providers. Review each provider's notice and site before continuing; availability and rate limits are controlled by third parties.",
"loading": "__MISSING__:Loading free providers...",
"loadFailed": "__MISSING__:Could not load free providers.",
"alreadyConfigured": "__MISSING__:All eligible free providers are already configured.",
"reviewProviderSite": "__MISSING__:Review provider site and terms",
"confirmation": "__MISSING__:I reviewed these third-party providers and want OmniRoute to create the selected connections.",
"setupSelected": "__MISSING__:Set up selected providers",
"settingUp": "__MISSING__:Setting up...",
"setupFailed": "__MISSING__:Could not set up the selected providers. Nothing existing was changed.",
"retryFailed": "__MISSING__:Retry failed providers",
"orUseApiKey": "__MISSING__:or connect with an API key",
"result": {
"created": "__MISSING__:created",
"skipped": "__MISSING__:already configured",
"failed": "__MISSING__:failed"
}
}
},
"providers": {
"title": "Доставчици",
@@ -7684,7 +7703,19 @@
"modalityBridgeVideoComingSoon": "Видео свързване (извадка на кадри + надписи) е в списъка със задачи — вижте проблема #9760.",
"modalityBridgeMovedTitle": "Vision Bridge преместен",
"modalityBridgeMovedBody": "Настройките на Vision Bridge вече са налични на специализираната страница Modality Bridge.",
"modalityBridgeMovedCta": "Отворете настройките на Modality Bridge"
"modalityBridgeMovedCta": "Отворете настройките на Modality Bridge",
"modalityBridgeAudioTitle": "Аудио Мост",
"modalityBridgeAudioDesc": "Транскрибирайте аудио с модел за разпознаване на реч, преди да продължите с избрания текстов модел.",
"modalityBridgeAudioEnabled": "Активирайте Audio Bridge",
"modalityBridgeAudioEnabledDesc": "Заменете аудио частите с транскрипции, когато целевият модел не може да обработва аудио.",
"modalityBridgeAudioModel": "Модел за преобразуване на реч в текст",
"modalityBridgeAudioModelAuto": "Авто (първият свързан доставчик на STT)",
"modalityBridgeAudioMaxClips": "Максимален брой аудио клипове на заявка",
"modalityBridgeAudioTestButton": "Тест с примерен аудио файл",
"modalityBridgeAudioTestRunning": "Тестване на аудио…",
"modalityBridgeAudioTestOk": "Audio Bridge OK — {count} клип(а) транскрибирани от {model}",
"modalityBridgeAudioTestNoop": "Audio Bridge не беше активиран (целта може да поддържа аудио, няма свързан STT доставчик или моста е деактивиран)",
"modalityBridgeAudioTestError": "Тестът на звука не успя: {message}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -4744,7 +4744,26 @@
"configure": "প্রদানকারী কনফিগার করুন"
},
"tierFlowDiagramAlt": "OmniRoute 3-স্তরের ফলব্যাক ডায়াগ্রাম",
"apiKeyMgmt": "API Key Mgmt"
"apiKeyMgmt": "API Key Mgmt",
"freeProviders": {
"title": "__MISSING__:Set up free providers",
"description": "__MISSING__:Optionally enable selected no-signup providers. Review each provider's notice and site before continuing; availability and rate limits are controlled by third parties.",
"loading": "__MISSING__:Loading free providers...",
"loadFailed": "__MISSING__:Could not load free providers.",
"alreadyConfigured": "__MISSING__:All eligible free providers are already configured.",
"reviewProviderSite": "__MISSING__:Review provider site and terms",
"confirmation": "__MISSING__:I reviewed these third-party providers and want OmniRoute to create the selected connections.",
"setupSelected": "__MISSING__:Set up selected providers",
"settingUp": "__MISSING__:Setting up...",
"setupFailed": "__MISSING__:Could not set up the selected providers. Nothing existing was changed.",
"retryFailed": "__MISSING__:Retry failed providers",
"orUseApiKey": "__MISSING__:or connect with an API key",
"result": {
"created": "__MISSING__:created",
"skipped": "__MISSING__:already configured",
"failed": "__MISSING__:failed"
}
}
},
"providers": {
"title": "Providers",
@@ -7684,7 +7703,19 @@
"modalityBridgeVideoComingSoon": "ভিডিও ব্রিজিং (ফ্রেম স্যাম্পলিং + ক্যাপশনিং) ব্যাকলগে রয়েছে — সমস্যা #9760 দেখুন।",
"modalityBridgeMovedTitle": "ভিশন ব্রিজ স্থানান্তরিত হয়েছে",
"modalityBridgeMovedBody": "ভিশন ব্রিজের সেটিংস এখন নির্দিষ্ট মডালিটি ব্রিজ পৃষ্ঠায় লাইভ।",
"modalityBridgeMovedCta": "মোডালিটি ব্রিজ সেটিংস খুলুন"
"modalityBridgeMovedCta": "মোডালিটি ব্রিজ সেটিংস খুলুন",
"modalityBridgeAudioTitle": "অডিও ব্রিজ",
"modalityBridgeAudioDesc": "নির্বাচিত টেক্সট মডেলের সাথে এগিয়ে যাওয়ার আগে একটি স্পিচ-টু-টেক্সট মডেলের সাহায্যে অডিও ট্রান্সক্রাইব করুন।",
"modalityBridgeAudioEnabled": "অডিও ব্রিজ সক্রিয় করুন",
"modalityBridgeAudioEnabledDesc": "লক্ষ্য মডেল অডিও প্রক্রিয়া করতে না পারলে অডিও অংশগুলি ট্রান্সক্রিপ্টের সাথে প্রতিস্থাপন করুন।",
"modalityBridgeAudioModel": "স্পিচ-টু-টেক্সট মডেল",
"modalityBridgeAudioModelAuto": "অটো (প্রথম সংযুক্ত STT প্রদানকারী)",
"modalityBridgeAudioMaxClips": "প্রতি অনুরোধে সর্বাধিক অডিও ক্লিপ",
"modalityBridgeAudioTestButton": "নমুনা অডিওর সাথে পরীক্ষা করুন",
"modalityBridgeAudioTestRunning": "অডিও পরীক্ষা করা হচ্ছে…",
"modalityBridgeAudioTestOk": "অডিও ব্রিজ ঠিক আছে — {count} ক্লিপ(গুলি) {model} দ্বারা ট্রান্সক্রাইব করা হয়েছে",
"modalityBridgeAudioTestNoop": "অডিও ব্রিজ সক্রিয় হয়নি (লক্ষ্য অডিও সমর্থন করতে পারে, কোন STT প্রদানকারী সংযুক্ত নয়, অথবা ব্রিজ নিষ্ক্রিয় রয়েছে)",
"modalityBridgeAudioTestError": "অডিও পরীক্ষা ব্যর্থ: {message}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -4744,7 +4744,26 @@
"configure": "Nakonfigurujte poskytovatele"
},
"tierFlowDiagramAlt": "Třívrstvý záložní diagram OmniRoute",
"apiKeyMgmt": "Správa API klíčů"
"apiKeyMgmt": "Správa API klíčů",
"freeProviders": {
"title": "__MISSING__:Set up free providers",
"description": "__MISSING__:Optionally enable selected no-signup providers. Review each provider's notice and site before continuing; availability and rate limits are controlled by third parties.",
"loading": "__MISSING__:Loading free providers...",
"loadFailed": "__MISSING__:Could not load free providers.",
"alreadyConfigured": "__MISSING__:All eligible free providers are already configured.",
"reviewProviderSite": "__MISSING__:Review provider site and terms",
"confirmation": "__MISSING__:I reviewed these third-party providers and want OmniRoute to create the selected connections.",
"setupSelected": "__MISSING__:Set up selected providers",
"settingUp": "__MISSING__:Setting up...",
"setupFailed": "__MISSING__:Could not set up the selected providers. Nothing existing was changed.",
"retryFailed": "__MISSING__:Retry failed providers",
"orUseApiKey": "__MISSING__:or connect with an API key",
"result": {
"created": "__MISSING__:created",
"skipped": "__MISSING__:already configured",
"failed": "__MISSING__:failed"
}
}
},
"providers": {
"title": "Poskytovatelé",
@@ -7684,7 +7703,19 @@
"modalityBridgeVideoComingSoon": "Video bridging (frame sampling + captioning) je na backlogu — viz problém #9760.",
"modalityBridgeMovedTitle": "Vision Bridge přesunuto",
"modalityBridgeMovedBody": "Nastavení Vision Bridge nyní žije na vyhrazené stránce Modality Bridge.",
"modalityBridgeMovedCta": "Otevřít nastavení Modality Bridge"
"modalityBridgeMovedCta": "Otevřít nastavení Modality Bridge",
"modalityBridgeAudioTitle": "Audio Bridge",
"modalityBridgeAudioDesc": "Přepište audio pomocí modelu převodu řeči na text, než pokračujete s vybraným textovým modelem.",
"modalityBridgeAudioEnabled": "Povolit Audio Bridge",
"modalityBridgeAudioEnabledDesc": "Nahraďte audio části přepisy, když cílový model nemůže zpracovat audio.",
"modalityBridgeAudioModel": "Model pro převod řeči na text",
"modalityBridgeAudioModelAuto": "Auto (první připojený poskytovatel STT)",
"modalityBridgeAudioMaxClips": "Maximální počet audio klipů na požadavek",
"modalityBridgeAudioTestButton": "Test s ukázkovým zvukem",
"modalityBridgeAudioTestRunning": "Testování zvuku…",
"modalityBridgeAudioTestOk": "Audio Bridge OK — {count} klip(y) přepsány pomocí {model}",
"modalityBridgeAudioTestNoop": "Audio Bridge se nepodařilo aktivovat (cílové zařízení může podporovat audio, žádný poskytovatel STT není připojen, nebo je most deaktivován)",
"modalityBridgeAudioTestError": "Test zvuku selhal: {message}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -4744,7 +4744,26 @@
"configure": "Konfigurer udbydere"
},
"tierFlowDiagramAlt": "OmniRoute 3-tiers fallback diagram",
"apiKeyMgmt": "API Key Mgmt"
"apiKeyMgmt": "API Key Mgmt",
"freeProviders": {
"title": "__MISSING__:Set up free providers",
"description": "__MISSING__:Optionally enable selected no-signup providers. Review each provider's notice and site before continuing; availability and rate limits are controlled by third parties.",
"loading": "__MISSING__:Loading free providers...",
"loadFailed": "__MISSING__:Could not load free providers.",
"alreadyConfigured": "__MISSING__:All eligible free providers are already configured.",
"reviewProviderSite": "__MISSING__:Review provider site and terms",
"confirmation": "__MISSING__:I reviewed these third-party providers and want OmniRoute to create the selected connections.",
"setupSelected": "__MISSING__:Set up selected providers",
"settingUp": "__MISSING__:Setting up...",
"setupFailed": "__MISSING__:Could not set up the selected providers. Nothing existing was changed.",
"retryFailed": "__MISSING__:Retry failed providers",
"orUseApiKey": "__MISSING__:or connect with an API key",
"result": {
"created": "__MISSING__:created",
"skipped": "__MISSING__:already configured",
"failed": "__MISSING__:failed"
}
}
},
"providers": {
"title": "Udbydere",
@@ -7684,7 +7703,19 @@
"modalityBridgeVideoComingSoon": "Video brokering (rammesampling + undertekster) er på backloggen — se problem #9760.",
"modalityBridgeMovedTitle": "Vision Bridge flyttet",
"modalityBridgeMovedBody": "Vision Bridge-indstillinger er nu live på den dedikerede Modality Bridge-side.",
"modalityBridgeMovedCta": "Åbn Modality Bridge-indstillinger"
"modalityBridgeMovedCta": "Åbn Modality Bridge-indstillinger",
"modalityBridgeAudioTitle": "Audio Bro",
"modalityBridgeAudioDesc": "Transskriber lyd med en tale-til-tekst model, før du fortsætter med den valgte tekstmodel.",
"modalityBridgeAudioEnabled": "Aktivér Audio Bridge",
"modalityBridgeAudioEnabledDesc": "Erstat lyddele med transkriptioner, når målmodellen ikke kan behandle lyd.",
"modalityBridgeAudioModel": "Tale-til-tekst model",
"modalityBridgeAudioModelAuto": "Auto (første tilsluttede STT-udbyder)",
"modalityBridgeAudioMaxClips": "Maksimalt antal lydklip pr. anmodning",
"modalityBridgeAudioTestButton": "Test med prøveaudio",
"modalityBridgeAudioTestRunning": "Tester lyd…",
"modalityBridgeAudioTestOk": "Audio Bridge OK — {count} klip(klip) transskriberet af {model}",
"modalityBridgeAudioTestNoop": "Audio Bridge blev ikke aktiveret (målet kan understøtte lyd, ingen STT-udbyder er tilsluttet, eller broen er deaktiveret)",
"modalityBridgeAudioTestError": "Lydtest mislykkedes: {message}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -4744,7 +4744,26 @@
"configure": "Anbieter konfigurieren"
},
"tierFlowDiagramAlt": "OmniRoute 3-Stufen-Fallback-Diagramm",
"apiKeyMgmt": "API-Schlüsselverwaltung"
"apiKeyMgmt": "API-Schlüsselverwaltung",
"freeProviders": {
"title": "__MISSING__:Set up free providers",
"description": "__MISSING__:Optionally enable selected no-signup providers. Review each provider's notice and site before continuing; availability and rate limits are controlled by third parties.",
"loading": "__MISSING__:Loading free providers...",
"loadFailed": "__MISSING__:Could not load free providers.",
"alreadyConfigured": "__MISSING__:All eligible free providers are already configured.",
"reviewProviderSite": "__MISSING__:Review provider site and terms",
"confirmation": "__MISSING__:I reviewed these third-party providers and want OmniRoute to create the selected connections.",
"setupSelected": "__MISSING__:Set up selected providers",
"settingUp": "__MISSING__:Setting up...",
"setupFailed": "__MISSING__:Could not set up the selected providers. Nothing existing was changed.",
"retryFailed": "__MISSING__:Retry failed providers",
"orUseApiKey": "__MISSING__:or connect with an API key",
"result": {
"created": "__MISSING__:created",
"skipped": "__MISSING__:already configured",
"failed": "__MISSING__:failed"
}
}
},
"providers": {
"title": "Anbieter",
@@ -7684,7 +7703,19 @@
"modalityBridgeVideoComingSoon": "Video-Bridging (Frame-Sampling + Untertitelung) steht auf der Warteliste siehe Issue #9760.",
"modalityBridgeMovedTitle": "Vision Bridge verschoben",
"modalityBridgeMovedBody": "Die Vision Bridge-Einstellungen sind jetzt auf der speziellen Modality Bridge-Seite verfügbar.",
"modalityBridgeMovedCta": "Modality Bridge-Einstellungen öffnen"
"modalityBridgeMovedCta": "Modality Bridge-Einstellungen öffnen",
"modalityBridgeAudioTitle": "Audio-Brücke",
"modalityBridgeAudioDesc": "Transkribiere Audio mit einem Sprach-zu-Text-Modell, bevor du mit dem gewählten Textmodell fortfährst.",
"modalityBridgeAudioEnabled": "Audio-Brücke aktivieren",
"modalityBridgeAudioEnabledDesc": "Ersetzen Sie Audioabschnitte durch Transkripte, wenn das Zielmodell Audio nicht verarbeiten kann.",
"modalityBridgeAudioModel": "Spracherkennungsmodell",
"modalityBridgeAudioModelAuto": "Auto (erster verbundener STT-Anbieter)",
"modalityBridgeAudioMaxClips": "Maximale Audio-Clips pro Anfrage",
"modalityBridgeAudioTestButton": "Testen mit Beispielaudio",
"modalityBridgeAudioTestRunning": "Audio wird getestet…",
"modalityBridgeAudioTestOk": "Audio Bridge OK — {count} Clip(s) von {model} transkribiert",
"modalityBridgeAudioTestNoop": "Audio Bridge wurde nicht aktiviert (das Ziel unterstützt möglicherweise Audio, kein STT-Anbieter ist verbunden oder die Brücke ist deaktiviert)",
"modalityBridgeAudioTestError": "Audiotest fehlgeschlagen: {message}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -4142,6 +4142,12 @@
"providerModelLabel": "Provider / Model",
"noRemoteProviders": "No providers with configured API key",
"selectProviderModel": "Select a model",
"customBaseUrlLabel": "Custom base URL (optional)",
"customModelIdLabel": "Custom model ID (optional)",
"customEndpointHelp": "Use an OpenAI-compatible /embeddings endpoint. Local endpoints are allowed; cloud metadata targets are blocked.",
"customEndpointSave": "Apply custom endpoint",
"customEndpointInvalid": "Enter both a valid HTTP(S) base URL and a model ID, or leave both fields empty.",
"customEndpointSaveFailed": "The custom embedding endpoint could not be saved.",
"staticEnabledLabel": "Enable Static Potion",
"staticEnabledDesc": "Download and use potion-base-8M model locally",
"transformersEnabledLabel": "Enable Transformers.js",
@@ -4744,7 +4750,26 @@
"configure": "Configure providers"
},
"tierFlowDiagramAlt": "OmniRoute 3-tier fallback diagram",
"apiKeyMgmt": "API Key Management"
"apiKeyMgmt": "API Key Management",
"freeProviders": {
"title": "Set up free providers",
"description": "Optionally enable selected no-signup providers. Review each provider's notice and site before continuing; availability and rate limits are controlled by third parties.",
"loading": "Loading free providers...",
"loadFailed": "Could not load free providers.",
"alreadyConfigured": "All eligible free providers are already configured.",
"reviewProviderSite": "Review provider site and terms",
"confirmation": "I reviewed these third-party providers and want OmniRoute to create the selected connections.",
"setupSelected": "Set up selected providers",
"settingUp": "Setting up...",
"setupFailed": "Could not set up the selected providers. Nothing existing was changed.",
"retryFailed": "Retry failed providers",
"orUseApiKey": "or connect with an API key",
"result": {
"created": "created",
"skipped": "already configured",
"failed": "failed"
}
}
},
"providers": {
"title": "Providers",
@@ -4948,6 +4973,7 @@
"baseUrlHint": "Required. Provider API base URL.",
"iconUrlLabel": "Icon URL",
"iconUrlHint": "Optional. Image URL shown as this provider's icon.",
"iconUrlInvalid": "Invalid icon URL. Use an http(s):// or data:image/*;base64 URL.",
"anthropicPrefixPlaceholder": "ac-prod",
"openaiPrefixPlaceholder": "oc-prod",
"anthropicBaseUrlPlaceholder": "https://api.anthropic.com/v1",
@@ -7684,7 +7710,19 @@
"modalityBridgeVideoComingSoon": "Video bridging (frame sampling + captioning) is on the backlog — see issue #9760.",
"modalityBridgeMovedTitle": "Vision Bridge moved",
"modalityBridgeMovedBody": "Vision Bridge settings now live in the dedicated Modality Bridge page.",
"modalityBridgeMovedCta": "Open Modality Bridge settings"
"modalityBridgeMovedCta": "Open Modality Bridge settings",
"modalityBridgeAudioTitle": "Audio Bridge",
"modalityBridgeAudioDesc": "Transcribe audio with a speech-to-text model before continuing with the chosen text model.",
"modalityBridgeAudioEnabled": "Enable Audio Bridge",
"modalityBridgeAudioEnabledDesc": "Replace audio parts with transcripts when the target model cannot process audio.",
"modalityBridgeAudioModel": "Speech-to-text model",
"modalityBridgeAudioModelAuto": "Auto (first connected STT provider)",
"modalityBridgeAudioMaxClips": "Max audio clips per request",
"modalityBridgeAudioTestButton": "Test with sample audio",
"modalityBridgeAudioTestRunning": "Testing audio…",
"modalityBridgeAudioTestOk": "Audio Bridge OK — {count} clip(s) transcribed by {model}",
"modalityBridgeAudioTestNoop": "Audio Bridge did not activate (the target may support audio, no STT provider is connected, or the bridge is disabled)",
"modalityBridgeAudioTestError": "Audio test failed: {message}"
},
"contextRtk": {
"title": "RTK Engine",

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